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
Original file line number Diff line number Diff line change
Expand Up @@ -124,21 +124,39 @@ class Meta:
)


class AutomationWorkflowHistoryTriggeredBySerializer(serializers.Serializer):
"""Who started the run, as `{id, type, name}` for the collaborator UI."""

id = serializers.IntegerField(source="triggered_by_id", read_only=True)
type = serializers.CharField(source="triggered_by_type", read_only=True)
name = serializers.CharField(source="triggered_by_name", read_only=True)


class AutomationWorkflowHistorySerializer(AutomationHistorySerializer):
plugin_data = serializers.SerializerMethodField()
triggered_by = serializers.SerializerMethodField()

class Meta:
model = AutomationWorkflowHistory
fields = AutomationHistorySerializer.Meta.fields + (
"is_test_run",
"simulate_until_node",
"plugin_data",
"triggered_by",
)

@extend_schema_field(serializers.DictField())
def get_plugin_data(self, obj):
return self.context.get("workflow_history_plugin_data", {}).get(obj.id, {})

@extend_schema_field(
AutomationWorkflowHistoryTriggeredBySerializer(allow_null=True)
)
def get_triggered_by(self, obj):
if obj.triggered_by_id is None:
return None
return AutomationWorkflowHistoryTriggeredBySerializer(obj).data


class AutomationWorkflowHistoryPagination(PageNumberPagination):
def get_paginated_response(self, data, *, success_count: int, fail_count: int):
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@
automation_data_provider_type_registry,
)
from baserow.contrib.automation.history.handler import AutomationHistoryHandler
from baserow.contrib.automation.history.models import (
AutomationNodeHistory,
)
from baserow.contrib.automation.history.models import AutomationWorkflowHistory
from baserow.contrib.automation.nodes.models import AutomationActionNode
from baserow.contrib.automation.workflows.models import AutomationWorkflow
from baserow.core.cache import local_cache
Expand All @@ -19,7 +17,7 @@ class AutomationDispatchContext(DispatchContext):
def __init__(
self,
workflow: AutomationWorkflow,
history: AutomationNodeHistory,
history: AutomationWorkflowHistory,
event_payload: Optional[Union[Dict, List[Dict]]] = None,
simulate_until_node: Optional[AutomationActionNode] = None,
current_iterations: Optional[Dict[int, int]] = None,
Expand Down
22 changes: 21 additions & 1 deletion backend/src/baserow/contrib/automation/history/handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@
from baserow.contrib.automation.nodes.models import AutomationNode
from baserow.contrib.automation.workflows.models import AutomationWorkflow
from baserow.core.db import specific_iterator
from baserow.core.registries import subject_type_registry
from baserow.core.services.handler import ServiceHandler
from baserow.core.services.models import Service
from baserow.core.types import Subject


class AutomationHistoryHandler:
Expand Down Expand Up @@ -73,8 +75,25 @@ def create_workflow_history(
status: HistoryStatusChoices = HistoryStatusChoices.STARTED,
completed_on: Optional[datetime] = None,
message: str = "",
triggered_by: Optional[Subject] = None,
) -> AutomationWorkflowHistory:
"""Creates a history entry for a Workflow run."""
"""
Creates a history entry for a Workflow run.

:param triggered_by: The subject who deliberately started the run, when
one did. An event-started run has none.
"""

triggered_by_values = {}
if triggered_by is not None:
subject_type = subject_type_registry.get_by_model(triggered_by)
triggered_by_values = {
"triggered_by_id": triggered_by.id,
"triggered_by_type": subject_type.type,
# TODO: use `subject_type.get_display_name` once agents are
# subjects (#6064). Users are the only subject starting a run.
"triggered_by_name": triggered_by.first_name,
}

return AutomationWorkflowHistory.objects.create(
workflow=workflow,
Expand All @@ -86,6 +105,7 @@ def create_workflow_history(
status=status,
completed_on=completed_on,
message=message,
**triggered_by_values,
)

def create_node_history(
Expand Down
22 changes: 22 additions & 0 deletions backend/src/baserow/contrib/automation/history/models.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from django.db import models

from baserow.contrib.automation.history.constants import HistoryStatusChoices
from baserow.core.subjects import UserSubjectType


class AutomationHistory(models.Model):
Expand Down Expand Up @@ -50,6 +51,27 @@ class AutomationWorkflowHistory(AutomationHistory):
help_text="Event payload received by the workflow.",
)

# Who started the run, as a subject rather than a user foreign key so an
# agent can be recorded too. The id is only meaningful together with the
# type, since user and agent ids can overlap. The name is kept so the entry
# still reads after the subject is deleted.
triggered_by_id = models.PositiveIntegerField(
null=True,
help_text="The id of the subject who started this run. Null when an "
"event started it.",
)
triggered_by_type = models.CharField(
max_length=255,
db_default=UserSubjectType.type,
help_text="The subject type of who started this run.",
)
triggered_by_name = models.CharField(
max_length=160,
blank=True,
db_default="",
help_text="The name of who started this run, when it started.",
)

class Meta(AutomationHistory.Meta):
indexes = [
models.Index(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# Generated by Django 5.2.16 on 2026-09-14 11:44

from django.db import migrations, models


class Migration(migrations.Migration):

dependencies = [
('automation', '0035_coregotoactionnode'),
]

operations = [
migrations.AddField(
model_name='automationworkflow',
name='test_run_triggered_by_id',
field=models.PositiveIntegerField(blank=True, help_text='The id of the subject who asked for the pending test run.', null=True),
),
migrations.AddField(
model_name='automationworkflow',
name='test_run_triggered_by_type',
field=models.CharField(db_default='auth.User', help_text='The subject type of who asked for the pending test run.', max_length=255),
),
migrations.AddField(
model_name='automationworkflowhistory',
name='triggered_by_id',
field=models.PositiveIntegerField(help_text='The id of the subject who started this run. Null when an event started it.', null=True),
),
migrations.AddField(
model_name='automationworkflowhistory',
name='triggered_by_name',
field=models.CharField(blank=True, db_default='', help_text='The name of who started this run, when it started.', max_length=160),
),
migrations.AddField(
model_name='automationworkflowhistory',
name='triggered_by_type',
field=models.CharField(db_default='auth.User', help_text='The subject type of who started this run.', max_length=255),
),
]
4 changes: 4 additions & 0 deletions backend/src/baserow/contrib/automation/nodes/node_types.py
Original file line number Diff line number Diff line change
Expand Up @@ -702,6 +702,10 @@ def on_event(
AutomationWorkflowHandler().async_start_workflow(
workflow,
service_payload,
# Read before the reset below clears it.
triggered_by=AutomationWorkflowHandler().get_test_run_triggered_by(
workflow
),
)

# We don't want subsequent events to trigger a new test run
Expand Down
Loading
Loading