@elizaos/plugin-workflow 2.0.0-beta.1 → 2.0.3-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +28 -26
  3. package/dist/actions/eval-code.d.ts +12 -0
  4. package/dist/actions/eval-code.d.ts.map +1 -0
  5. package/dist/actions/eval-code.js +59 -0
  6. package/dist/actions/eval-code.js.map +1 -0
  7. package/dist/actions/index.d.ts +1 -0
  8. package/dist/actions/index.d.ts.map +1 -1
  9. package/dist/actions/index.js +1 -0
  10. package/dist/actions/index.js.map +1 -1
  11. package/dist/actions/workflow.d.ts +7 -0
  12. package/dist/actions/workflow.d.ts.map +1 -1
  13. package/dist/actions/workflow.js +462 -10
  14. package/dist/actions/workflow.js.map +1 -1
  15. package/dist/db/schema.d.ts +196 -0
  16. package/dist/db/schema.d.ts.map +1 -1
  17. package/dist/db/schema.js +23 -0
  18. package/dist/db/schema.js.map +1 -1
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +9 -64
  21. package/dist/index.js.map +1 -1
  22. package/dist/lib/automations-builder.d.ts.map +1 -1
  23. package/dist/lib/automations-builder.js +10 -35
  24. package/dist/lib/automations-builder.js.map +1 -1
  25. package/dist/lib/automations-types.d.ts +2 -2
  26. package/dist/lib/automations-types.d.ts.map +1 -1
  27. package/dist/lib/automations-types.js.map +1 -1
  28. package/dist/lib/index.d.ts +0 -2
  29. package/dist/lib/index.d.ts.map +1 -1
  30. package/dist/lib/index.js +1 -2
  31. package/dist/lib/index.js.map +1 -1
  32. package/dist/lib/workflow-clarification.d.ts +2 -2
  33. package/dist/lib/workflow-clarification.d.ts.map +1 -1
  34. package/dist/lib/workflow-clarification.js +15 -11
  35. package/dist/lib/workflow-clarification.js.map +1 -1
  36. package/dist/plugin-routes.d.ts.map +1 -1
  37. package/dist/plugin-routes.js +6 -0
  38. package/dist/plugin-routes.js.map +1 -1
  39. package/dist/providers/activeWorkflows.js +2 -2
  40. package/dist/providers/activeWorkflows.js.map +1 -1
  41. package/dist/providers/workflowStatus.js +1 -1
  42. package/dist/providers/workflowStatus.js.map +1 -1
  43. package/dist/routes/workflow-routes.d.ts.map +1 -1
  44. package/dist/routes/workflow-routes.js +68 -2
  45. package/dist/routes/workflow-routes.js.map +1 -1
  46. package/dist/routes/workflows.d.ts.map +1 -1
  47. package/dist/routes/workflows.js +5 -1
  48. package/dist/routes/workflows.js.map +1 -1
  49. package/dist/services/embedded-workflow-service.d.ts +74 -17
  50. package/dist/services/embedded-workflow-service.d.ts.map +1 -1
  51. package/dist/services/embedded-workflow-service.js +343 -149
  52. package/dist/services/embedded-workflow-service.js.map +1 -1
  53. package/dist/services/smithers-runtime.d.ts +47 -0
  54. package/dist/services/smithers-runtime.d.ts.map +1 -0
  55. package/dist/services/smithers-runtime.js +444 -0
  56. package/dist/services/smithers-runtime.js.map +1 -0
  57. package/dist/services/workflow-credential-store.js +1 -1
  58. package/dist/services/workflow-credential-store.js.map +1 -1
  59. package/dist/services/workflow-dispatch.d.ts +31 -1
  60. package/dist/services/workflow-dispatch.d.ts.map +1 -1
  61. package/dist/services/workflow-dispatch.js +75 -10
  62. package/dist/services/workflow-dispatch.js.map +1 -1
  63. package/dist/services/workflow-service.d.ts +27 -1
  64. package/dist/services/workflow-service.d.ts.map +1 -1
  65. package/dist/services/workflow-service.js +133 -11
  66. package/dist/services/workflow-service.js.map +1 -1
  67. package/dist/trigger-routes.d.ts +2 -18
  68. package/dist/trigger-routes.d.ts.map +1 -1
  69. package/dist/trigger-routes.js +11 -39
  70. package/dist/trigger-routes.js.map +1 -1
  71. package/dist/types/index.d.ts +82 -2
  72. package/dist/types/index.d.ts.map +1 -1
  73. package/dist/types/index.js.map +1 -1
  74. package/dist/types/workflow-contracts.d.ts +118 -0
  75. package/dist/types/workflow-contracts.d.ts.map +1 -0
  76. package/dist/types/workflow-contracts.js +2 -0
  77. package/dist/types/workflow-contracts.js.map +1 -0
  78. package/dist/utils/catalog.js +2 -2
  79. package/dist/utils/catalog.js.map +1 -1
  80. package/dist/utils/clarification.d.ts +1 -1
  81. package/dist/utils/clarification.d.ts.map +1 -1
  82. package/dist/utils/clarification.js +15 -4
  83. package/dist/utils/clarification.js.map +1 -1
  84. package/dist/utils/context.js +1 -1
  85. package/dist/utils/context.js.map +1 -1
  86. package/dist/utils/evaluation-samples.d.ts +6 -0
  87. package/dist/utils/evaluation-samples.d.ts.map +1 -0
  88. package/dist/utils/evaluation-samples.js +216 -0
  89. package/dist/utils/evaluation-samples.js.map +1 -0
  90. package/dist/utils/execution-diagnostics.d.ts +26 -0
  91. package/dist/utils/execution-diagnostics.d.ts.map +1 -0
  92. package/dist/utils/execution-diagnostics.js +159 -0
  93. package/dist/utils/execution-diagnostics.js.map +1 -0
  94. package/dist/utils/generation.d.ts.map +1 -1
  95. package/dist/utils/generation.js +134 -19
  96. package/dist/utils/generation.js.map +1 -1
  97. package/dist/utils/host-capabilities.d.ts.map +1 -1
  98. package/dist/utils/host-capabilities.js +20 -5
  99. package/dist/utils/host-capabilities.js.map +1 -1
  100. package/dist/utils/inferSyntheticOutputSchema.js +3 -3
  101. package/dist/utils/inferSyntheticOutputSchema.js.map +1 -1
  102. package/dist/utils/outputSchema.js +1 -1
  103. package/dist/utils/outputSchema.js.map +1 -1
  104. package/dist/utils/validateAndRepair.js +10 -10
  105. package/dist/utils/validateAndRepair.js.map +1 -1
  106. package/dist/utils/workflow-prompts/draftIntent.d.ts +1 -1
  107. package/dist/utils/workflow-prompts/draftIntent.d.ts.map +1 -1
  108. package/dist/utils/workflow-prompts/draftIntent.js +1 -1
  109. package/dist/utils/workflow-prompts/keywordExtraction.d.ts +1 -1
  110. package/dist/utils/workflow-prompts/keywordExtraction.d.ts.map +1 -1
  111. package/dist/utils/workflow-prompts/keywordExtraction.js +1 -1
  112. package/dist/utils/workflow-prompts/workflowGeneration.d.ts +1 -1
  113. package/dist/utils/workflow-prompts/workflowGeneration.d.ts.map +1 -1
  114. package/dist/utils/workflow-prompts/workflowGeneration.js +4 -4
  115. package/dist/utils/workflow-prompts/workflowMatching.d.ts +1 -1
  116. package/dist/utils/workflow-prompts/workflowMatching.d.ts.map +1 -1
  117. package/dist/utils/workflow-prompts/workflowMatching.js +1 -1
  118. package/dist/utils/workflow.d.ts +1 -0
  119. package/dist/utils/workflow.d.ts.map +1 -1
  120. package/dist/utils/workflow.js +44 -8
  121. package/dist/utils/workflow.js.map +1 -1
  122. package/package.json +27 -8
  123. package/registry-entry.json +25 -0
  124. package/src/actions/eval-code.ts +81 -0
  125. package/src/actions/index.ts +1 -0
  126. package/src/actions/workflow.ts +518 -10
  127. package/src/db/schema.ts +31 -0
  128. package/src/index.ts +9 -82
  129. package/src/lib/automations-builder.ts +11 -35
  130. package/src/lib/automations-types.ts +1 -2
  131. package/src/lib/index.ts +0 -8
  132. package/src/lib/workflow-clarification.ts +18 -13
  133. package/src/plugin-routes.ts +6 -0
  134. package/src/providers/activeWorkflows.ts +2 -2
  135. package/src/providers/workflowStatus.ts +1 -1
  136. package/src/routes/workflow-routes.ts +100 -2
  137. package/src/routes/workflows.ts +5 -1
  138. package/src/services/embedded-workflow-service.ts +447 -172
  139. package/src/services/smithers-runtime.ts +526 -0
  140. package/src/services/workflow-credential-store.ts +1 -1
  141. package/src/services/workflow-dispatch.ts +116 -13
  142. package/src/services/workflow-service.ts +186 -10
  143. package/src/trigger-routes.ts +12 -70
  144. package/src/types/index.ts +94 -2
  145. package/src/types/workflow-contracts.ts +166 -0
  146. package/src/utils/catalog.ts +2 -2
  147. package/src/utils/clarification.ts +19 -5
  148. package/src/utils/context.ts +1 -1
  149. package/src/utils/evaluation-samples.ts +239 -0
  150. package/src/utils/execution-diagnostics.ts +192 -0
  151. package/src/utils/generation.ts +224 -32
  152. package/src/utils/host-capabilities.ts +21 -5
  153. package/src/utils/inferSyntheticOutputSchema.ts +3 -3
  154. package/src/utils/outputSchema.ts +1 -1
  155. package/src/utils/validateAndRepair.ts +10 -10
  156. package/src/utils/workflow-prompts/draftIntent.ts +1 -1
  157. package/src/utils/workflow-prompts/keywordExtraction.ts +1 -1
  158. package/src/utils/workflow-prompts/workflowGeneration.ts +4 -4
  159. package/src/utils/workflow-prompts/workflowMatching.ts +1 -1
  160. package/src/utils/workflow.ts +56 -8
  161. package/dist/lib/legacy-task-migration.d.ts +0 -20
  162. package/dist/lib/legacy-task-migration.d.ts.map +0 -1
  163. package/dist/lib/legacy-task-migration.js +0 -110
  164. package/dist/lib/legacy-task-migration.js.map +0 -1
  165. package/dist/lib/legacy-text-trigger-migration.d.ts +0 -18
  166. package/dist/lib/legacy-text-trigger-migration.d.ts.map +0 -1
  167. package/dist/lib/legacy-text-trigger-migration.js +0 -131
  168. package/dist/lib/legacy-text-trigger-migration.js.map +0 -1
  169. package/src/lib/legacy-task-migration.ts +0 -143
  170. package/src/lib/legacy-text-trigger-migration.ts +0 -178
