diff --git a/docs/generated/postgres-schema/README.md b/docs/generated/postgres-schema/README.md
index 427dea8fea3..1b15aaf06e8 100644
--- a/docs/generated/postgres-schema/README.md
+++ b/docs/generated/postgres-schema/README.md
@@ -113,6 +113,7 @@ Auto-generated from the PostgreSQL migrations in @n8n/db. Do not edit by hand.
| [public.workflow_entity](public.workflow_entity.md) | 20 | | BASE TABLE |
| [public.workflow_history](public.workflow_history.md) | 11 | | BASE TABLE |
| [public.workflow_publication_outbox](public.workflow_publication_outbox.md) | 7 | | BASE TABLE |
+| [public.workflow_publication_trigger_status](public.workflow_publication_trigger_status.md) | 7 | | BASE TABLE |
| [public.workflow_publish_history](public.workflow_publish_history.md) | 6 | | BASE TABLE |
| [public.workflow_published_version](public.workflow_published_version.md) | 4 | | BASE TABLE |
| [public.workflow_statistics](public.workflow_statistics.md) | 7 | | BASE TABLE |
@@ -282,6 +283,8 @@ erDiagram
"public.workflow_entity" }o--o| "public.workflow_history" : "FOREIGN KEY (#quot;activeVersionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE RESTRICT"
"public.workflow_entity" }o--o| "public.folder" : "FOREIGN KEY (#quot;parentFolderId#quot;) REFERENCES folder(id) ON DELETE CASCADE"
"public.workflow_history" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
+"public.workflow_publication_trigger_status" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
+"public.workflow_publication_trigger_status" }o--|| "public.workflow_history" : "FOREIGN KEY (#quot;versionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE CASCADE"
"public.workflow_publish_history" }o--o| "public.user" : "FOREIGN KEY (#quot;userId#quot;) REFERENCES #quot;user#quot;(id) ON DELETE SET NULL"
"public.workflow_publish_history" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflow_publish_history" }o--o| "public.workflow_history" : "FOREIGN KEY (#quot;versionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE SET NULL"
@@ -1295,6 +1298,15 @@ erDiagram
timestamp_3__with_time_zone updatedAt
varchar_36_ workflowId
}
+"public.workflow_publication_trigger_status" {
+ timestamp_3__with_time_zone createdAt
+ text errorMessage
+ varchar_36_ nodeId
+ varchar_20_ status
+ timestamp_3__with_time_zone updatedAt
+ varchar_36_ versionId FK
+ varchar_36_ workflowId FK
+}
"public.workflow_publish_history" {
timestamp_3__with_time_zone createdAt
varchar_36_ event
diff --git a/docs/generated/postgres-schema/public.workflow_entity.md b/docs/generated/postgres-schema/public.workflow_entity.md
index d4669e00a45..0c2820d1d72 100644
--- a/docs/generated/postgres-schema/public.workflow_entity.md
+++ b/docs/generated/postgres-schema/public.workflow_entity.md
@@ -9,7 +9,7 @@
| connections | json | | false | | | |
| createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
| description | text | | true | | | |
-| id | varchar(36) | | false | [public.ai_builder_temporary_workflow](public.ai_builder_temporary_workflow.md) [public.chat_hub_messages](public.chat_hub_messages.md) [public.chat_hub_sessions](public.chat_hub_sessions.md) [public.evaluation_collection](public.evaluation_collection.md) [public.evaluation_config](public.evaluation_config.md) [public.execution_entity](public.execution_entity.md) [public.insights_metadata](public.insights_metadata.md) [public.processed_data](public.processed_data.md) [public.shared_workflow](public.shared_workflow.md) [public.test_run](public.test_run.md) [public.webhook_entity](public.webhook_entity.md) [public.workflow_builder_session](public.workflow_builder_session.md) [public.workflow_dependency](public.workflow_dependency.md) [public.workflow_history](public.workflow_history.md) [public.workflow_publish_history](public.workflow_publish_history.md) [public.workflow_published_version](public.workflow_published_version.md) [public.workflows_tags](public.workflows_tags.md) | | |
+| id | varchar(36) | | false | [public.ai_builder_temporary_workflow](public.ai_builder_temporary_workflow.md) [public.chat_hub_messages](public.chat_hub_messages.md) [public.chat_hub_sessions](public.chat_hub_sessions.md) [public.evaluation_collection](public.evaluation_collection.md) [public.evaluation_config](public.evaluation_config.md) [public.execution_entity](public.execution_entity.md) [public.insights_metadata](public.insights_metadata.md) [public.processed_data](public.processed_data.md) [public.shared_workflow](public.shared_workflow.md) [public.test_run](public.test_run.md) [public.webhook_entity](public.webhook_entity.md) [public.workflow_builder_session](public.workflow_builder_session.md) [public.workflow_dependency](public.workflow_dependency.md) [public.workflow_history](public.workflow_history.md) [public.workflow_publication_trigger_status](public.workflow_publication_trigger_status.md) [public.workflow_publish_history](public.workflow_publish_history.md) [public.workflow_published_version](public.workflow_published_version.md) [public.workflows_tags](public.workflows_tags.md) | | |
| isArchived | boolean | false | false | | | |
| meta | json | | true | | | |
| name | varchar(128) | | false | | | |
@@ -80,6 +80,7 @@ erDiagram
"public.workflow_builder_session" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflow_dependency" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflow_history" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
+"public.workflow_publication_trigger_status" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflow_publish_history" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
"public.workflow_published_version" |o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE RESTRICT"
"public.workflows_tags" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
@@ -274,6 +275,15 @@ erDiagram
varchar_36_ workflowId FK
integer workflowVersionId
}
+"public.workflow_publication_trigger_status" {
+ timestamp_3__with_time_zone createdAt
+ text errorMessage
+ varchar_36_ nodeId
+ varchar_20_ status
+ timestamp_3__with_time_zone updatedAt
+ varchar_36_ versionId FK
+ varchar_36_ workflowId FK
+}
"public.workflow_publish_history" {
timestamp_3__with_time_zone createdAt
varchar_36_ event
diff --git a/docs/generated/postgres-schema/public.workflow_history.md b/docs/generated/postgres-schema/public.workflow_history.md
index e832f5883a1..484518aedc8 100644
--- a/docs/generated/postgres-schema/public.workflow_history.md
+++ b/docs/generated/postgres-schema/public.workflow_history.md
@@ -13,7 +13,7 @@
| nodeGroups | json | '[]'::json | false | | | |
| nodes | json | | false | | | |
| updatedAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
-| versionId | varchar(36) | | false | [public.workflow_entity](public.workflow_entity.md) [public.workflow_publish_history](public.workflow_publish_history.md) [public.workflow_published_version](public.workflow_published_version.md) | | |
+| versionId | varchar(36) | | false | [public.workflow_entity](public.workflow_entity.md) [public.workflow_publication_trigger_status](public.workflow_publication_trigger_status.md) [public.workflow_publish_history](public.workflow_publish_history.md) [public.workflow_published_version](public.workflow_published_version.md) | | |
| workflowId | varchar(36) | | false | | [public.workflow_entity](public.workflow_entity.md) | |
## Constraints
@@ -45,6 +45,7 @@
erDiagram
"public.workflow_entity" }o--o| "public.workflow_history" : "FOREIGN KEY (#quot;activeVersionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE RESTRICT"
+"public.workflow_publication_trigger_status" }o--|| "public.workflow_history" : "FOREIGN KEY (#quot;versionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE CASCADE"
"public.workflow_publish_history" }o--o| "public.workflow_history" : "FOREIGN KEY (#quot;versionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE SET NULL"
"public.workflow_published_version" }o--|| "public.workflow_history" : "FOREIGN KEY (#quot;publishedVersionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE RESTRICT"
"public.workflow_history" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
@@ -84,6 +85,15 @@ erDiagram
integer versionCounter
character_36_ versionId
}
+"public.workflow_publication_trigger_status" {
+ timestamp_3__with_time_zone createdAt
+ text errorMessage
+ varchar_36_ nodeId
+ varchar_20_ status
+ timestamp_3__with_time_zone updatedAt
+ varchar_36_ versionId FK
+ varchar_36_ workflowId FK
+}
"public.workflow_publish_history" {
timestamp_3__with_time_zone createdAt
varchar_36_ event
diff --git a/docs/generated/postgres-schema/public.workflow_publication_trigger_status.md b/docs/generated/postgres-schema/public.workflow_publication_trigger_status.md
new file mode 100644
index 00000000000..60540f855ae
--- /dev/null
+++ b/docs/generated/postgres-schema/public.workflow_publication_trigger_status.md
@@ -0,0 +1,92 @@
+# public.workflow_publication_trigger_status
+
+## Columns
+
+| Name | Type | Default | Nullable | Children | Parents | Comment |
+| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
+| createdAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
+| errorMessage | text | | true | | | |
+| nodeId | varchar(36) | | false | | | |
+| status | varchar(20) | | false | | | |
+| updatedAt | timestamp(3) with time zone | CURRENT_TIMESTAMP(3) | false | | | |
+| versionId | varchar(36) | | false | | [public.workflow_history](public.workflow_history.md) | References workflow_history.versionId: the published version these statuses were recorded for |
+| workflowId | varchar(36) | | false | | [public.workflow_entity](public.workflow_entity.md) | |
+
+## Constraints
+
+| Name | Type | Definition |
+| ---- | ---- | ---------- |
+| CHK_workflow_publication_trigger_status_status | CHECK | CHECK (((status)::text = ANY ((ARRAY['activated'::character varying, 'failed'::character varying])::text[]))) |
+| FK_b7b496d8d1a21158c65f475cd88 | FOREIGN KEY | FOREIGN KEY ("workflowId") REFERENCES workflow_entity(id) ON DELETE CASCADE |
+| FK_ef1994db9d0ac1b6a5c89b5f729 | FOREIGN KEY | FOREIGN KEY ("versionId") REFERENCES workflow_history("versionId") ON DELETE CASCADE |
+| PK_14aa18b83513fb92d7523909e02 | PRIMARY KEY | PRIMARY KEY ("workflowId", "nodeId") |
+| workflow_publication_trigger_status_createdAt_not_null | n | NOT NULL "createdAt" |
+| workflow_publication_trigger_status_nodeId_not_null | n | NOT NULL "nodeId" |
+| workflow_publication_trigger_status_status_not_null | n | NOT NULL status |
+| workflow_publication_trigger_status_updatedAt_not_null | n | NOT NULL "updatedAt" |
+| workflow_publication_trigger_status_versionId_not_null | n | NOT NULL "versionId" |
+| workflow_publication_trigger_status_workflowId_not_null | n | NOT NULL "workflowId" |
+
+## Indexes
+
+| Name | Definition |
+| ---- | ---------- |
+| PK_14aa18b83513fb92d7523909e02 | CREATE UNIQUE INDEX "PK_14aa18b83513fb92d7523909e02" ON public.workflow_publication_trigger_status USING btree ("workflowId", "nodeId") |
+
+## Relations
+
+```mermaid
+erDiagram
+
+"public.workflow_publication_trigger_status" }o--|| "public.workflow_history" : "FOREIGN KEY (#quot;versionId#quot;) REFERENCES workflow_history(#quot;versionId#quot;) ON DELETE CASCADE"
+"public.workflow_publication_trigger_status" }o--|| "public.workflow_entity" : "FOREIGN KEY (#quot;workflowId#quot;) REFERENCES workflow_entity(id) ON DELETE CASCADE"
+
+"public.workflow_publication_trigger_status" {
+ timestamp_3__with_time_zone createdAt
+ text errorMessage
+ varchar_36_ nodeId
+ varchar_20_ status
+ timestamp_3__with_time_zone updatedAt
+ varchar_36_ versionId FK
+ varchar_36_ workflowId FK
+}
+"public.workflow_history" {
+ varchar_255_ authors
+ boolean autosaved
+ json connections
+ timestamp_3__with_time_zone createdAt
+ text description
+ varchar_128_ name
+ json nodeGroups
+ json nodes
+ timestamp_3__with_time_zone updatedAt
+ varchar_36_ versionId
+ varchar_36_ workflowId FK
+}
+"public.workflow_entity" {
+ boolean active
+ varchar_36_ activeVersionId FK
+ json connections
+ timestamp_3__with_time_zone createdAt
+ text description
+ varchar_36_ id
+ boolean isArchived
+ json meta
+ varchar_128_ name
+ json nodeGroups
+ json nodes
+ varchar_36_ parentFolderId FK
+ json pinData
+ json settings
+ varchar sourceWorkflowId
+ json staticData
+ integer triggerCount
+ timestamp_3__with_time_zone updatedAt
+ integer versionCounter
+ character_36_ versionId
+}
+```
+
+---
+
+> Generated by [tbls](https://github.com/k1LoW/tbls)
diff --git a/docs/generated/sqlite-schema/README.md b/docs/generated/sqlite-schema/README.md
index f36a5d309f6..8a3d7535eaa 100644
--- a/docs/generated/sqlite-schema/README.md
+++ b/docs/generated/sqlite-schema/README.md
@@ -113,6 +113,7 @@ Auto-generated from the SQLite migrations in @n8n/db. Do not edit by hand.
| [workflow_entity](workflow_entity.md) | 20 | | table |
| [workflow_history](workflow_history.md) | 11 | | table |
| [workflow_publication_outbox](workflow_publication_outbox.md) | 7 | | table |
+| [workflow_publication_trigger_status](workflow_publication_trigger_status.md) | 7 | | table |
| [workflow_publish_history](workflow_publish_history.md) | 6 | | table |
| [workflow_published_version](workflow_published_version.md) | 4 | | table |
| [workflow_statistics](workflow_statistics.md) | 7 | | table |
@@ -267,6 +268,8 @@ erDiagram
"workflow_entity" }o--o| "workflow_history" : "FOREIGN KEY (activeVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE RESTRICT MATCH NONE"
"workflow_entity" }o--o| "folder" : "FOREIGN KEY (parentFolderId) REFERENCES folder (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_history" }o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
+"workflow_publication_trigger_status" }o--|| "workflow_history" : "FOREIGN KEY (versionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
+"workflow_publication_trigger_status" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_publish_history" }o--o| "workflow_history" : "FOREIGN KEY (versionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_publish_history" }o--o| "user" : "FOREIGN KEY (userId) REFERENCES user (id) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_publish_history" }o--o| "workflow_history" : "FOREIGN KEY (versionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
@@ -1285,6 +1288,15 @@ erDiagram
datetime_3_ updatedAt
varchar_36_ workflowId
}
+"workflow_publication_trigger_status" {
+ datetime_3_ createdAt
+ TEXT errorMessage
+ varchar_36_ nodeId PK
+ varchar_20_ status
+ datetime_3_ updatedAt
+ varchar_36_ versionId FK
+ varchar_36_ workflowId PK
+}
"workflow_publish_history" {
datetime_3_ createdAt
varchar_36_ event
diff --git a/docs/generated/sqlite-schema/workflow_entity.md b/docs/generated/sqlite-schema/workflow_entity.md
index 3976342822a..e903c4b4a20 100644
--- a/docs/generated/sqlite-schema/workflow_entity.md
+++ b/docs/generated/sqlite-schema/workflow_entity.md
@@ -20,7 +20,7 @@ CREATE TABLE "workflow_entity" ("id" varchar(36) PRIMARY KEY NOT NULL, "name" va
| connections | TEXT | | true | | | |
| createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
| description | TEXT | | true | | | |
-| id | varchar(36) | | false | [ai_builder_temporary_workflow](ai_builder_temporary_workflow.md) [chat_hub_messages](chat_hub_messages.md) [chat_hub_sessions](chat_hub_sessions.md) [evaluation_collection](evaluation_collection.md) [evaluation_config](evaluation_config.md) [execution_entity](execution_entity.md) [insights_metadata](insights_metadata.md) [processed_data](processed_data.md) [shared_workflow](shared_workflow.md) [test_run](test_run.md) [workflow_builder_session](workflow_builder_session.md) [workflow_dependency](workflow_dependency.md) [workflow_history](workflow_history.md) [workflow_publish_history](workflow_publish_history.md) [workflow_published_version](workflow_published_version.md) [workflows_tags](workflows_tags.md) | | |
+| id | varchar(36) | | false | [ai_builder_temporary_workflow](ai_builder_temporary_workflow.md) [chat_hub_messages](chat_hub_messages.md) [chat_hub_sessions](chat_hub_sessions.md) [evaluation_collection](evaluation_collection.md) [evaluation_config](evaluation_config.md) [execution_entity](execution_entity.md) [insights_metadata](insights_metadata.md) [processed_data](processed_data.md) [shared_workflow](shared_workflow.md) [test_run](test_run.md) [workflow_builder_session](workflow_builder_session.md) [workflow_dependency](workflow_dependency.md) [workflow_history](workflow_history.md) [workflow_publication_trigger_status](workflow_publication_trigger_status.md) [workflow_publish_history](workflow_publish_history.md) [workflow_published_version](workflow_published_version.md) [workflows_tags](workflows_tags.md) | | |
| isArchived | boolean | FALSE | false | | | |
| meta | TEXT | | true | | | |
| name | varchar(128) | | false | | | |
@@ -78,6 +78,7 @@ erDiagram
"workflow_builder_session" }o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_dependency" }o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_history" }o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
+"workflow_publication_trigger_status" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_publish_history" }o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_published_version" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE RESTRICT MATCH NONE"
"workflow_published_version" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
@@ -265,6 +266,15 @@ erDiagram
varchar_36_ workflowId FK
INTEGER workflowVersionId
}
+"workflow_publication_trigger_status" {
+ datetime_3_ createdAt
+ TEXT errorMessage
+ varchar_36_ nodeId PK
+ varchar_20_ status
+ datetime_3_ updatedAt
+ varchar_36_ versionId FK
+ varchar_36_ workflowId PK
+}
"workflow_publish_history" {
datetime_3_ createdAt
varchar_36_ event
diff --git a/docs/generated/sqlite-schema/workflow_history.md b/docs/generated/sqlite-schema/workflow_history.md
index 2762dae636b..df889efd452 100644
--- a/docs/generated/sqlite-schema/workflow_history.md
+++ b/docs/generated/sqlite-schema/workflow_history.md
@@ -24,7 +24,7 @@ CREATE TABLE "workflow_history" ("versionId" varchar(36) PRIMARY KEY NOT NULL, "
| nodeGroups | TEXT | '[]' | false | | | |
| nodes | TEXT | | false | | | |
| updatedAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
-| versionId | varchar(36) | | false | [workflow_entity](workflow_entity.md) [workflow_publish_history](workflow_publish_history.md) [workflow_published_version](workflow_published_version.md) | | |
+| versionId | varchar(36) | | false | [workflow_entity](workflow_entity.md) [workflow_publication_trigger_status](workflow_publication_trigger_status.md) [workflow_publish_history](workflow_publish_history.md) [workflow_published_version](workflow_published_version.md) | | |
| workflowId | varchar(36) | | false | | [workflow_entity](workflow_entity.md) | |
## Constraints
@@ -48,6 +48,7 @@ CREATE TABLE "workflow_history" ("versionId" varchar(36) PRIMARY KEY NOT NULL, "
erDiagram
"workflow_entity" }o--o| "workflow_history" : "FOREIGN KEY (activeVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE RESTRICT MATCH NONE"
+"workflow_publication_trigger_status" }o--|| "workflow_history" : "FOREIGN KEY (versionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_publish_history" }o--o| "workflow_history" : "FOREIGN KEY (versionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE SET NULL MATCH NONE"
"workflow_publish_history" }o--o| "workflow_history" : "FOREIGN KEY (versionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
"workflow_published_version" }o--|| "workflow_history" : "FOREIGN KEY (publishedVersionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE RESTRICT MATCH NONE"
@@ -89,6 +90,15 @@ erDiagram
INTEGER versionCounter
varchar_36_ versionId
}
+"workflow_publication_trigger_status" {
+ datetime_3_ createdAt
+ TEXT errorMessage
+ varchar_36_ nodeId PK
+ varchar_20_ status
+ datetime_3_ updatedAt
+ varchar_36_ versionId FK
+ varchar_36_ workflowId PK
+}
"workflow_publish_history" {
datetime_3_ createdAt
varchar_36_ event
diff --git a/docs/generated/sqlite-schema/workflow_publication_trigger_status.md b/docs/generated/sqlite-schema/workflow_publication_trigger_status.md
new file mode 100644
index 00000000000..70ef8a62863
--- /dev/null
+++ b/docs/generated/sqlite-schema/workflow_publication_trigger_status.md
@@ -0,0 +1,99 @@
+# workflow_publication_trigger_status
+
+## Description
+
+
+Table Definition
+
+```sql
+CREATE TABLE "workflow_publication_trigger_status" ("workflowId" varchar(36) NOT NULL, "nodeId" varchar(36) NOT NULL, "versionId" varchar(36) NOT NULL, "status" varchar(20) NOT NULL, "errorMessage" text, "createdAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), "updatedAt" datetime(3) NOT NULL DEFAULT (STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW')), CONSTRAINT "CHK_workflow_publication_trigger_status_status" CHECK ("status" IN ('activated', 'failed')), CONSTRAINT "FK_b7b496d8d1a21158c65f475cd88" FOREIGN KEY ("workflowId") REFERENCES "workflow_entity" ("id") ON DELETE CASCADE, CONSTRAINT "FK_ef1994db9d0ac1b6a5c89b5f729" FOREIGN KEY ("versionId") REFERENCES "workflow_history" ("versionId") ON DELETE CASCADE, PRIMARY KEY ("workflowId", "nodeId"))
+```
+
+
+
+## Columns
+
+| Name | Type | Default | Nullable | Children | Parents | Comment |
+| ---- | ---- | ------- | -------- | -------- | ------- | ------- |
+| createdAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
+| errorMessage | TEXT | | true | | | |
+| nodeId | varchar(36) | | false | | | |
+| status | varchar(20) | | false | | | |
+| updatedAt | datetime(3) | STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW') | false | | | |
+| versionId | varchar(36) | | false | | [workflow_history](workflow_history.md) | |
+| workflowId | varchar(36) | | false | | [workflow_entity](workflow_entity.md) | |
+
+## Constraints
+
+| Name | Type | Definition |
+| ---- | ---- | ---------- |
+| - | CHECK | CHECK ("status" IN ('activated', 'failed')) |
+| - (Foreign key ID: 0) | FOREIGN KEY | FOREIGN KEY (versionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
+| - (Foreign key ID: 1) | FOREIGN KEY | FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE |
+| nodeId | PRIMARY KEY | PRIMARY KEY (nodeId) |
+| sqlite_autoindex_workflow_publication_trigger_status_1 | PRIMARY KEY | PRIMARY KEY (workflowId, nodeId) |
+| workflowId | PRIMARY KEY | PRIMARY KEY (workflowId) |
+
+## Indexes
+
+| Name | Definition |
+| ---- | ---------- |
+| sqlite_autoindex_workflow_publication_trigger_status_1 | PRIMARY KEY (workflowId, nodeId) |
+
+## Relations
+
+```mermaid
+erDiagram
+
+"workflow_publication_trigger_status" }o--|| "workflow_history" : "FOREIGN KEY (versionId) REFERENCES workflow_history (versionId) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
+"workflow_publication_trigger_status" |o--|| "workflow_entity" : "FOREIGN KEY (workflowId) REFERENCES workflow_entity (id) ON UPDATE NO ACTION ON DELETE CASCADE MATCH NONE"
+
+"workflow_publication_trigger_status" {
+ datetime_3_ createdAt
+ TEXT errorMessage
+ varchar_36_ nodeId PK
+ varchar_20_ status
+ datetime_3_ updatedAt
+ varchar_36_ versionId FK
+ varchar_36_ workflowId PK
+}
+"workflow_history" {
+ varchar_255_ authors
+ boolean autosaved
+ TEXT connections
+ datetime_3_ createdAt
+ TEXT description
+ varchar_128_ name
+ TEXT nodeGroups
+ TEXT nodes
+ datetime_3_ updatedAt
+ varchar_36_ versionId PK
+ varchar_36_ workflowId FK
+}
+"workflow_entity" {
+ boolean active
+ varchar_36_ activeVersionId FK
+ TEXT connections
+ datetime_3_ createdAt
+ TEXT description
+ varchar_36_ id PK
+ boolean isArchived
+ TEXT meta
+ varchar_128_ name
+ TEXT nodeGroups
+ TEXT nodes
+ varchar_36_ parentFolderId FK
+ TEXT pinData
+ TEXT settings
+ varchar sourceWorkflowId
+ TEXT staticData
+ INTEGER triggerCount
+ datetime_3_ updatedAt
+ INTEGER versionCounter
+ varchar_36_ versionId
+}
+```
+
+---
+
+> Generated by [tbls](https://github.com/k1LoW/tbls)
diff --git a/packages/@n8n/db/src/entities/index.ts b/packages/@n8n/db/src/entities/index.ts
index 8d3df22ad8e..9a27f47f233 100644
--- a/packages/@n8n/db/src/entities/index.ts
+++ b/packages/@n8n/db/src/entities/index.ts
@@ -46,6 +46,10 @@ import {
WorkflowPublicationOutbox,
WorkflowPublicationOutboxStatus,
} from './workflow-publication-outbox';
+import {
+ WorkflowPublicationTriggerStatus,
+ type WorkflowPublicationTriggerStatusType,
+} from './workflow-publication-trigger-status';
import { WorkflowPublishHistory } from './workflow-publish-history';
import { WorkflowPublishedVersion } from './workflow-published-version';
import { WorkflowStatistics } from './workflow-statistics';
@@ -89,6 +93,8 @@ export {
WorkflowHistory,
WorkflowPublicationOutbox,
WorkflowPublicationOutboxStatus,
+ WorkflowPublicationTriggerStatus,
+ type WorkflowPublicationTriggerStatusType,
WorkflowPublishedVersion,
WorkflowPublishHistory,
ExecutionData,
@@ -136,6 +142,7 @@ export const entities = {
AuthProviderSyncHistory,
WorkflowHistory,
WorkflowPublicationOutbox,
+ WorkflowPublicationTriggerStatus,
WorkflowPublishedVersion,
WorkflowPublishHistory,
ExecutionData,
diff --git a/packages/@n8n/db/src/entities/workflow-publication-trigger-status.ts b/packages/@n8n/db/src/entities/workflow-publication-trigger-status.ts
new file mode 100644
index 00000000000..10fd1e47f03
--- /dev/null
+++ b/packages/@n8n/db/src/entities/workflow-publication-trigger-status.ts
@@ -0,0 +1,40 @@
+import { Column, Entity, JoinColumn, ManyToOne, PrimaryColumn, Relation } from '@n8n/typeorm';
+
+import { WithTimestamps } from './abstract-entity';
+import type { WorkflowEntity } from './workflow-entity';
+import type { WorkflowHistory } from './workflow-history';
+
+export type WorkflowPublicationTriggerStatusType = 'activated' | 'failed';
+
+/**
+ * Per-trigger outcome of the most recent version-advancing publication for a
+ * workflow. Full-replaced on each completed/partial/failed publication; the
+ * row composition is the source of truth for the published/partial/failed
+ * headline surfaced to the UI.
+ */
+@Entity({ name: 'workflow_publication_trigger_status' })
+export class WorkflowPublicationTriggerStatus extends WithTimestamps {
+ @PrimaryColumn({ type: 'varchar', length: 36 })
+ workflowId: string;
+
+ @PrimaryColumn({ type: 'varchar', length: 36 })
+ nodeId: string;
+
+ /** The `workflow_history` version these statuses were recorded for. */
+ @Column({ type: 'varchar', length: 36 })
+ versionId: string;
+
+ @Column({ type: 'varchar', length: 20 })
+ status: WorkflowPublicationTriggerStatusType;
+
+ @Column({ type: 'text', nullable: true })
+ errorMessage: string | null;
+
+ @ManyToOne('WorkflowEntity', { onDelete: 'CASCADE' })
+ @JoinColumn({ name: 'workflowId' })
+ workflow: Relation;
+
+ @ManyToOne('WorkflowHistory', { onDelete: 'CASCADE' })
+ @JoinColumn({ name: 'versionId', referencedColumnName: 'versionId' })
+ publishedVersion: Relation;
+}
diff --git a/packages/@n8n/db/src/migrations/common/1784000000040-CreateWorkflowPublicationTriggerStatusTable.ts b/packages/@n8n/db/src/migrations/common/1784000000040-CreateWorkflowPublicationTriggerStatusTable.ts
new file mode 100644
index 00000000000..44f38e9f134
--- /dev/null
+++ b/packages/@n8n/db/src/migrations/common/1784000000040-CreateWorkflowPublicationTriggerStatusTable.ts
@@ -0,0 +1,36 @@
+import type { MigrationContext, ReversibleMigration } from '../migration-types';
+
+const tableName = 'workflow_publication_trigger_status';
+
+export class CreateWorkflowPublicationTriggerStatusTable1784000000040
+ implements ReversibleMigration
+{
+ async up({ schemaBuilder: { createTable, column } }: MigrationContext) {
+ await createTable(tableName)
+ .withColumns(
+ column('workflowId').varchar(36).notNull.primary,
+ column('nodeId').varchar(36).notNull.primary,
+ column('versionId')
+ .varchar(36)
+ .notNull.comment(
+ 'References workflow_history.versionId: the published version these statuses were recorded for',
+ ),
+ column('status').varchar(20).notNull.withEnumCheck(['activated', 'failed']),
+ column('errorMessage').text,
+ )
+ .withForeignKey('workflowId', {
+ tableName: 'workflow_entity',
+ columnName: 'id',
+ onDelete: 'CASCADE',
+ })
+ .withForeignKey('versionId', {
+ tableName: 'workflow_history',
+ columnName: 'versionId',
+ onDelete: 'CASCADE',
+ }).withTimestamps;
+ }
+
+ async down({ schemaBuilder: { dropTable } }: MigrationContext) {
+ await dropTable(tableName);
+ }
+}
diff --git a/packages/@n8n/db/src/migrations/postgresdb/index.ts b/packages/@n8n/db/src/migrations/postgresdb/index.ts
index 66d7a858029..91b93be0055 100644
--- a/packages/@n8n/db/src/migrations/postgresdb/index.ts
+++ b/packages/@n8n/db/src/migrations/postgresdb/index.ts
@@ -213,6 +213,7 @@ import { CreateInstanceAiThreadGrantTable1784000000036 } from '../common/1784000
import { DropAgentDescriptionFromAgents1784000000037 } from '../common/1784000000037-DropAgentDescriptionFromAgents';
import { SetChatHubEnabledFromUsage1784000000038 } from '../common/1784000000038-SetChatHubEnabledFromUsage';
import { DropAgentExecutionFallbackColumns1784000000039 } from '../common/1784000000039-DropAgentExecutionFallbackColumns';
+import { CreateWorkflowPublicationTriggerStatusTable1784000000040 } from '../common/1784000000040-CreateWorkflowPublicationTriggerStatusTable';
import type { Migration } from '../migration-types';
export const postgresMigrations: Migration[] = [
@@ -431,4 +432,5 @@ export const postgresMigrations: Migration[] = [
DropAgentDescriptionFromAgents1784000000037,
SetChatHubEnabledFromUsage1784000000038,
DropAgentExecutionFallbackColumns1784000000039,
+ CreateWorkflowPublicationTriggerStatusTable1784000000040,
];
diff --git a/packages/@n8n/db/src/migrations/sqlite/index.ts b/packages/@n8n/db/src/migrations/sqlite/index.ts
index c1e9ab80ea1..249fc8d5069 100644
--- a/packages/@n8n/db/src/migrations/sqlite/index.ts
+++ b/packages/@n8n/db/src/migrations/sqlite/index.ts
@@ -206,6 +206,7 @@ import { AddUniqueAgentFileNames1784000000035 } from '../common/1784000000035-Ad
import { CreateInstanceAiThreadGrantTable1784000000036 } from '../common/1784000000036-CreateInstanceAiThreadGrantTable';
import { SetChatHubEnabledFromUsage1784000000038 } from '../common/1784000000038-SetChatHubEnabledFromUsage';
import { DropAgentExecutionFallbackColumns1784000000039 } from '../common/1784000000039-DropAgentExecutionFallbackColumns';
+import { CreateWorkflowPublicationTriggerStatusTable1784000000040 } from '../common/1784000000040-CreateWorkflowPublicationTriggerStatusTable';
const sqliteMigrations: Migration[] = [
InitialMigration1588102412422,
@@ -415,6 +416,7 @@ const sqliteMigrations: Migration[] = [
DropAgentDescriptionFromAgents1784000000037,
SetChatHubEnabledFromUsage1784000000038,
DropAgentExecutionFallbackColumns1784000000039,
+ CreateWorkflowPublicationTriggerStatusTable1784000000040,
];
export { sqliteMigrations };
diff --git a/packages/@n8n/db/src/repositories/index.ts b/packages/@n8n/db/src/repositories/index.ts
index 3414e9bc189..8f999a19d02 100644
--- a/packages/@n8n/db/src/repositories/index.ts
+++ b/packages/@n8n/db/src/repositories/index.ts
@@ -48,6 +48,10 @@ export { SharedWorkflowRepository } from './shared-workflow.repository';
export { SharedCredentialsRepository } from './shared-credentials.repository';
export { WorkflowRepository } from './workflow.repository';
export { WorkflowPublicationOutboxRepository } from './workflow-publication-outbox.repository';
+export {
+ WorkflowPublicationTriggerStatusRepository,
+ type TriggerStatusRow,
+} from './workflow-publication-trigger-status.repository';
export {
WorkflowPublishedVersionRepository,
type PublishedWorkflowDataForExecution,
diff --git a/packages/@n8n/db/src/repositories/workflow-publication-outbox.repository.ts b/packages/@n8n/db/src/repositories/workflow-publication-outbox.repository.ts
index f6a9fc065a4..639eeb2c2e7 100644
--- a/packages/@n8n/db/src/repositories/workflow-publication-outbox.repository.ts
+++ b/packages/@n8n/db/src/repositories/workflow-publication-outbox.repository.ts
@@ -251,18 +251,22 @@ export class WorkflowPublicationOutboxRepository extends Repository {
- const result = await this.update(
+ /** Mark a claimed record as successfully processed. Pass `trx` to enroll in an existing transaction. */
+ async markCompleted(id: number, trx?: EntityManager): Promise {
+ const manager = trx ?? this.manager;
+ const result = await manager.update(
+ WorkflowPublicationOutbox,
{ id, status: Status.InProgress },
{ status: Status.Completed, errorMessage: null },
);
this.assertSingleRowAffected(result.affected, id, Status.Completed);
}
- /** Mark a claimed record as failed and record the error for diagnostics. */
- async markFailed(id: number, errorMessage: string): Promise {
- const result = await this.update(
+ /** Mark a claimed record as failed and record the error for diagnostics. Pass `trx` to enroll in an existing transaction. */
+ async markFailed(id: number, errorMessage: string, trx?: EntityManager): Promise {
+ const manager = trx ?? this.manager;
+ const result = await manager.update(
+ WorkflowPublicationOutbox,
{ id, status: Status.InProgress },
{ status: Status.Failed, errorMessage },
);
@@ -272,10 +276,13 @@ export class WorkflowPublicationOutboxRepository extends Repository {
- const result = await this.update(
+ async markPartialSuccess(id: number, errorMessage: string, trx?: EntityManager): Promise {
+ const manager = trx ?? this.manager;
+ const result = await manager.update(
+ WorkflowPublicationOutbox,
{ id, status: Status.InProgress },
{ status: Status.PartialSuccess, errorMessage },
);
diff --git a/packages/@n8n/db/src/repositories/workflow-publication-trigger-status.repository.ts b/packages/@n8n/db/src/repositories/workflow-publication-trigger-status.repository.ts
new file mode 100644
index 00000000000..92fd8a32390
--- /dev/null
+++ b/packages/@n8n/db/src/repositories/workflow-publication-trigger-status.repository.ts
@@ -0,0 +1,39 @@
+import { Service } from '@n8n/di';
+import { DataSource, EntityManager, Repository } from '@n8n/typeorm';
+
+import { WorkflowPublicationTriggerStatus } from '../entities';
+
+export type TriggerStatusRow = {
+ nodeId: string;
+ versionId: string;
+ status: WorkflowPublicationTriggerStatus['status'];
+ errorMessage: string | null;
+};
+
+@Service()
+export class WorkflowPublicationTriggerStatusRepository extends Repository {
+ constructor(dataSource: DataSource) {
+ super(WorkflowPublicationTriggerStatus, dataSource.manager);
+ }
+
+ /** Replace all rows for a workflow with `rows`, atomically. Pass `trx` to enroll in an existing transaction. */
+ async replaceForWorkflow(
+ workflowId: string,
+ rows: TriggerStatusRow[],
+ trx?: EntityManager,
+ ): Promise {
+ const run = async (em: EntityManager) => {
+ await em.delete(WorkflowPublicationTriggerStatus, { workflowId });
+ if (rows.length === 0) return;
+ await em.insert(
+ WorkflowPublicationTriggerStatus,
+ rows.map((row) => ({ workflowId, ...row })),
+ );
+ };
+ await (trx ? run(trx) : this.manager.transaction(run));
+ }
+
+ async findByWorkflowId(workflowId: string): Promise {
+ return await this.findBy({ workflowId });
+ }
+}
diff --git a/packages/cli/src/workflows/publication/__tests__/publication-status-reporter.test.ts b/packages/cli/src/workflows/publication/__tests__/publication-status-reporter.test.ts
index dc97a68f47f..a7acbf04350 100644
--- a/packages/cli/src/workflows/publication/__tests__/publication-status-reporter.test.ts
+++ b/packages/cli/src/workflows/publication/__tests__/publication-status-reporter.test.ts
@@ -1,5 +1,11 @@
import type { Logger } from '@n8n/backend-common';
-import type { WorkflowPublicationOutbox, WorkflowPublicationOutboxRepository } from '@n8n/db';
+import type {
+ EntityManager,
+ WorkflowPublicationOutbox,
+ WorkflowPublicationOutboxRepository,
+ WorkflowPublicationTriggerStatusRepository,
+} from '@n8n/db';
+import type { Mock } from 'vitest';
import { mock } from 'vitest-mock-extended';
import type { ErrorReporter } from 'n8n-core';
@@ -12,9 +18,13 @@ describe('PublicationStatusReporter', () => {
logger.scoped.mockReturnValue(logger);
const errorReporter = mock();
- const outboxRepository = mock();
+ const outboxRepository = mock({
+ manager: mock(),
+ });
const activationErrorsService = mock();
const push = mock();
+ const triggerStatusRepository = mock();
+ const entityManager = mock();
const reporter = new PublicationStatusReporter(
logger,
@@ -22,6 +32,7 @@ describe('PublicationStatusReporter', () => {
outboxRepository,
activationErrorsService,
push,
+ triggerStatusRepository,
);
function makeRecord(
@@ -46,12 +57,41 @@ describe('PublicationStatusReporter', () => {
outboxRepository.markPartialSuccess.mockResolvedValue(undefined);
activationErrorsService.deregister.mockResolvedValue(undefined);
activationErrorsService.register.mockResolvedValue(undefined);
+ triggerStatusRepository.replaceForWorkflow.mockResolvedValue(undefined);
+ (outboxRepository.manager.transaction as unknown as Mock).mockImplementation(
+ async (runInTransaction: (trx: EntityManager) => Promise) =>
+ await runInTransaction(entityManager),
+ );
});
- test('completed marks the record completed and clears activation errors', async () => {
- await reporter.report(makeRecord(), { type: 'completed' });
+ test('completed writes trigger rows, marks the record completed, and clears activation errors', async () => {
+ await reporter.report(makeRecord(), {
+ type: 'completed',
+ triggerStatuses: [
+ { nodeId: 'a', nodeName: 'Webhook', status: 'activated' },
+ { nodeId: 'b', nodeName: 'Schedule', status: 'activated' },
+ ],
+ });
- expect(outboxRepository.markCompleted).toHaveBeenCalledWith(1);
+ expect(triggerStatusRepository.replaceForWorkflow).toHaveBeenCalledWith(
+ 'wf-1',
+ [
+ {
+ nodeId: 'a',
+ versionId: 'v-2',
+ status: 'activated',
+ errorMessage: null,
+ },
+ {
+ nodeId: 'b',
+ versionId: 'v-2',
+ status: 'activated',
+ errorMessage: null,
+ },
+ ],
+ entityManager,
+ );
+ expect(outboxRepository.markCompleted).toHaveBeenCalledWith(1, entityManager);
expect(activationErrorsService.deregister).toHaveBeenCalledWith('wf-1');
expect(outboxRepository.markFailed).not.toHaveBeenCalled();
expect(push.broadcast).toHaveBeenCalledWith({
@@ -60,10 +100,15 @@ describe('PublicationStatusReporter', () => {
});
});
- test('unpublished marks the record completed, clears errors, and pushes deactivation', async () => {
+ test('unpublished clears trigger rows, marks the record completed, clears errors, and pushes deactivation', async () => {
await reporter.report(makeRecord(), { type: 'unpublished' });
- expect(outboxRepository.markCompleted).toHaveBeenCalledWith(1);
+ expect(triggerStatusRepository.replaceForWorkflow).toHaveBeenCalledWith(
+ 'wf-1',
+ [],
+ entityManager,
+ );
+ expect(outboxRepository.markCompleted).toHaveBeenCalledWith(1, entityManager);
expect(activationErrorsService.deregister).toHaveBeenCalledWith('wf-1');
expect(outboxRepository.markFailed).not.toHaveBeenCalled();
expect(push.broadcast).toHaveBeenCalledWith({
@@ -77,7 +122,7 @@ describe('PublicationStatusReporter', () => {
async (reason) => {
await reporter.report(makeRecord(), { type: 'skipped', reason });
- expect(outboxRepository.markCompleted).toHaveBeenCalledWith(1);
+ expect(outboxRepository.markCompleted).toHaveBeenCalledWith(1, entityManager);
expect(activationErrorsService.deregister).toHaveBeenCalledWith('wf-1');
expect(outboxRepository.markFailed).not.toHaveBeenCalled();
expect(push.broadcast).not.toHaveBeenCalled();
@@ -101,8 +146,13 @@ describe('PublicationStatusReporter', () => {
await reporter.report(makeRecord(), { type: 'failed', error });
+ expect(triggerStatusRepository.replaceForWorkflow).not.toHaveBeenCalled();
expect(errorReporter.error).toHaveBeenCalledWith(error, { shouldBeLogged: true });
- expect(outboxRepository.markFailed).toHaveBeenCalledWith(1, 'registration failed');
+ expect(outboxRepository.markFailed).toHaveBeenCalledWith(
+ 1,
+ 'registration failed',
+ entityManager,
+ );
expect(outboxRepository.markCompleted).not.toHaveBeenCalled();
expect(push.broadcast).toHaveBeenCalledWith({
type: 'workflowFailedToActivate',
@@ -110,21 +160,87 @@ describe('PublicationStatusReporter', () => {
});
});
- test('partial marks partial_success, registers per-node detail, and pushes the failures', async () => {
+ test('failed with triggerStatuses writes rows before marking failed', async () => {
+ const error = new Error('partial registration failed');
+
+ await reporter.report(makeRecord(), {
+ type: 'failed',
+ error,
+ triggerStatuses: [
+ { nodeId: 'a', nodeName: 'Webhook', status: 'activated' },
+ { nodeId: 'b', nodeName: 'Schedule', status: 'failed', errorMessage: 'cron unavailable' },
+ ],
+ });
+
+ expect(triggerStatusRepository.replaceForWorkflow).toHaveBeenCalledWith(
+ 'wf-1',
+ [
+ {
+ nodeId: 'a',
+ versionId: 'v-2',
+ status: 'activated',
+ errorMessage: null,
+ },
+ {
+ nodeId: 'b',
+ versionId: 'v-2',
+ status: 'failed',
+ errorMessage: 'cron unavailable',
+ },
+ ],
+ entityManager,
+ );
+ expect(outboxRepository.markFailed).toHaveBeenCalledWith(
+ 1,
+ 'partial registration failed',
+ entityManager,
+ );
+ });
+
+ test('partial marks partial_success, writes all trigger rows, and pushes the failures without registering activation errors', async () => {
await reporter.report(makeRecord(), {
type: 'partial',
- activatedNodeIds: ['a'],
- failures: [
- { nodeId: 'b', nodeName: 'Schedule', error: new Error('cron unavailable') },
- { nodeId: 'c', nodeName: 'Kafka', error: new Error('broker down') },
+ triggerStatuses: [
+ { nodeId: 'a', nodeName: 'Webhook', status: 'activated' },
+ { nodeId: 'b', nodeName: 'Schedule', status: 'failed', errorMessage: 'cron unavailable' },
+ { nodeId: 'c', nodeName: 'Kafka', status: 'failed', errorMessage: 'broker down' },
],
});
const expectedMessage =
'Some triggers failed to activate: "Schedule": cron unavailable; "Kafka": broker down';
- expect(outboxRepository.markPartialSuccess).toHaveBeenCalledWith(1, expectedMessage);
- expect(activationErrorsService.register).toHaveBeenCalledWith('wf-1', expectedMessage);
+ expect(outboxRepository.markPartialSuccess).toHaveBeenCalledWith(
+ 1,
+ expectedMessage,
+ entityManager,
+ );
+ expect(triggerStatusRepository.replaceForWorkflow).toHaveBeenCalledWith(
+ 'wf-1',
+ [
+ {
+ nodeId: 'a',
+ versionId: 'v-2',
+ status: 'activated',
+ errorMessage: null,
+ },
+ {
+ nodeId: 'b',
+ versionId: 'v-2',
+ status: 'failed',
+ errorMessage: 'cron unavailable',
+ },
+ {
+ nodeId: 'c',
+ versionId: 'v-2',
+ status: 'failed',
+ errorMessage: 'broker down',
+ },
+ ],
+ entityManager,
+ );
+ // CAT-3432: partial path must NOT register activation errors
+ expect(activationErrorsService.register).not.toHaveBeenCalled();
expect(push.broadcast).toHaveBeenCalledWith({
type: 'workflowPartiallyActivated',
data: {
diff --git a/packages/cli/src/workflows/publication/__tests__/workflow-publication-applier.test.ts b/packages/cli/src/workflows/publication/__tests__/workflow-publication-applier.test.ts
index 52fec479fa3..12ca55cff7f 100644
--- a/packages/cli/src/workflows/publication/__tests__/workflow-publication-applier.test.ts
+++ b/packages/cli/src/workflows/publication/__tests__/workflow-publication-applier.test.ts
@@ -212,7 +212,10 @@ describe('WorkflowPublicationApplier', () => {
const result = await applier.apply(makeRecord());
- expect(result).toEqual({ type: 'completed' });
+ expect(result).toEqual({
+ type: 'completed',
+ triggerStatuses: [{ nodeId: 'a', nodeName: 'a', status: 'activated' }],
+ });
expect(workflowPublishedVersionRepository.setPublishedVersion).toHaveBeenCalledWith(
'wf-1',
'v-2',
@@ -230,7 +233,13 @@ describe('WorkflowPublicationApplier', () => {
const result = await applier.apply(makeRecord());
- expect(result).toEqual({ type: 'completed' });
+ expect(result).toEqual({
+ type: 'completed',
+ triggerStatuses: [
+ { nodeId: 'a', nodeName: 'a', status: 'activated' },
+ { nodeId: 'b', nodeName: 'b', status: 'activated' },
+ ],
+ });
expect(workflowTriggerActivator.deactivate).not.toHaveBeenCalled();
expect(workflowTriggerActivator.activate).toHaveBeenCalledWith(
expect.objectContaining({ id: 'wf-1' }),
@@ -254,7 +263,10 @@ describe('WorkflowPublicationApplier', () => {
const result = await applier.apply(makeRecord());
- expect(result).toEqual({ type: 'completed' });
+ expect(result).toEqual({
+ type: 'completed',
+ triggerStatuses: [{ nodeId: 'a', nodeName: 'a', status: 'activated' }],
+ });
expect(workflowTriggerActivator.getUnregisteredNonWebhookTriggerNodeIds).toHaveBeenCalledWith(
'wf-1',
[trigger],
@@ -274,7 +286,10 @@ describe('WorkflowPublicationApplier', () => {
const result = await applier.apply(makeRecord());
- expect(result).toEqual({ type: 'completed' });
+ expect(result).toEqual({
+ type: 'completed',
+ triggerStatuses: [{ nodeId: 'a', nodeName: 'a', status: 'activated' }],
+ });
expect(workflowTriggerActivator.activate).not.toHaveBeenCalled();
expect(workflowTriggerActivator.deactivate).not.toHaveBeenCalled();
expect(workflowPublishedVersionRepository.setPublishedVersion).toHaveBeenCalledWith(
@@ -293,7 +308,10 @@ describe('WorkflowPublicationApplier', () => {
const result = await applier.apply(makeRecord());
- expect(result).toEqual({ type: 'completed' });
+ expect(result).toEqual({
+ type: 'completed',
+ triggerStatuses: [{ nodeId: 'a', nodeName: 'a', status: 'activated' }],
+ });
expect(workflowTriggerActivator.getNodesWithUnregisteredWebhooks).toHaveBeenCalledWith(
expect.objectContaining({ id: 'wf-1' }),
newVersion,
@@ -313,7 +331,13 @@ describe('WorkflowPublicationApplier', () => {
const result = await applier.apply(makeRecord());
- expect(result).toEqual({ type: 'completed' });
+ expect(result).toEqual({
+ type: 'completed',
+ triggerStatuses: [
+ { nodeId: 'a', nodeName: 'a', status: 'activated' },
+ { nodeId: 'b', nodeName: 'b', status: 'activated' },
+ ],
+ });
expect(workflowTriggerActivator.activate).toHaveBeenCalledWith(
expect.objectContaining({ id: 'wf-1' }),
newVersion,
@@ -326,7 +350,10 @@ describe('WorkflowPublicationApplier', () => {
const result = await applier.apply(makeRecord());
- expect(result).toEqual({ type: 'completed' });
+ expect(result).toEqual({
+ type: 'completed',
+ triggerStatuses: [{ nodeId: 'a', nodeName: 'a', status: 'activated' }],
+ });
expect(workflowTriggerActivator.deactivate).toHaveBeenCalledWith(
expect.objectContaining({ id: 'wf-1' }),
oldVersion,
@@ -365,7 +392,10 @@ describe('WorkflowPublicationApplier', () => {
const result = await applier.apply(makeRecord());
- expect(result).toEqual({ type: 'completed' });
+ expect(result).toEqual({
+ type: 'completed',
+ triggerStatuses: [{ nodeId: 'a', nodeName: 'a', status: 'activated' }],
+ });
expect(workflowTriggerActivator.deactivate).toHaveBeenCalledWith(
expect.objectContaining({ id: 'wf-1' }),
oldVersion,
@@ -423,8 +453,10 @@ describe('WorkflowPublicationApplier', () => {
expect(result).toEqual({
type: 'partial',
- activatedNodeIds: ['a'],
- failures: [{ nodeId: 'b', nodeName: 'b', error }],
+ triggerStatuses: [
+ { nodeId: 'a', nodeName: 'a', status: 'activated' },
+ { nodeId: 'b', nodeName: 'b', status: 'failed', errorMessage: 'third-party unavailable' },
+ ],
});
// The new version is published despite the partial activation; no deactivation.
expect(workflowPublishedVersionRepository.setPublishedVersion).toHaveBeenCalledWith(
@@ -446,8 +478,10 @@ describe('WorkflowPublicationApplier', () => {
expect(result).toEqual({
type: 'partial',
- activatedNodeIds: ['a'],
- failures: [{ nodeId: 'b', nodeName: 'b', error }],
+ triggerStatuses: [
+ { nodeId: 'a', nodeName: 'a', status: 'activated' },
+ { nodeId: 'b', nodeName: 'b', status: 'failed', errorMessage: error.message },
+ ],
});
expect(workflowPublishedVersionRepository.setPublishedVersion).toHaveBeenCalledWith(
'wf-1',
@@ -466,7 +500,13 @@ describe('WorkflowPublicationApplier', () => {
const result = await applier.apply(makeRecord());
// A single failure passes its error through, preserving the type.
- expect(result).toEqual({ type: 'failed', error });
+ expect(result).toEqual({
+ type: 'failed',
+ error,
+ triggerStatuses: [
+ { nodeId: 'b', nodeName: 'b', status: 'failed', errorMessage: error.message },
+ ],
+ });
expect(workflowPublishedVersionRepository.setPublishedVersion).toHaveBeenCalledWith(
'wf-1',
'v-2',
@@ -485,7 +525,13 @@ describe('WorkflowPublicationApplier', () => {
const result = await applier.apply(makeRecord());
- expect(result).toEqual({ type: 'partial', activatedNodeIds: [], failures });
+ expect(result).toEqual({
+ type: 'partial',
+ triggerStatuses: [
+ { nodeId: 'b', nodeName: 'b', status: 'failed', errorMessage: deterministic.message },
+ { nodeId: 'c', nodeName: 'c', status: 'failed', errorMessage: 'third-party unavailable' },
+ ],
+ });
});
test('treats a first publication (no published-version mapping yet) as all-added', async () => {
@@ -494,7 +540,10 @@ describe('WorkflowPublicationApplier', () => {
const result = await applier.apply(makeRecord());
- expect(result).toEqual({ type: 'completed' });
+ expect(result).toEqual({
+ type: 'completed',
+ triggerStatuses: [{ nodeId: 'a', nodeName: 'a', status: 'activated' }],
+ });
expect(workflowTriggerActivator.deactivate).not.toHaveBeenCalled();
expect(workflowTriggerActivator.activate).toHaveBeenCalledWith(
expect.objectContaining({ id: 'wf-1' }),
diff --git a/packages/cli/src/workflows/publication/__tests__/workflow-publication-outbox-consumer.test.ts b/packages/cli/src/workflows/publication/__tests__/workflow-publication-outbox-consumer.test.ts
index 1cd45691317..8bc71468779 100644
--- a/packages/cli/src/workflows/publication/__tests__/workflow-publication-outbox-consumer.test.ts
+++ b/packages/cli/src/workflows/publication/__tests__/workflow-publication-outbox-consumer.test.ts
@@ -62,7 +62,7 @@ describe('WorkflowPublicationOutboxConsumer', () => {
vi.useFakeTimers();
tracing.startSpan.mockImplementation(async (_opts, spanCb) => await spanCb(mock()));
outboxRepository.claimNextPendingRecord.mockResolvedValue(null);
- applier.apply.mockResolvedValue({ type: 'completed' });
+ applier.apply.mockResolvedValue({ type: 'completed', triggerStatuses: [] });
reporter.report.mockResolvedValue(undefined);
consumer = createConsumer();
});
@@ -205,7 +205,7 @@ describe('WorkflowPublicationOutboxConsumer', () => {
await new Promise((resolve) => {
releaseApply = resolve;
});
- return { type: 'completed' };
+ return { type: 'completed', triggerStatuses: [] };
});
consumer.startPolling();
@@ -235,7 +235,7 @@ describe('WorkflowPublicationOutboxConsumer', () => {
describe('processRecord', () => {
test('applies the record then reports the result', async () => {
const record = makeRecord();
- const result: PublicationResult = { type: 'completed' };
+ const result: PublicationResult = { type: 'completed', triggerStatuses: [] };
applier.apply.mockResolvedValue(result);
await consumer.processRecord(record);
diff --git a/packages/cli/src/workflows/publication/publication-result.ts b/packages/cli/src/workflows/publication/publication-result.ts
index cc00431907d..93ba390364d 100644
--- a/packages/cli/src/workflows/publication/publication-result.ts
+++ b/packages/cli/src/workflows/publication/publication-result.ts
@@ -1,5 +1,3 @@
-import type { TriggerActivationFailure } from '@/workflows/triggers/workflow-trigger-activator';
-
/**
* Why a publication did no trigger work and was completed without advancing any
* triggers. The outcome is still a success (the record is marked completed); the
@@ -11,6 +9,26 @@ export type PublicationSkipReason =
/** The workflow is no longer active, so there are no triggers to reconcile. */
| 'workflow-inactive';
+/** A trigger that activated successfully; carries no error. */
+type ActivatedTriggerPublicationStatus = {
+ nodeId: string;
+ nodeName: string;
+ status: 'activated';
+};
+
+/** A trigger that failed to activate; always carries the failure message. */
+export type FailedTriggerPublicationStatus = {
+ nodeId: string;
+ nodeName: string;
+ status: 'failed';
+ errorMessage: string;
+};
+
+/** The activation status of a single trigger node after a publication attempt. */
+export type TriggerPublicationStatus =
+ | ActivatedTriggerPublicationStatus
+ | FailedTriggerPublicationStatus;
+
/**
* The outcome of applying a single publication outbox record, as produced by
* {@link WorkflowPublicationApplier} and consumed by
@@ -20,7 +38,7 @@ export type PublicationSkipReason =
*/
export type PublicationResult =
/** Triggers reconciled (or no change needed); the published version advanced. */
- | { type: 'completed' }
+ | { type: 'completed'; triggerStatuses: TriggerPublicationStatus[] }
/**
* The workflow was unpublished: the triggers of the previously published
* version were torn down and the `workflow_published_version` mapping removed.
@@ -34,8 +52,8 @@ export type PublicationResult =
/**
* The published version advanced and some triggers are running, but others
* failed to register. The record is marked `partial_success` and the workflow
- * stays published (no auto-unpublish); the failures carry per-node detail.
+ * stays published (no auto-unpublish); per-trigger detail is in `triggerStatuses`.
*/
- | { type: 'partial'; activatedNodeIds: Array; failures: TriggerActivationFailure[] }
+ | { type: 'partial'; triggerStatuses: TriggerPublicationStatus[] }
/** The publication failed; the record is failed and the error is reported. */
- | { type: 'failed'; error: Error };
+ | { type: 'failed'; error: Error; triggerStatuses?: TriggerPublicationStatus[] };
diff --git a/packages/cli/src/workflows/publication/publication-status-reporter.ts b/packages/cli/src/workflows/publication/publication-status-reporter.ts
index 688dcc0901b..16e0d410ff2 100644
--- a/packages/cli/src/workflows/publication/publication-status-reporter.ts
+++ b/packages/cli/src/workflows/publication/publication-status-reporter.ts
@@ -1,21 +1,27 @@
import { Logger } from '@n8n/backend-common';
-import { WorkflowPublicationOutbox, WorkflowPublicationOutboxRepository } from '@n8n/db';
+import {
+ WorkflowPublicationOutbox,
+ WorkflowPublicationOutboxRepository,
+ WorkflowPublicationTriggerStatusRepository,
+ type TriggerStatusRow,
+} from '@n8n/db';
import { Service } from '@n8n/di';
import { ErrorReporter } from 'n8n-core';
import { ActivationErrorsService } from '@/activation-errors.service';
import { Push } from '@/push';
import type {
+ FailedTriggerPublicationStatus,
PublicationResult,
PublicationSkipReason,
+ TriggerPublicationStatus,
} from '@/workflows/publication/publication-result';
-import type { TriggerActivationFailure } from '@/workflows/triggers/workflow-trigger-activator';
/**
* Turns a {@link PublicationResult} into terminal state. This is the only place
* that writes terminal outbox statuses and the only place that maps a result to
- * its side effects: clearing or (in a later phase) recording activation errors,
- * and pushing publication status to the UI.
+ * its side effects: persisting per-trigger status rows, clearing legacy activation
+ * errors on success, and pushing publication status to the UI.
*/
@Service()
export class PublicationStatusReporter {
@@ -25,6 +31,7 @@ export class PublicationStatusReporter {
private readonly outboxRepository: WorkflowPublicationOutboxRepository,
private readonly activationErrorsService: ActivationErrorsService,
private readonly push: Push,
+ private readonly triggerStatusRepository: WorkflowPublicationTriggerStatusRepository,
) {
this.logger = this.logger.scoped('workflow-publication');
}
@@ -32,7 +39,7 @@ export class PublicationStatusReporter {
async report(record: WorkflowPublicationOutbox, result: PublicationResult): Promise {
switch (result.type) {
case 'completed': {
- await this.complete(record);
+ await this.complete(record, this.toRows(record, result.triggerStatuses));
this.push.broadcast({
type: 'workflowActivated',
data: { workflowId: record.workflowId, activeVersionId: record.publishedVersionId },
@@ -41,7 +48,7 @@ export class PublicationStatusReporter {
}
case 'unpublished': {
- await this.complete(record);
+ await this.complete(record, /*triggerStatuses=*/ []);
this.push.broadcast({
type: 'workflowDeactivated',
data: { workflowId: record.workflowId },
@@ -68,14 +75,24 @@ export class PublicationStatusReporter {
}
case 'failed': {
+ const { triggerStatuses } = result;
+ await this.outboxRepository.manager.transaction(async (trx) => {
+ if (triggerStatuses) {
+ await this.triggerStatusRepository.replaceForWorkflow(
+ record.workflowId,
+ this.toRows(record, triggerStatuses),
+ trx,
+ );
+ }
+ await this.outboxRepository.markFailed(record.id, result.error.message, trx);
+ });
this.errorReporter.error(result.error, { shouldBeLogged: true });
- await this.outboxRepository.markFailed(record.id, result.error.message);
this.pushFailedToActivate(record.workflowId, result.error.message);
return;
}
case 'partial': {
- await this.reportPartial(record, result.failures);
+ await this.reportPartial(record, result.triggerStatuses);
return;
}
}
@@ -83,27 +100,36 @@ export class PublicationStatusReporter {
/**
* Reports a partial publication: the new version stays published with the
- * surviving triggers running. Records a `partial_success` status, registers a
- * structured per-node activation error so it surfaces on reload, and pushes the
- * failure detail to connected clients. The workflow is not unpublished.
+ * surviving triggers running. Marks the outbox record `partial_success`,
+ * full-replaces the workflow's per-trigger status rows, and pushes the
+ * per-node failure detail to connected clients. The workflow is not unpublished.
*
* The push is leader-local for now; multi-main pubsub routing is tracked as
* follow-up work (see CAT-3423).
*/
private async reportPartial(
record: WorkflowPublicationOutbox,
- failures: TriggerActivationFailure[],
+ triggerStatuses: TriggerPublicationStatus[],
): Promise {
+ const failures = triggerStatuses.filter(
+ (s): s is FailedTriggerPublicationStatus => s.status === 'failed',
+ );
const errorMessage = this.formatActivationError(failures);
this.logger.warn('Workflow partially published; some triggers failed to activate', {
workflowId: record.workflowId,
outboxId: record.id,
- failedNodeIds: failures.map((failure) => failure.nodeId),
+ failedNodeIds: failures.map((s) => s.nodeId),
});
- await this.outboxRepository.markPartialSuccess(record.id, errorMessage);
- await this.activationErrorsService.register(record.workflowId, errorMessage);
+ await this.outboxRepository.manager.transaction(async (trx) => {
+ await this.triggerStatusRepository.replaceForWorkflow(
+ record.workflowId,
+ this.toRows(record, triggerStatuses),
+ trx,
+ );
+ await this.outboxRepository.markPartialSuccess(record.id, errorMessage, trx);
+ });
this.push.broadcast({
type: 'workflowPartiallyActivated',
@@ -111,19 +137,32 @@ export class PublicationStatusReporter {
workflowId: record.workflowId,
activeVersionId: record.publishedVersionId,
errorMessage,
- failedNodes: failures.map((failure) => ({
- nodeId: failure.nodeId,
- nodeName: failure.nodeName,
- errorMessage: failure.error.message,
+ failedNodes: failures.map((triggerStatus) => ({
+ nodeId: triggerStatus.nodeId,
+ nodeName: triggerStatus.nodeName,
+ errorMessage: triggerStatus.errorMessage,
})),
},
});
}
+ /** Maps trigger publication statuses to repository row objects, stamping the published version. */
+ private toRows(
+ record: WorkflowPublicationOutbox,
+ statuses: TriggerPublicationStatus[],
+ ): TriggerStatusRow[] {
+ return statuses.map((triggerStatus) => ({
+ nodeId: triggerStatus.nodeId,
+ versionId: record.publishedVersionId,
+ status: triggerStatus.status,
+ errorMessage: triggerStatus.status === 'failed' ? triggerStatus.errorMessage : null,
+ }));
+ }
+
/** Builds a human-readable message naming each failed node and its error. */
- private formatActivationError(failures: TriggerActivationFailure[]): string {
+ private formatActivationError(failures: FailedTriggerPublicationStatus[]): string {
const detail = failures
- .map((failure) => `"${failure.nodeName}": ${failure.error.message}`)
+ .map((status) => `"${status.nodeName}": ${status.errorMessage}`)
.join('; ');
return `Some triggers failed to activate: ${detail}`;
@@ -137,9 +176,24 @@ export class PublicationStatusReporter {
});
}
- /** Marks the record completed and clears any activation errors for the workflow. */
- private async complete(record: WorkflowPublicationOutbox): Promise {
- await this.outboxRepository.markCompleted(record.id);
+ /**
+ * Marks the record completed and clears any activation errors for the workflow.
+ * If there are any per-trigger statuses passed in, they are persisted in the same transaction.
+ */
+ private async complete(
+ record: WorkflowPublicationOutbox,
+ triggerStatuses?: TriggerStatusRow[],
+ ): Promise {
+ await this.outboxRepository.manager.transaction(async (trx) => {
+ if (triggerStatuses !== undefined) {
+ await this.triggerStatusRepository.replaceForWorkflow(
+ record.workflowId,
+ triggerStatuses,
+ trx,
+ );
+ }
+ await this.outboxRepository.markCompleted(record.id, trx);
+ });
await this.activationErrorsService.deregister(record.workflowId);
}
diff --git a/packages/cli/src/workflows/publication/workflow-publication-applier.ts b/packages/cli/src/workflows/publication/workflow-publication-applier.ts
index 630f480ccf2..5b5f61bfc9b 100644
--- a/packages/cli/src/workflows/publication/workflow-publication-applier.ts
+++ b/packages/cli/src/workflows/publication/workflow-publication-applier.ts
@@ -8,9 +8,13 @@ import {
WorkflowRepository,
} from '@n8n/db';
import { Service } from '@n8n/di';
+import type { INode } from 'n8n-workflow';
import { ensureError } from 'n8n-workflow';
-import type { PublicationResult } from '@/workflows/publication/publication-result';
+import type {
+ PublicationResult,
+ TriggerPublicationStatus,
+} from '@/workflows/publication/publication-result';
import { computeTriggerDiff } from '@/workflows/publication/trigger-diff';
import { isTransientActivationError } from '@/workflows/triggers/trigger-activation-retry';
import {
@@ -127,7 +131,13 @@ export class WorkflowPublicationApplier {
// triggers keep running and re-read the new version on their next fire.
if (toAdd.size === 0 && toRemove.size === 0) {
await this.advancePublishedVersion(record);
- return { type: 'completed' };
+ return {
+ type: 'completed',
+ triggerStatuses: this.buildTriggerStatuses(desiredTriggerNodes, {
+ activated: [],
+ failures: [],
+ }),
+ };
}
// Must happen BEFORE advancing the version, using the currently published
@@ -142,7 +152,7 @@ export class WorkflowPublicationApplier {
try {
if (toAdd.size > 0) {
const outcome = await this.workflowTriggerActivator.activate(workflow, newVersion, toAdd);
- return this.classifyActivationOutcome(outcome);
+ return this.classifyActivationOutcome(outcome, desiredTriggerNodes);
}
if (toRemove.size > 0) {
@@ -152,7 +162,13 @@ export class WorkflowPublicationApplier {
return { type: 'failed', error: ensureError(e) };
}
- return { type: 'completed' };
+ return {
+ type: 'completed',
+ triggerStatuses: this.buildTriggerStatuses(desiredTriggerNodes, {
+ activated: [],
+ failures: [],
+ }),
+ };
}
/**
@@ -191,23 +207,45 @@ export class WorkflowPublicationApplier {
}
/**
- * Maps a per-node activation outcomes to a combined publication result.
+ * Maps per-node activation outcomes to a combined publication result, attaching
+ * the full desired trigger set's statuses to every version-advancing result.
*/
- private classifyActivationOutcome(outcome: TriggerActivationOutcome): PublicationResult {
- if (outcome.failures.length === 0) return { type: 'completed' };
+ private classifyActivationOutcome(
+ outcome: TriggerActivationOutcome,
+ desiredTriggerNodes: INode[],
+ ): PublicationResult {
+ const triggerStatuses = this.buildTriggerStatuses(desiredTriggerNodes, outcome);
+ if (outcome.failures.length === 0) return { type: 'completed', triggerStatuses };
- const allDeterministic = outcome.failures.every(
- (failure) => !isTransientActivationError(failure.error),
- );
+ const allDeterministic = outcome.failures.every((f) => !isTransientActivationError(f.error));
if (outcome.activated.length === 0 && allDeterministic) {
- return { type: 'failed', error: this.toActivationError(outcome.failures) };
+ return { type: 'failed', error: this.toActivationError(outcome.failures), triggerStatuses };
}
- return {
- type: 'partial',
- activatedNodeIds: outcome.activated,
- failures: outcome.failures,
- };
+ return { type: 'partial', triggerStatuses };
+ }
+
+ /**
+ * Builds per-trigger statuses for the full set of desired trigger nodes.
+ * Nodes in `outcome.failures` are marked `failed`; all others are `activated`,
+ * including unchanged-but-still-running triggers that were not in `toAdd`.
+ */
+ private buildTriggerStatuses(
+ desiredTriggerNodes: INode[],
+ outcome: TriggerActivationOutcome,
+ ): TriggerPublicationStatus[] {
+ const failureByNodeId = new Map(outcome.failures.map((f) => [f.nodeId, f]));
+ return desiredTriggerNodes.map((node): TriggerPublicationStatus => {
+ const failure = failureByNodeId.get(node.id);
+ return failure
+ ? {
+ nodeId: node.id,
+ nodeName: node.name,
+ status: 'failed',
+ errorMessage: failure.error.message,
+ }
+ : { nodeId: node.id, nodeName: node.name, status: 'activated' };
+ });
}
/**
diff --git a/packages/cli/test/integration/workflow-publication-trigger-status.repository.test.ts b/packages/cli/test/integration/workflow-publication-trigger-status.repository.test.ts
new file mode 100644
index 00000000000..d92a897c2d4
--- /dev/null
+++ b/packages/cli/test/integration/workflow-publication-trigger-status.repository.test.ts
@@ -0,0 +1,89 @@
+import { createWorkflow, createWorkflowHistory, testDb } from '@n8n/backend-test-utils';
+import type { IWorkflowDb } from '@n8n/db';
+import {
+ WorkflowHistoryRepository,
+ WorkflowPublicationTriggerStatusRepository,
+ WorkflowRepository,
+} from '@n8n/db';
+import { Container } from '@n8n/di';
+
+/** Seeds `workflow_history` rows so the trigger-status versionId FK is satisfiable. */
+async function seedVersions(workflow: IWorkflowDb, versionIds: string[]): Promise {
+ for (const versionId of versionIds) {
+ await createWorkflowHistory(workflow, undefined, undefined, { versionId });
+ }
+}
+
+describe('WorkflowPublicationTriggerStatusRepository', () => {
+ let repo: WorkflowPublicationTriggerStatusRepository;
+ let workflowRepository: WorkflowRepository;
+ let workflowHistoryRepository: WorkflowHistoryRepository;
+
+ // Shared fixture for the non-destructive tests; the CASCADE tests create their
+ // own workflow/version since they delete the parent.
+ let workflow: IWorkflowDb;
+
+ beforeAll(async () => {
+ await testDb.init();
+ repo = Container.get(WorkflowPublicationTriggerStatusRepository);
+ workflowRepository = Container.get(WorkflowRepository);
+ workflowHistoryRepository = Container.get(WorkflowHistoryRepository);
+
+ workflow = await createWorkflow();
+ await seedVersions(workflow, ['v1', 'v2']);
+ });
+ afterEach(async () => {
+ await testDb.truncate(['WorkflowPublicationTriggerStatus']);
+ });
+ afterAll(async () => await testDb.terminate());
+
+ it('replaceForWorkflow inserts rows then overwrites them', async () => {
+ await repo.replaceForWorkflow(workflow.id, [
+ { nodeId: 'n1', versionId: 'v1', status: 'activated', errorMessage: null },
+ { nodeId: 'n2', versionId: 'v1', status: 'failed', errorMessage: 'boom' },
+ ]);
+ expect(await repo.findByWorkflowId(workflow.id)).toHaveLength(2);
+
+ await repo.replaceForWorkflow(workflow.id, [
+ { nodeId: 'n1', versionId: 'v2', status: 'activated', errorMessage: null },
+ ]);
+ const rows = await repo.findByWorkflowId(workflow.id);
+ expect(rows).toHaveLength(1);
+ expect(rows[0]).toMatchObject({ nodeId: 'n1', versionId: 'v2', status: 'activated' });
+ });
+
+ it('replaceForWorkflow with an empty list clears all rows', async () => {
+ await repo.replaceForWorkflow(workflow.id, [
+ { nodeId: 'n1', versionId: 'v1', status: 'activated', errorMessage: null },
+ ]);
+ await repo.replaceForWorkflow(workflow.id, []);
+ expect(await repo.findByWorkflowId(workflow.id)).toHaveLength(0);
+ });
+
+ it('FK CASCADE deletes trigger status rows when parent workflow is deleted', async () => {
+ const ownWorkflow = await createWorkflow();
+ await seedVersions(ownWorkflow, ['v-wf-cascade']);
+ await repo.replaceForWorkflow(ownWorkflow.id, [
+ { nodeId: 'n1', versionId: 'v-wf-cascade', status: 'activated', errorMessage: null },
+ { nodeId: 'n2', versionId: 'v-wf-cascade', status: 'failed', errorMessage: 'boom' },
+ ]);
+ expect(await repo.findByWorkflowId(ownWorkflow.id)).toHaveLength(2);
+
+ await workflowRepository.delete(ownWorkflow.id);
+
+ expect(await repo.findByWorkflowId(ownWorkflow.id)).toEqual([]);
+ });
+
+ it('FK CASCADE deletes trigger status rows when the referenced version is deleted', async () => {
+ const ownWorkflow = await createWorkflow();
+ await seedVersions(ownWorkflow, ['v-version-cascade']);
+ await repo.replaceForWorkflow(ownWorkflow.id, [
+ { nodeId: 'n1', versionId: 'v-version-cascade', status: 'activated', errorMessage: null },
+ ]);
+ expect(await repo.findByWorkflowId(ownWorkflow.id)).toHaveLength(1);
+
+ await workflowHistoryRepository.delete({ versionId: 'v-version-cascade' });
+
+ expect(await repo.findByWorkflowId(ownWorkflow.id)).toEqual([]);
+ });
+});