Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 26 additions & 2 deletions .agents/skills/create-update-service/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,8 +67,9 @@ For a new or updated service type, check these areas:
2. The `ServiceType` subclass exposes the right `type`, `model_class`, `dispatch_types`, `allowed_fields`, and serializer configuration.
3. Related nested objects are handled in `after_create`, update helpers, or custom methods when needed.
4. Context/schema methods are implemented if the service emits data for downstream nodes.
5. The service is registered in `backend/src/baserow/contrib/integrations/apps.py`.
6. A migration is added if models changed.
5. Serialized foreign keys or IDs are migrated during import/export.
6. The service is registered in `backend/src/baserow/contrib/integrations/apps.py`.
7. A migration is added if models changed.

For a new or updated integration type, check these areas:

Expand All @@ -90,6 +91,29 @@ Common backend files to inspect:
- `backend/src/baserow/contrib/automation/nodes/**`
- `backend/src/baserow/contrib/dashboard/**`

### Import/Export ID Migration

When a service serializes a foreign key or path containing IDs, migrate it through
`id_mapping` during import.

- Use `deserialize_property()` for simple fields like `workflow_id`, `table_id`,
or `field_id`.
- Use `create_instance_from_serialized()` for nested rows or lists that must be
recreated after the service exists.
- Use `import_serialized()` when IDs or UUIDs must be reserved before normal
deserialization.
- For data-provider paths, also check `import_path()` and
`import_context_path()`. Formula fields are normally migrated separately via
the `import_formula` callback.
- Common keys include `automation_workflows`, `automation_workflow_nodes`,
`builder_*`, `database_tables`, `database_fields`, `integrations`, and
`services`.

Examples: `CoreStartWorkflowServiceType.deserialize_property()` for workflow IDs,
`CoreRouterServiceType` for edge UIDs, local Baserow service types for table/field
IDs, and premium grouped aggregate services for nested field references. Add a
regression test that imports with an old-to-new `id_mapping`.

## Product Surface Checklist

### Builder or Dashboard data source
Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -170,3 +170,4 @@ web-frontend/.nuxt-storybook/
web-frontend/storybook-static

.agent
MEMORY.md
10 changes: 9 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -63,5 +63,13 @@ Do not commit secrets or local overrides. Use `.env.local` for development, keep
## Good practices

- On a specific branch, always merge backend migrations file instead of creating new ones.
- Django migrations must be executed with zero downtime. This means the new database schema must remain compatible with the previous application version during the deployment.
- Every new field must define a `db_default`.
- Do not remove fields unless you are certain they are no longer used by the previous application version. Instead, keep the field and add a `# TODO ZDM: remove this field in the next version` comment so it can be safely removed in a subsequent release.
- CSS classes respect BEM methodology.
- When working on translations, only update english unless told otherwise. Other languages are handled with weblate. Don't nest keys too much, just keep one level of nesting.
- When working on translations, only update english unless told otherwise. Other languages are handled with Weblate. Don't nest keys too much, just keep one level of nesting.

## Memory

Before starting work, read `MEMORY.md` file at same level of `AGENTS.md` file if it exists for historical context and design decisions.
Update that memory file to keep track of the decisions.
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ class AutomationWorkflowSerializer(serializers.ModelSerializer):
published_on = serializers.SerializerMethodField()
state = serializers.SerializerMethodField()
notification_recipient_ids = serializers.SerializerMethodField()
immediate_dispatch = serializers.SerializerMethodField()