@@ -1,5 +1,13 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { type IAgentRuntime, logger, Service, type Task, type UUID } from '@elizaos/core';
2
+ import {
3
+ type IAgentRuntime,
4
+ logger,
5
+ Service,
6
+ stringToUuid,
7
+ TRIGGER_SCHEMA_VERSION,
8
+ type TriggerConfig,
9
+ type UUID,
10
+ } from '@elizaos/core';
3
11
  import { and, desc, eq, sql } from 'drizzle-orm';
4
12
  import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
5
13
  import {
@@ -7,6 +15,7 @@ import {
7
15
  embeddedExecutions,
8
16
  embeddedTags,
9
17
  embeddedWorkflows,
18
+ workflowRevisions,
10
19
  } from '../db/schema';
11
20
  import type {
12
21
  WorkflowCredential,
@@ -14,22 +23,24 @@ import type {
14
23
  WorkflowDefinitionResponse,
15
24
  WorkflowExecution,
16
25
  WorkflowNode,
26
+ WorkflowRevision,
27
+ WorkflowRevisionOperation,
17
28
  WorkflowTag,
18
29
  } from '../types/index';
19
30
  import { WorkflowApiError } from '../types/index';
20
31
  import { detectHostCapabilities } from '../utils/host-capabilities';
32
+ import { runWorkflowWithSmithers, type SmithersExecutionPlan } from './smithers-runtime';
21
33
 
22
34
  export const EMBEDDED_WORKFLOW_SERVICE_TYPE = 'embedded_workflow_service';
23
35
 
24
- /** TaskWorker name for scheduled workflow runs. Tasks created with this name
25
- * carry metadata.workflowId + metadata.kind = 'workflow' and get fired by
26
- * the core TaskService on the configured updateInterval. */
27
- export const WORKFLOW_RUN_TASK_WORKER_NAME = 'workflow.run';
28
-
29
- /** TaskWorker name for one-shot webhook-triggered workflow runs. A future
30
- * webhook trigger provider creates a one-shot Task pointing at this worker;
31
- * payload travels in metadata.payload. */
32
- export const WORKFLOW_WEBHOOK_TASK_WORKER_NAME = 'workflow.webhook';
36
+ /**
37
+ * Task name + tag contract for scheduled workflow runs. Mirrored from
38
+ * `packages/agent/src/triggers/runtime.ts` because plugin-workflow can't
39
+ * import @elizaos/agent (would create a dep cycle). The agent's
40
+ * `registerTriggerTaskWorker` consumes tasks with this name.
41
+ */
42
+ export const TRIGGER_TASK_NAME = 'TRIGGER_DISPATCH';
43
+ export const TRIGGER_TASK_TAGS: readonly string[] = ['queue', 'repeat', 'trigger'];
33
44
 
34
45
  /** Discriminator on TaskMetadata so the UI can route workflow tasks. */
35
46
  export const WORKFLOW_TASK_KIND = 'workflow';
@@ -37,6 +48,15 @@ export const WORKFLOW_TASK_KIND = 'workflow';
37
48
  /** Stable tag used on every workflow-backed Task so we can list+delete them. */
38
49
  const WORKFLOW_TASK_TAG = 'workflow';
39
50
 
51
+ /**
52
+ * Legacy task names retained only for rehydration cleanup. `workflow.run`
53
+ * was the prior scheduled-dispatch path; it bypassed `executeTriggerTask`
54
+ * and accumulated no run history. `workflow.webhook` had no producer and
55
+ * was dead from the start. Both are migrated/removed on service start.
56
+ */
57
+ const LEGACY_WORKFLOW_RUN_TASK_NAME = 'workflow.run';
58
+ const LEGACY_WORKFLOW_WEBHOOK_TASK_NAME = 'workflow.webhook';
59
+
40
60
  type WorkflowExecuteMode = WorkflowExecution['mode'];
41
61
 
42
62
  interface INodeExecutionData {
@@ -102,8 +122,33 @@ interface StoredWorkflowRow {
102
122
  versionId: string;
103
123
  }
104
124
 
125
+ interface StoredWorkflowRevisionRow extends StoredWorkflowRow {
126
+ id: string;
127
+ workflowId: string;
128
+ capturedAt: string;
129
+ operation: WorkflowRevisionOperation;
130
+ }
131
+
105
132
  interface ExecuteOptions {
106
133
  mode?: WorkflowExecuteMode;
134
+ /**
135
+ * Optional payload to seed the start node's first item. Used by the
136
+ * dispatch service to forward event-bridge data (e.g. `{eventKind,
137
+ * eventPayload}`) into trigger-mode workflows so `respondToEvent` and
138
+ * other nodes can read upstream context. Ignored when empty.
139
+ */
140
+ triggerData?: Record<string, unknown>;
141
+ /**
142
+ * Optional idempotency key. Persisted alongside the resulting
143
+ * execution row so the dispatch layer can detect duplicates and
144
+ * short-circuit re-runs (e.g. minute-bucketed schedule fires).
145
+ */
146
+ idempotencyKey?: string;
147
+ /**
148
+ * When false, failed manual/debug runs are returned as persisted error
149
+ * executions instead of being thrown away as route-level exceptions.
150
+ */
151
+ throwOnError?: boolean;
107
152
  }
108
153
 
109
154
  interface IncomingConnection {
@@ -165,6 +210,21 @@ function responseFromWorkflow(
165
210
  };
166
211
  }
167
212
 
213
+ function revisionFromRow(row: StoredWorkflowRevisionRow): WorkflowRevision {
214
+ return {
215
+ id: row.id,
216
+ workflowId: row.workflowId,
217
+ versionId: row.versionId,
218
+ name: row.workflow.name,
219
+ active: row.workflow.active === true,
220
+ workflow: cloneJson(row.workflow),
221
+ createdAt: row.createdAt,
222
+ updatedAt: row.updatedAt,
223
+ capturedAt: row.capturedAt,
224
+ operation: row.operation,
225
+ };
226
+ }
227
+
168
228
  function readString(value: unknown, fallback: string): string {
169
229
  return typeof value === 'string' && value.length > 0 ? value : fallback;
170
230
  }
@@ -173,6 +233,17 @@ function readNumber(value: unknown, fallback: number): number {
173
233
  return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
174
234
  }
175
235
 
236
+ /**
237
+ * Build the per-dispatch idempotency key used to dedup back-to-back
238
+ * scheduled fires for the same workflow within the same minute. Shared
239
+ * by `armSchedules` (which writes it into the task metadata) and
240
+ * `WorkflowDispatchService.execute` (which looks it up before running).
241
+ */
242
+ export function buildScheduleIdempotencyKey(workflowId: string, nextRunAtMs: number): string {
243
+ const minuteBucket = Math.floor(nextRunAtMs / 60_000);
244
+ return `${workflowId}:${minuteBucket}`;
245
+ }
246
+
176
247
  function resolveScheduleIntervalMs(parameters: Record<string, unknown>): number {
177
248
  const explicitMs = readNumber(parameters.intervalMs, NaN);
178
249
  if (Number.isFinite(explicitMs) && explicitMs > 0) return explicitMs;
@@ -882,8 +953,8 @@ function createMergeNode(): INodeType {
882
953
  ] as never,
883
954
  },
884
955
  async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
885
- const first = this.getInputData(0) ?? [];
886
- const second = this.getInputData(1) ?? [];
956
+ const first = this.getInputData(0);
957
+ const second = this.getInputData(1);
887
958
  return [[...first, ...second]];
888
959
  },
889
960
  };
@@ -970,7 +1041,7 @@ function createDateTimeNode(): INodeType {
970
1041
  const now = new Date().toISOString();
971
1042
  return [
972
1043
  inputItems.map((item, index) => ({
973
- json: { ...(item.json ?? {}), [fieldName]: now } as INodeExecutionData['json'],
1044
+ json: { ...item.json, [fieldName]: now } as INodeExecutionData['json'],
974
1045
  pairedItem: item.pairedItem ?? { item: index },
975
1046
  })),
976
1047
  ];
@@ -1006,7 +1077,7 @@ function createCryptoNode(): INodeType {
1006
1077
  raw === '' || typeof raw === 'undefined' ? JSON.stringify(item.json) : String(raw);
1007
1078
  return {
1008
1079
  json: {
1009
- ...(item.json ?? {}),
1080
+ ...item.json,
1010
1081
  [fieldName]: createHash(algorithm).update(source).digest('hex'),
1011
1082
  } as INodeExecutionData['json'],
1012
1083
  pairedItem: item.pairedItem ?? { item: index },
@@ -1070,6 +1141,19 @@ async function runQuickJsCode(jsCode: string, inputItems: INodeExecutionData[]):
1070
1141
  });
1071
1142
  }
1072
1143
 
1144
+ /**
1145
+ * Evaluate a snippet of JavaScript in the same isolated QuickJS sandbox the
1146
+ * Code node uses (5s deadline, 32 MiB cap, no host/network/fs access). Optional
1147
+ * `inputJson` is exposed to the snippet as `$json` / `item.json` / `$input[0]`.
1148
+ * The snippet body runs inside an IIFE, so `return <value>` yields the result.
1149
+ * Public entry point for the EVAL_CODE action (#8914).
1150
+ */
1151
+ export async function evalQuickJsCode(jsCode: string, inputJson?: unknown): Promise<unknown> {
1152
+ const items: INodeExecutionData[] =
1153
+ inputJson === undefined ? [] : [{ json: (inputJson ?? {}) as Record<string, unknown> }];
1154
+ return runQuickJsCode(jsCode, items);
1155
+ }
1156
+
1073
1157
  type AutonomyServiceLike = Service & {
1074
1158
  getAutonomousRoomId?(): UUID | undefined;
1075
1159
  getTargetRoomId?(): UUID | undefined;
@@ -1153,7 +1237,7 @@ function createRespondToEventNode(): INodeType {
1153
1237
 
1154
1238
  const autonomyService = resolveAutonomyService(runtime);
1155
1239
  if (!autonomyService) {
1156
- runtime.logger?.warn?.(
1240
+ runtime.logger.warn(
1157
1241
  { src: 'plugin:workflow:respondToEvent', nodeName: node.name, executionId },
1158
1242
  '[respondToEvent] Autonomy service not registered — skipping injection'
1159
1243
  );
@@ -1162,7 +1246,7 @@ function createRespondToEventNode(): INodeType {
1162
1246
 
1163
1247
  const roomId = resolveAutonomyRoomId(autonomyService);
1164
1248
  if (!roomId) {
1165
- runtime.logger?.warn?.(
1249
+ runtime.logger.warn(
1166
1250
  { src: 'plugin:workflow:respondToEvent', nodeName: node.name, executionId },
1167
1251
  '[respondToEvent] No autonomy room resolvable — skipping injection'
1168
1252
  );
@@ -1334,7 +1418,6 @@ export class EmbeddedWorkflowService extends Service {
1334
1418
  { src: 'plugin:workflow:embedded' },
1335
1419
  'Embedded workflow service registered (lazy runtime load)'
1336
1420
  );
1337
- service.registerTaskWorkers();
1338
1421
  if (runtime.db) {
1339
1422
  await service.ensureSchema();
1340
1423
  await service.rehydrateSchedules();
@@ -1347,48 +1430,6 @@ export class EmbeddedWorkflowService extends Service {
1347
1430
  // there is nothing in-process to tear down here.
1348
1431
  }
1349
1432
 
1350
- /** Register the workflow.run + workflow.webhook task workers with the
1351
- * runtime's TaskService. Idempotent — safe to call once per service start. */
1352
- private registerTaskWorkers(): void {
1353
- if (typeof this.runtime.registerTaskWorker !== 'function') return;
1354
-
1355
- if (!this.runtime.getTaskWorker?.(WORKFLOW_RUN_TASK_WORKER_NAME)) {
1356
- this.runtime.registerTaskWorker({
1357
- name: WORKFLOW_RUN_TASK_WORKER_NAME,
1358
- execute: async (_rt, _opts, task: Task) => {
1359
- const workflowId =
1360
- typeof task.metadata?.workflowId === 'string' ? task.metadata.workflowId : null;
1361
- if (!workflowId) {
1362
- throw new Error(
1363
- `${WORKFLOW_RUN_TASK_WORKER_NAME} task ${task.id ?? '?'} missing metadata.workflowId`
1364
- );
1365
- }
1366
- await this.executeWorkflow(workflowId, { mode: 'trigger' });
1367
- return undefined;
1368
- },
1369
- });
1370
- }
1371
-
1372
- if (!this.runtime.getTaskWorker?.(WORKFLOW_WEBHOOK_TASK_WORKER_NAME)) {
1373
- this.runtime.registerTaskWorker({
1374
- name: WORKFLOW_WEBHOOK_TASK_WORKER_NAME,
1375
- execute: async (_rt, _opts, task: Task) => {
1376
- const meta = task.metadata as Record<string, unknown> | undefined;
1377
- const path = typeof meta?.path === 'string' ? meta.path : null;
1378
- const method = typeof meta?.method === 'string' ? meta.method : 'POST';
1379
- const payload = isRecord(meta?.payload) ? meta.payload : {};
1380
- if (!path) {
1381
- throw new Error(
1382
- `${WORKFLOW_WEBHOOK_TASK_WORKER_NAME} task ${task.id ?? '?'} missing metadata.path`
1383
- );
1384
- }
1385
- await this.executeWebhook(path, payload, method);
1386
- return undefined;
1387
- },
1388
- });
1389
- }
1390
- }
1391
-
1392
1433
  get host(): string {
1393
1434
  return EMBEDDED_HOST;
1394
1435
  }
@@ -1456,6 +1497,32 @@ export class EmbeddedWorkflowService extends Service {
1456
1497
  CREATE INDEX IF NOT EXISTS "idx_embedded_workflows_updated_at"
1457
1498
  ON "workflow"."embedded_workflows" ("updated_at")
1458
1499
  `);
1500
+ await db.execute(sql`
1501
+ CREATE TABLE IF NOT EXISTS "workflow"."workflow_revisions" (
1502
+ "id" text PRIMARY KEY,
1503
+ "workflow_id" text NOT NULL,
1504
+ "version_id" text NOT NULL,
1505
+ "name" text NOT NULL,
1506
+ "active" boolean DEFAULT false NOT NULL,
1507
+ "workflow" jsonb NOT NULL,
1508
+ "created_at" text NOT NULL,
1509
+ "updated_at" text NOT NULL,
1510
+ "captured_at" text NOT NULL,
1511
+ "operation" text NOT NULL
1512
+ )
1513
+ `);
1514
+ await db.execute(sql`
1515
+ CREATE INDEX IF NOT EXISTS "idx_workflow_revisions_workflow_id"
1516
+ ON "workflow"."workflow_revisions" ("workflow_id")
1517
+ `);
1518
+ await db.execute(sql`
1519
+ CREATE UNIQUE INDEX IF NOT EXISTS "idx_workflow_revisions_workflow_version"
1520
+ ON "workflow"."workflow_revisions" ("workflow_id", "version_id")
1521
+ `);
1522
+ await db.execute(sql`
1523
+ CREATE INDEX IF NOT EXISTS "idx_workflow_revisions_captured_at"
1524
+ ON "workflow"."workflow_revisions" ("captured_at")
1525
+ `);
1459
1526
  await db.execute(sql`
1460
1527
  CREATE TABLE IF NOT EXISTS "workflow"."embedded_executions" (
1461
1528
  "id" text PRIMARY KEY,
@@ -1465,9 +1532,15 @@ export class EmbeddedWorkflowService extends Service {
1465
1532
  "finished" boolean DEFAULT false NOT NULL,
1466
1533
  "started_at" text NOT NULL,
1467
1534
  "stopped_at" text,
1468
- "execution" jsonb NOT NULL
1535
+ "execution" jsonb NOT NULL,
1536
+ "idempotency_key" text
1469
1537
  )
1470
1538
  `);
1539
+ // Online migration: add idempotency_key to pre-existing tables.
1540
+ await db.execute(sql`
1541
+ ALTER TABLE "workflow"."embedded_executions"
1542
+ ADD COLUMN IF NOT EXISTS "idempotency_key" text
1543
+ `);
1471
1544
  await db.execute(sql`
1472
1545
  CREATE INDEX IF NOT EXISTS "idx_embedded_executions_workflow_id"
1473
1546
  ON "workflow"."embedded_executions" ("workflow_id")
@@ -1480,6 +1553,10 @@ export class EmbeddedWorkflowService extends Service {
1480
1553
  CREATE INDEX IF NOT EXISTS "idx_embedded_executions_started_at"
1481
1554
  ON "workflow"."embedded_executions" ("started_at")
1482
1555
  `);
1556
+ await db.execute(sql`
1557
+ CREATE INDEX IF NOT EXISTS "idx_embedded_executions_idempotency_key"
1558
+ ON "workflow"."embedded_executions" ("idempotency_key")
1559
+ `);
1483
1560
  await db.execute(sql`
1484
1561
  CREATE TABLE IF NOT EXISTS "workflow"."embedded_credentials" (
1485
1562
  "id" text PRIMARY KEY,
@@ -1540,6 +1617,7 @@ export class EmbeddedWorkflowService extends Service {
1540
1617
  this.assertRegisteredNodes(workflow);
1541
1618
  const existing = await this.getStoredWorkflow(id);
1542
1619
  const db = this.getDb();
1620
+ await this.captureWorkflowRevision(id, existing, 'update');
1543
1621
  const updatedAt = nowIso();
1544
1622
  const versionId = randomUUID();
1545
1623
  const stored = normalizeWorkflowPayload(workflow, id, existing.workflow.active ?? false);
@@ -1598,6 +1676,7 @@ export class EmbeddedWorkflowService extends Service {
1598
1676
  this.clearSchedules(id);
1599
1677
  const existing = await this.getStoredWorkflow(id);
1600
1678
  const db = this.getDb();
1679
+ await this.captureWorkflowRevision(id, existing, 'delete');
1601
1680
  await db.delete(embeddedWorkflows).where(eq(embeddedWorkflows.id, id));
1602
1681
  if (!existing) {
1603
1682
  throw new WorkflowApiError(`Workflow not found: ${id}`, 404);
@@ -1608,6 +1687,7 @@ export class EmbeddedWorkflowService extends Service {
1608
1687
  const entry = await this.getStoredWorkflow(id);
1609
1688
  this.assertHostSupports(entry.workflow);
1610
1689
  const db = this.getDb();
1690
+ await this.captureWorkflowRevision(id, entry, 'activate');
1611
1691
  entry.workflow.active = true;
1612
1692
  entry.updatedAt = nowIso();
1613
1693
  entry.versionId = randomUUID();
@@ -1627,6 +1707,7 @@ export class EmbeddedWorkflowService extends Service {
1627
1707
  async deactivateWorkflow(id: string): Promise<WorkflowDefinitionResponse> {
1628
1708
  const entry = await this.getStoredWorkflow(id);
1629
1709
  const db = this.getDb();
1710
+ await this.captureWorkflowRevision(id, entry, 'deactivate');
1630
1711
  entry.workflow.active = false;
1631
1712
  entry.updatedAt = nowIso();
1632
1713
  entry.versionId = randomUUID();
@@ -1653,6 +1734,7 @@ export class EmbeddedWorkflowService extends Service {
1653
1734
  if (!tag) throw new WorkflowApiError(`Tag not found: ${tagId}`, 404);
1654
1735
  tags.push({ id: tag.id, name: tag.name, createdAt: tag.createdAt, updatedAt: tag.updatedAt });
1655
1736
  }
1737
+ await this.captureWorkflowRevision(id, entry, 'tags');
1656
1738
  entry.workflow.tags = cloneJson(tags);
1657
1739
  entry.updatedAt = nowIso();
1658
1740
  entry.versionId = randomUUID();
@@ -1667,6 +1749,81 @@ export class EmbeddedWorkflowService extends Service {
1667
1749
  return cloneJson(tags);
1668
1750
  }
1669
1751
 
1752
+ async listWorkflowRevisions(
1753
+ workflowId: string,
1754
+ limit = 20
1755
+ ): Promise<{ data: WorkflowRevision[] }> {
1756
+ await this.ensureSchema();
1757
+ const db = this.getDb();
1758
+ const rows = await db
1759
+ .select()
1760
+ .from(workflowRevisions)
1761
+ .where(eq(workflowRevisions.workflowId, workflowId))
1762
+ .orderBy(desc(workflowRevisions.capturedAt))
1763
+ .limit(Math.min(Math.max(1, limit), 50));
1764
+ return {
1765
+ data: rows.map((row) =>
1766
+ revisionFromRow({
1767
+ id: row.id,
1768
+ workflowId: row.workflowId,
1769
+ workflow: row.workflow,
1770
+ createdAt: row.createdAt,
1771
+ updatedAt: row.updatedAt,
1772
+ versionId: row.versionId,
1773
+ capturedAt: row.capturedAt,
1774
+ operation: row.operation as WorkflowRevisionOperation,
1775
+ })
1776
+ ),
1777
+ };
1778
+ }
1779
+
1780
+ async restoreWorkflowRevision(
1781
+ workflowId: string,
1782
+ versionId: string
1783
+ ): Promise<WorkflowDefinitionResponse> {
1784
+ await this.ensureSchema();
1785
+ const db = this.getDb();
1786
+ const revisionRows = await db
1787
+ .select()
1788
+ .from(workflowRevisions)
1789
+ .where(
1790
+ and(
1791
+ eq(workflowRevisions.workflowId, workflowId),
1792
+ eq(workflowRevisions.versionId, versionId)
1793
+ )
1794
+ )
1795
+ .limit(1);
1796
+ const revision = revisionRows[0];
1797
+ if (!revision) {
1798
+ throw new WorkflowApiError(`Workflow revision not found: ${workflowId}/${versionId}`, 404);
1799
+ }
1800
+
1801
+ const current = await this.getStoredWorkflow(workflowId);
1802
+ const restored = normalizeWorkflowPayload(revision.workflow, workflowId, revision.active);
1803
+ this.assertRegisteredNodes(restored);
1804
+ this.assertHostSupports(restored);
1805
+ await this.captureWorkflowRevision(workflowId, current, 'restore');
1806
+
1807
+ const updatedAt = nowIso();
1808
+ const nextVersionId = randomUUID();
1809
+ await db
1810
+ .update(embeddedWorkflows)
1811
+ .set({
1812
+ name: restored.name,
1813
+ active: restored.active ?? false,
1814
+ workflow: restored,
1815
+ updatedAt,
1816
+ versionId: nextVersionId,
1817
+ })
1818
+ .where(eq(embeddedWorkflows.id, workflowId));
1819
+ if (restored.active) {
1820
+ await this.armSchedules(workflowId);
1821
+ } else {
1822
+ await this.clearSchedules(workflowId);
1823
+ }
1824
+ return responseFromWorkflow(restored, current.createdAt, updatedAt, nextVersionId);
1825
+ }
1826
+
1670
1827
  async createCredential(credential: {
1671
1828
  name: string;
1672
1829
  type: string;
@@ -1778,7 +1935,39 @@ export class EmbeddedWorkflowService extends Service {
1778
1935
 
1779
1936
  async executeWorkflow(id: string, options: ExecuteOptions = {}): Promise<WorkflowExecution> {
1780
1937
  const entry = await this.getStoredWorkflow(id);
1781
- return this.runWorkflow(entry.workflow, options.mode ?? 'manual');
1938
+ return this.runWorkflow(
1939
+ entry.workflow,
1940
+ options.mode ?? 'manual',
1941
+ options.triggerData,
1942
+ options.idempotencyKey,
1943
+ options.throwOnError ?? true
1944
+ );
1945
+ }
1946
+
1947
+ /**
1948
+ * Look up the most recent execution row tagged with this idempotency
1949
+ * key for the given workflow. Returns null when none exists. The
1950
+ * dispatch layer uses this to dedup back-to-back schedule fires that
1951
+ * share a minute bucket — see WorkflowDispatchService.execute.
1952
+ */
1953
+ async findExecutionByIdempotencyKey(
1954
+ workflowId: string,
1955
+ idempotencyKey: string
1956
+ ): Promise<WorkflowExecution | null> {
1957
+ await this.ensureSchema();
1958
+ const rows = await this.getDb()
1959
+ .select()
1960
+ .from(embeddedExecutions)
1961
+ .where(
1962
+ and(
1963
+ eq(embeddedExecutions.workflowId, workflowId),
1964
+ eq(embeddedExecutions.idempotencyKey, idempotencyKey)
1965
+ )
1966
+ )
1967
+ .orderBy(desc(embeddedExecutions.startedAt))
1968
+ .limit(1);
1969
+ const row = rows[0];
1970
+ return row ? cloneJson(row.execution) : null;
1782
1971
  }
1783
1972
 
1784
1973
  async executeWebhook(
@@ -1841,6 +2030,29 @@ export class EmbeddedWorkflowService extends Service {
1841
2030
  return executions;
1842
2031
  }
1843
2032
 
2033
+ private async captureWorkflowRevision(
2034
+ workflowId: string,
2035
+ entry: StoredWorkflowRow,
2036
+ operation: WorkflowRevisionOperation
2037
+ ): Promise<void> {
2038
+ await this.ensureSchema();
2039
+ await this.getDb()
2040
+ .insert(workflowRevisions)
2041
+ .values({
2042
+ id: randomUUID(),
2043
+ workflowId,
2044
+ versionId: entry.versionId,
2045
+ name: entry.workflow.name,
2046
+ active: entry.workflow.active === true,
2047
+ workflow: cloneJson(entry.workflow),
2048
+ createdAt: entry.createdAt,
2049
+ updatedAt: entry.updatedAt,
2050
+ capturedAt: nowIso(),
2051
+ operation,
2052
+ })
2053
+ .onConflictDoNothing();
2054
+ }
2055
+
1844
2056
  private async getStoredWorkflow(id: string): Promise<StoredWorkflowRow> {
1845
2057
  await this.ensureSchema();
1846
2058
  const rows = await this.getDb()
@@ -1921,9 +2133,14 @@ export class EmbeddedWorkflowService extends Service {
1921
2133
  /** Re-create core Tasks for every active workflow on service start.
1922
2134
  * Tasks themselves persist across restart; this is a reconcile step that
1923
2135
  * ensures workflows whose schedule changed (or whose tasks were never
1924
- * created in the first place) end up correctly scheduled. */
2136
+ * created in the first place) end up correctly scheduled.
2137
+ *
2138
+ * Also performs a one-shot migration: any pre-existing legacy
2139
+ * `workflow.run` / `workflow.webhook` task rows are deleted so the new
2140
+ * `TRIGGER_DISPATCH` path is the single source of scheduled runs. */
1925
2141
  private async rehydrateSchedules(): Promise<void> {
1926
2142
  await this.ensureSchema();
2143
+ await this.deleteLegacyScheduleTasks();
1927
2144
  const rows = await this.getDb()
1928
2145
  .select()
1929
2146
  .from(embeddedWorkflows)
@@ -1933,9 +2150,73 @@ export class EmbeddedWorkflowService extends Service {
1933
2150
  }
1934
2151
  }
1935
2152
 
1936
- /** Create one recurring core Task per scheduleTrigger node on the workflow.
1937
- * Idempotent: existing tasks for this workflow are removed first so the
1938
- * task set always reflects the current workflow definition. */
2153
+ /** Remove legacy `workflow.run` / `workflow.webhook` Tasks left behind
2154
+ * by earlier service versions. Returns the count so callers (and the
2155
+ * migration log) can verify the cleanup. */
2156
+ private async deleteLegacyScheduleTasks(): Promise<number> {
2157
+ if (
2158
+ typeof this.runtime.getTasks !== 'function' ||
2159
+ typeof this.runtime.deleteTask !== 'function'
2160
+ ) {
2161
+ return 0;
2162
+ }
2163
+ const tasks = await this.runtime.getTasks({
2164
+ tags: [WORKFLOW_TASK_TAG],
2165
+ agentIds: [this.runtime.agentId],
2166
+ });
2167
+ if (!tasks.length) return 0;
2168
+ let removed = 0;
2169
+ for (const task of tasks) {
2170
+ if (!task.id) continue;
2171
+ if (
2172
+ task.name === LEGACY_WORKFLOW_RUN_TASK_NAME ||
2173
+ task.name === LEGACY_WORKFLOW_WEBHOOK_TASK_NAME
2174
+ ) {
2175
+ await this.runtime.deleteTask(task.id);
2176
+ removed += 1;
2177
+ }
2178
+ }
2179
+ if (removed > 0) {
2180
+ logger.info(
2181
+ { src: 'plugin:workflow:embedded', removed },
2182
+ `Removed ${removed} legacy workflow task row(s); schedules will re-arm via TRIGGER_DISPATCH`
2183
+ );
2184
+ }
2185
+ return removed;
2186
+ }
2187
+
2188
+ /** Build a `TriggerConfig` for a workflow schedule node. The resulting
2189
+ * config is what the agent's `executeTriggerTask` reads off the task
2190
+ * metadata when the scheduler fires. */
2191
+ private buildScheduleTrigger(
2192
+ workflowId: string,
2193
+ workflowName: string,
2194
+ intervalMs: number
2195
+ ): TriggerConfig {
2196
+ const triggerId = stringToUuid(`${workflowId}:schedule:${randomUUID()}`);
2197
+ return {
2198
+ version: TRIGGER_SCHEMA_VERSION,
2199
+ triggerId,
2200
+ displayName: `Scheduled workflow run: ${workflowName}`,
2201
+ instructions: `Run workflow ${workflowName}`,
2202
+ triggerType: 'interval',
2203
+ enabled: true,
2204
+ wakeMode: 'inject_now',
2205
+ createdBy: 'workflow.schedule',
2206
+ intervalMs,
2207
+ runCount: 0,
2208
+ kind: 'workflow',
2209
+ workflowId,
2210
+ workflowName,
2211
+ };
2212
+ }
2213
+
2214
+ /** Create one recurring `TRIGGER_DISPATCH` Task per scheduleTrigger
2215
+ * node on the workflow. Idempotent: existing tasks for this workflow
2216
+ * are removed first so the task set always reflects the current
2217
+ * workflow definition. Each task carries an idempotency key derived
2218
+ * from `(workflowId, nextRunAt-minute-bucket)` so that simultaneous
2219
+ * fires within the same minute deduplicate at dispatch. */
1939
2220
  private async armSchedules(workflowId: string): Promise<void> {
1940
2221
  await this.clearSchedules(workflowId);
1941
2222
  if (typeof this.runtime.createTask !== 'function') return;
@@ -1945,19 +2226,30 @@ export class EmbeddedWorkflowService extends Service {
1945
2226
  );
1946
2227
  if (scheduleNodes.length === 0) return;
1947
2228
 
2229
+ const nowMs = Date.now();
1948
2230
  for (const node of scheduleNodes) {
1949
2231
  const intervalMs = resolveScheduleIntervalMs(node.parameters);
2232
+ const trigger = this.buildScheduleTrigger(workflowId, entry.workflow.name, intervalMs);
2233
+ const nextRunAtMs = nowMs + intervalMs;
2234
+ const triggerWithSchedule: TriggerConfig = {
2235
+ ...trigger,
2236
+ nextRunAtMs,
2237
+ };
2238
+ const idempotencyKey = buildScheduleIdempotencyKey(workflowId, nextRunAtMs);
1950
2239
  await this.runtime.createTask({
1951
- name: WORKFLOW_RUN_TASK_WORKER_NAME,
1952
- description: `Scheduled workflow run: ${entry.workflow.name}`,
1953
- tags: ['queue', 'repeat', WORKFLOW_TASK_TAG],
2240
+ name: TRIGGER_TASK_NAME,
2241
+ description: trigger.displayName,
2242
+ tags: [...TRIGGER_TASK_TAGS, WORKFLOW_TASK_TAG],
1954
2243
  metadata: {
2244
+ blocking: true,
2245
+ updatedAt: nowMs,
2246
+ updateInterval: intervalMs,
2247
+ baseInterval: intervalMs,
1955
2248
  kind: WORKFLOW_TASK_KIND,
1956
2249
  workflowId,
1957
2250
  scheduleNodeId: node.id,
1958
- updateInterval: intervalMs,
1959
- baseInterval: intervalMs,
1960
- updatedAt: Date.now(),
2251
+ idempotencyKey,
2252
+ trigger: triggerWithSchedule,
1961
2253
  },
1962
2254
  });
1963
2255
  }
@@ -1970,7 +2262,7 @@ export class EmbeddedWorkflowService extends Service {
1970
2262
  tags: [WORKFLOW_TASK_TAG],
1971
2263
  agentIds: [this.runtime.agentId],
1972
2264
  });
1973
- if (!tasks?.length) return;
2265
+ if (!tasks.length) return;
1974
2266
  for (const task of tasks) {
1975
2267
  if (
1976
2268
  task.id &&
@@ -1981,8 +2273,12 @@ export class EmbeddedWorkflowService extends Service {
1981
2273
  }
1982
2274
  }
1983
2275
 
1984
- private async saveExecution(execution: WorkflowExecution): Promise<void> {
2276
+ private async saveExecution(
2277
+ execution: WorkflowExecution,
2278
+ idempotencyKey?: string
2279
+ ): Promise<void> {
1985
2280
  await this.ensureSchema();
2281
+ const key = idempotencyKey ?? null;
1986
2282
  await this.getDb()
1987
2283
  .insert(embeddedExecutions)
1988
2284
  .values({
@@ -1994,6 +2290,7 @@ export class EmbeddedWorkflowService extends Service {
1994
2290
  startedAt: execution.startedAt,
1995
2291
  stoppedAt: execution.stoppedAt ?? null,
1996
2292
  execution: cloneJson(execution),
2293
+ idempotencyKey: key,
1997
2294
  })
1998
2295
  .onConflictDoUpdate({
1999
2296
  target: embeddedExecutions.id,
@@ -2005,6 +2302,7 @@ export class EmbeddedWorkflowService extends Service {
2005
2302
  startedAt: execution.startedAt,
2006
2303
  stoppedAt: execution.stoppedAt ?? null,
2007
2304
  execution: cloneJson(execution),
2305
+ idempotencyKey: key,
2008
2306
  },
2009
2307
  });
2010
2308
  }
@@ -2013,16 +2311,16 @@ export class EmbeddedWorkflowService extends Service {
2013
2311
  workflowData: WorkflowDefinition
2014
2312
  ): Map<string, IncomingConnection[]> {
2015
2313
  const incoming = new Map<string, IncomingConnection[]>();
2016
- for (const [source, outputsByType] of Object.entries(workflowData.connections ?? {})) {
2017
- const mainOutputs = outputsByType.main ?? [];
2314
+ for (const [source, outputsByType] of Object.entries(workflowData.connections)) {
2315
+ const mainOutputs = outputsByType.main;
2018
2316
  mainOutputs.forEach((connections, sourceOutputIndex) => {
2019
- for (const connection of connections ?? []) {
2317
+ for (const connection of connections) {
2020
2318
  if (connection.type !== 'main') continue;
2021
2319
  const destination = incoming.get(connection.node) ?? [];
2022
2320
  destination.push({
2023
2321
  source,
2024
2322
  sourceOutputIndex,
2025
- destinationInputIndex: connection.index ?? 0,
2323
+ destinationInputIndex: connection.index,
2026
2324
  });
2027
2325
  incoming.set(connection.node, destination);
2028
2326
  }
@@ -2072,21 +2370,51 @@ export class EmbeddedWorkflowService extends Service {
2072
2370
  return start;
2073
2371
  }
2074
2372
 
2075
- private collectInputData(
2076
- nodeName: string,
2077
- incoming: Map<string, IncomingConnection[]>,
2078
- nodeOutputs: Map<string, INodeExecutionData[][]>
2079
- ): INodeExecutionData[][] {
2080
- const inputData: INodeExecutionData[][] = [];
2081
- for (const connection of incoming.get(nodeName) ?? []) {
2082
- const sourceOutputs = nodeOutputs.get(connection.source) ?? [];
2083
- const sourceItems = sourceOutputs[connection.sourceOutputIndex] ?? [];
2084
- inputData[connection.destinationInputIndex] = [
2085
- ...(inputData[connection.destinationInputIndex] ?? []),
2086
- ...sourceItems,
2087
- ];
2373
+ private resolveExecutionPlan(
2374
+ workflowData: WorkflowDefinition,
2375
+ mode: WorkflowExecuteMode
2376
+ ): SmithersExecutionPlan {
2377
+ const enabledNodes = workflowData.nodes.filter((node) => !node.disabled);
2378
+ const nodeByName = new Map(enabledNodes.map((node) => [node.name, node]));
2379
+ const incoming = this.buildIncomingConnections(workflowData);
2380
+ const startNodes = this.resolveStartNodes(workflowData, mode, incoming);
2381
+ const orderedNodes: WorkflowNode[] = [];
2382
+ const executed = new Set<string>();
2383
+
2384
+ while (executed.size < enabledNodes.length) {
2385
+ let progressed = false;
2386
+
2387
+ for (const node of enabledNodes) {
2388
+ if (executed.has(node.name)) continue;
2389
+
2390
+ const incomingConnections =
2391
+ incoming.get(node.name)?.filter((connection) => nodeByName.has(connection.source)) ?? [];
2392
+ const isStartNode = startNodes.has(node.name);
2393
+ const dependenciesComplete = incomingConnections.every((connection) =>
2394
+ executed.has(connection.source)
2395
+ );
2396
+
2397
+ if (!isStartNode && !dependenciesComplete) continue;
2398
+
2399
+ orderedNodes.push(node);
2400
+ executed.add(node.name);
2401
+ progressed = true;
2402
+ }
2403
+
2404
+ if (!progressed) {
2405
+ const unresolved = enabledNodes
2406
+ .filter((node) => !executed.has(node.name))
2407
+ .map((node) => node.name)
2408
+ .join(', ');
2409
+ throw new Error(`Unable to resolve workflow execution order for node(s): ${unresolved}`);
2410
+ }
2088
2411
  }
2089
- return inputData.length > 0 ? inputData : [[]];
2412
+
2413
+ return {
2414
+ enabledNodes: orderedNodes,
2415
+ startNodes: [...startNodes],
2416
+ incoming: Object.fromEntries(incoming.entries()),
2417
+ };
2090
2418
  }
2091
2419
 
2092
2420
  private async executeNode(
@@ -2107,7 +2435,10 @@ export class EmbeddedWorkflowService extends Service {
2107
2435
 
2108
2436
  private async runWorkflow(
2109
2437
  workflowData: WorkflowDefinition,
2110
- mode: WorkflowExecuteMode
2438
+ mode: WorkflowExecuteMode,
2439
+ triggerData?: Record<string, unknown>,
2440
+ idempotencyKey?: string,
2441
+ throwOnError = true
2111
2442
  ): Promise<WorkflowExecution> {
2112
2443
  const executionId = randomUUID();
2113
2444
  const startedAt = new Date();
@@ -2118,88 +2449,29 @@ export class EmbeddedWorkflowService extends Service {
2118
2449
  startedAt: startedAt.toISOString(),
2119
2450
  workflowId: workflowData.id ?? '',
2120
2451
  status: 'running',
2452
+ ...(triggerData || idempotencyKey
2453
+ ? {
2454
+ customData: {
2455
+ ...(triggerData ? { triggerData } : {}),
2456
+ ...(idempotencyKey ? { idempotencyKey } : {}),
2457
+ },
2458
+ }
2459
+ : {}),
2121
2460
  };
2122
- await this.saveExecution(pending);
2461
+ await this.saveExecution(pending, idempotencyKey);
2123
2462
 
2124
2463
  try {
2125
- const enabledNodes = workflowData.nodes.filter((node) => !node.disabled);
2126
- const nodeByName = new Map(enabledNodes.map((node) => [node.name, node]));
2127
- const incoming = this.buildIncomingConnections(workflowData);
2128
- const startNodes = this.resolveStartNodes(workflowData, mode, incoming);
2129
- const nodeOutputs = new Map<string, INodeExecutionData[][]>();
2130
- const executed = new Set<string>();
2131
- const runData: Record<string, unknown[]> = {};
2132
- let lastNodeExecuted: string | undefined;
2133
-
2134
- while (executed.size < enabledNodes.length) {
2135
- let progressed = false;
2136
-
2137
- for (const node of enabledNodes) {
2138
- if (executed.has(node.name)) continue;
2139
-
2140
- const incomingConnections =
2141
- incoming.get(node.name)?.filter((connection) => nodeByName.has(connection.source)) ??
2142
- [];
2143
- const isStartNode = startNodes.has(node.name);
2144
- const dependenciesComplete = incomingConnections.every((connection) =>
2145
- executed.has(connection.source)
2146
- );
2147
-
2148
- if (!isStartNode && !dependenciesComplete) continue;
2149
-
2150
- const inputData =
2151
- isStartNode && incomingConnections.length === 0
2152
- ? [[]]
2153
- : this.collectInputData(node.name, incoming, nodeOutputs);
2154
- const hasInputItems = inputData.some((items) => items.length > 0);
2155
- const started = Date.now();
2156
-
2157
- const outputData =
2158
- !isStartNode && incomingConnections.length > 0 && !hasInputItems
2159
- ? [[]]
2160
- : await this.executeNode(node, inputData, executionId);
2161
-
2162
- nodeOutputs.set(node.name, outputData);
2163
- runData[node.name] = [
2164
- {
2165
- startTime: started,
2166
- executionTime: Date.now() - started,
2167
- data: { main: cloneJson(outputData) },
2168
- source: incomingConnections.map((connection) => ({
2169
- previousNode: connection.source,
2170
- previousNodeOutput: connection.sourceOutputIndex,
2171
- previousNodeRun: 0,
2172
- })),
2173
- },
2174
- ];
2175
- executed.add(node.name);
2176
- lastNodeExecuted = node.name;
2177
- progressed = true;
2178
- }
2179
-
2180
- if (!progressed) {
2181
- const unresolved = enabledNodes
2182
- .filter((node) => !executed.has(node.name))
2183
- .map((node) => node.name)
2184
- .join(', ');
2185
- throw new Error(`Unable to resolve workflow execution order for node(s): ${unresolved}`);
2186
- }
2187
- }
2188
-
2189
- const stoppedAt = new Date();
2190
- const execution: WorkflowExecution = {
2191
- ...pending,
2192
- finished: true,
2193
- status: 'success',
2194
- stoppedAt: stoppedAt.toISOString(),
2195
- data: {
2196
- resultData: {
2197
- runData,
2198
- lastNodeExecuted,
2199
- },
2200
- },
2201
- };
2202
- await this.saveExecution(execution);
2464
+ const plan = this.resolveExecutionPlan(workflowData, mode);
2465
+ const execution = await runWorkflowWithSmithers({
2466
+ workflow: workflowData,
2467
+ executionId,
2468
+ pending,
2469
+ mode,
2470
+ triggerData,
2471
+ plan,
2472
+ runNode: (node, inputData) => this.executeNode(node, inputData, executionId),
2473
+ });
2474
+ await this.saveExecution(execution, idempotencyKey);
2203
2475
  return cloneJson(execution);
2204
2476
  } catch (error) {
2205
2477
  const stoppedAt = new Date();
@@ -2217,7 +2489,10 @@ export class EmbeddedWorkflowService extends Service {
2217
2489
  },
2218
2490
  },
2219
2491
  };
2220
- await this.saveExecution(execution);
2492
+ await this.saveExecution(execution, idempotencyKey);
2493
+ if (!throwOnError) {
2494
+ return cloneJson(execution);
2495
+ }
2221
2496
  throw error;
2222
2497
  }
2223
2498
  }