class Meta:
model = AutomationWorkflow
Expand All @@ -33,6 +34,7 @@ class Meta:
"state",
"graph",
"notification_recipient_ids",
"immediate_dispatch",
)
extra_kwargs = {
"id": {"read_only": True},
Expand All @@ -49,7 +51,8 @@ def get_published_on(self, obj):
@extend_schema_field(OpenApiTypes.STR)
def get_state(self, obj):
published_workflow = AutomationWorkflowHandler().get_published_workflow(obj)
return published_workflow.state if published_workflow else WorkflowState.DRAFT
state = published_workflow.state if published_workflow else WorkflowState.DRAFT
return WorkflowState(state).value

@extend_schema_field(serializers.ListField(child=serializers.IntegerField()))
def get_notification_recipient_ids(self, obj):
Expand All @@ -59,6 +62,10 @@ def get_notification_recipient_ids(self, obj):

return sorted((recipient.id for recipient in obj.notification_recipients.all()))

@extend_schema_field(OpenApiTypes.BOOL)
def get_immediate_dispatch(self, obj):
return obj.can_be_immediately_dispatched()


class CreateAutomationWorkflowSerializer(serializers.ModelSerializer):
class Meta:
Expand Down
4 changes: 4 additions & 0 deletions backend/src/baserow/contrib/automation/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@ def ready(self):
CoreHttpRequestNodeType,
CoreHTTPTriggerNodeType,
CoreIteratorNodeType,
CoreManualTriggerNodeType,
CorePeriodicTriggerNodeType,
CoreRouterActionNodeType,
CoreSMTPEmailNodeType,
CoreStartWorkflowNodeType,
LocalBaserowAggregateRowsNodeType,
LocalBaserowCreateRowNodeType,
LocalBaserowCreateRowsNodeType,
Expand Down Expand Up @@ -183,6 +185,7 @@ def ready(self):
automation_node_type_registry.register(CoreCSVFileReaderNodeType())
automation_node_type_registry.register(CoreSMTPEmailNodeType())
automation_node_type_registry.register(CoreRouterActionNodeType())
automation_node_type_registry.register(CoreStartWorkflowNodeType())
automation_node_type_registry.register(LocalBaserowRowsCreatedNodeTriggerType())
automation_node_type_registry.register(LocalBaserowRowsUpdatedNodeTriggerType())
automation_node_type_registry.register(LocalBaserowRowsDeletedNodeTriggerType())
Expand All @@ -191,6 +194,7 @@ def ready(self):
)
automation_node_type_registry.register(CorePeriodicTriggerNodeType())
automation_node_type_registry.register(CoreHTTPTriggerNodeType())
automation_node_type_registry.register(CoreManualTriggerNodeType())
automation_node_type_registry.register(AIAgentActionNodeType())
automation_node_type_registry.register(SlackWriteMessageActionNodeType())

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
("automation", "0033_localbaserowcreaterowsactionnode"),
]

operations = [
migrations.CreateModel(
name="CoreManualTriggerNode",
fields=[
(
"automationnode_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="automation.automationnode",
),
),
],
options={
"abstract": False,
},
bases=("automation.automationnode",),
),
migrations.CreateModel(
name="CoreStartWorkflowActionNode",
fields=[
(
"automationnode_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="automation.automationnode",
),
),
],
options={
"abstract": False,
},
bases=("automation.automationnode",),
),
]
6 changes: 6 additions & 0 deletions backend/src/baserow/contrib/automation/nodes/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,9 @@ class CorePeriodicTriggerNode(AutomationTriggerNode): ...
class CoreHTTPTriggerNode(AutomationTriggerNode): ...


class CoreManualTriggerNode(AutomationTriggerNode): ...


class LocalBaserowCreateRowActionNode(AutomationActionNode): ...


Expand Down Expand Up @@ -188,6 +191,9 @@ class CoreIteratorActionNode(AutomationActionNode): ...
class CoreCSVFileReaderActionNode(AutomationActionNode): ...


class CoreStartWorkflowActionNode(AutomationActionNode): ...


class AIAgentActionNode(AutomationActionNode): ...


Expand Down
16 changes: 16 additions & 0 deletions backend/src/baserow/contrib/automation/nodes/node_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@
CoreHTTPRequestActionNode,
CoreHTTPTriggerNode,
CoreIteratorActionNode,
CoreManualTriggerNode,
CorePeriodicTriggerNode,
CoreRouterActionNode,
CoreSMTPEmailActionNode,
CoreStartWorkflowActionNode,
LocalBaserowAggregateRowsActionNode,
LocalBaserowCreateRowActionNode,
LocalBaserowCreateRowsActionNode,
Expand All @@ -49,9 +51,11 @@
CoreHTTPRequestServiceType,
CoreHTTPTriggerServiceType,
CoreIteratorServiceType,
CoreManualTriggerServiceType,
CorePeriodicServiceType,
CoreRouterServiceType,
CoreSMTPEmailServiceType,
CoreStartWorkflowServiceType,
)
from baserow.contrib.integrations.local_baserow.service_types import (
LocalBaserowAggregateRowsUserServiceType,
Expand Down Expand Up @@ -228,6 +232,12 @@ class CoreSMTPEmailNodeType(AutomationNodeActionNodeType):
service_type = CoreSMTPEmailServiceType.type


class CoreStartWorkflowNodeType(AutomationNodeActionNodeType):
type = "start_workflow"
model_class = CoreStartWorkflowActionNode
service_type = CoreStartWorkflowServiceType.type


class AIAgentActionNodeType(AutomationNodeActionNodeType):
display_name = _("AI agent")
type = "ai_agent"
Expand Down Expand Up @@ -476,6 +486,12 @@ class CoreHTTPTriggerNodeType(AutomationNodeTriggerType):
service_type = CoreHTTPTriggerServiceType.type


class CoreManualTriggerNodeType(AutomationNodeTriggerType):
type = "manual"
model_class = CoreManualTriggerNode
service_type = CoreManualTriggerServiceType.type


class SlackWriteMessageActionNodeType(AutomationNodeActionNodeType):
display_name = _("Slack write message")
type = "slack_write_message"
Expand Down
8 changes: 4 additions & 4 deletions backend/src/baserow/contrib/automation/workflows/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -825,9 +825,9 @@ def toggle_test_run(

if simulate_until_node is None: # Full test
AutomationWorkflowHandler().set_workflow_temporary_states(workflow)
if workflow.can_immediately_be_tested():
# If the service related to the trigger can immediately be tested
# we immediately trigger the workflow run
if workflow.can_be_immediately_dispatched():
# If the service related to the trigger can immediately dispatch,
# we immediately trigger the workflow run.
self.async_start_workflow(workflow)
else:
AutomationWorkflowHandler().set_workflow_temporary_states(
Expand All @@ -843,7 +843,7 @@ def toggle_test_run(
history=None,
simulate_until_node=simulate_until_node,
)
if workflow.can_immediately_be_tested() or (
if workflow.can_be_immediately_dispatched() or (
trigger.service.get_type().get_sample_data(
trigger.service.specific, dispatch_context
)
Expand Down
12 changes: 8 additions & 4 deletions backend/src/baserow/contrib/automation/workflows/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,13 +143,17 @@ def get_trigger(self) -> "AutomationTriggerNode":

return self.get_graph().get_point_at_position(None, "south", "")

def can_immediately_be_tested(self):
def can_be_immediately_dispatched(self):
"""
True of the workflow trigger can immediately be dispatched in test mode.
True if the workflow trigger can dispatch without waiting for an event.
"""

service = self.get_trigger().service.specific
return service.get_type().can_immediately_be_tested(service)
trigger = self.get_trigger()
if trigger is None:
return False

service = trigger.service.specific
return service.get_type().can_be_immediately_dispatched(service)

@property
def is_published(self) -> bool:
Expand Down
2 changes: 2 additions & 0 deletions backend/src/baserow/contrib/builder/apps.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,7 @@ def ready(self):
CoreCSVFileReaderActionType,
CoreHttpRequestActionType,
CoreSMTPEmailActionType,
CoreStartWorkflowActionType,
CreateRowWorkflowActionType,
DeleteRowWorkflowActionType,
LocalBaserowCreateRowsWorkflowActionType,
Expand Down Expand Up @@ -342,6 +343,7 @@ def ready(self):
builder_workflow_action_type_registry.register(CoreHttpRequestActionType())
builder_workflow_action_type_registry.register(CoreSMTPEmailActionType())
builder_workflow_action_type_registry.register(CoreCSVFileReaderActionType())
builder_workflow_action_type_registry.register(CoreStartWorkflowActionType())
builder_workflow_action_type_registry.register(AIAgentWorkflowActionType())
builder_workflow_action_type_registry.register(
SlackWriteMessageWorkflowActionType()
Expand Down
2 changes: 2 additions & 0 deletions backend/src/baserow/contrib/builder/elements/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,10 +418,12 @@ class COLUMN_STACKING_TYPES(models.TextChoices):
choices=LAYOUT_TYPES.choices,
max_length=20,
default=LAYOUT_TYPES.AUTO,
db_default=LAYOUT_TYPES.AUTO,
help_text="The layout type determining column weights.",
)
column_weights = models.JSONField(
default=list,
db_default=[],
help_text=(
"Custom weight configuration for each column. Used when layout_type is "
"'custom'."
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ class Migration(migrations.Migration):
("1:2:1", "Ratio 1 2 1"),
("custom", "Custom"),
],
db_default="auto",
default="auto",
help_text="The layout type determining column weights.",
max_length=20,
Expand All @@ -35,6 +36,7 @@ class Migration(migrations.Migration):
model_name="columnelement",
name="column_weights",
field=models.JSONField(
db_default=[],
default=list,
help_text=(
"Custom weight configuration for each column. Used when "
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):
dependencies = [
("builder", "0073_localbaserowcreaterowsworkflowaction"),
("integrations", "0031_corestartworkflowservice"),
]

operations = [
migrations.CreateModel(
name="CoreStartWorkflowWorkflowAction",
fields=[
(
"builderworkflowaction_ptr",
models.OneToOneField(
auto_created=True,
on_delete=django.db.models.deletion.CASCADE,
parent_link=True,
primary_key=True,
serialize=False,
to="builder.builderworkflowaction",
),
),
(
"service",
models.ForeignKey(
help_text="The service which this action is associated with.",
on_delete=django.db.models.deletion.CASCADE,
to="core.service",
),
),
],
options={
"abstract": False,
},
bases=("builder.builderworkflowaction",),
),
]
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,9 @@ class CoreSMTPEmailWorkflowAction(BuilderWorkflowServiceAction): ...
class CoreCSVFileReaderWorkflowAction(BuilderWorkflowServiceAction): ...


class CoreStartWorkflowWorkflowAction(BuilderWorkflowServiceAction): ...


class AIAgentWorkflowAction(BuilderWorkflowServiceAction): ...


Expand Down
Loading
Loading