@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,22 +1,31 @@
1
1
  import { createHash, randomUUID } from 'node:crypto';
2
- import { logger, Service } from '@elizaos/core';
2
+ import { logger, Service, stringToUuid, TRIGGER_SCHEMA_VERSION, } from '@elizaos/core';
3
3
  import { and, desc, eq, sql } from 'drizzle-orm';
4
- import { embeddedCredentials, embeddedExecutions, embeddedTags, embeddedWorkflows, } from '../db/schema';
4
+ import { embeddedCredentials, embeddedExecutions, embeddedTags, embeddedWorkflows, workflowRevisions, } from '../db/schema';
5
5
  import { WorkflowApiError } from '../types/index';
6
6
  import { detectHostCapabilities } from '../utils/host-capabilities';
7
+ import { runWorkflowWithSmithers } from './smithers-runtime';
7
8
  export const EMBEDDED_WORKFLOW_SERVICE_TYPE = 'embedded_workflow_service';
8
- /** TaskWorker name for scheduled workflow runs. Tasks created with this name
9
- * carry metadata.workflowId + metadata.kind = 'workflow' and get fired by
10
- * the core TaskService on the configured updateInterval. */
11
- export const WORKFLOW_RUN_TASK_WORKER_NAME = 'workflow.run';
12
- /** TaskWorker name for one-shot webhook-triggered workflow runs. A future
13
- * webhook trigger provider creates a one-shot Task pointing at this worker;
14
- * payload travels in metadata.payload. */
15
- export const WORKFLOW_WEBHOOK_TASK_WORKER_NAME = 'workflow.webhook';
9
+ /**
10
+ * Task name + tag contract for scheduled workflow runs. Mirrored from
11
+ * `packages/agent/src/triggers/runtime.ts` because plugin-workflow can't
12
+ * import @elizaos/agent (would create a dep cycle). The agent's
13
+ * `registerTriggerTaskWorker` consumes tasks with this name.
14
+ */
15
+ export const TRIGGER_TASK_NAME = 'TRIGGER_DISPATCH';
16
+ export const TRIGGER_TASK_TAGS = ['queue', 'repeat', 'trigger'];
16
17
  /** Discriminator on TaskMetadata so the UI can route workflow tasks. */
17
18
  export const WORKFLOW_TASK_KIND = 'workflow';
18
19
  /** Stable tag used on every workflow-backed Task so we can list+delete them. */
19
20
  const WORKFLOW_TASK_TAG = 'workflow';
21
+ /**
22
+ * Legacy task names retained only for rehydration cleanup. `workflow.run`
23
+ * was the prior scheduled-dispatch path; it bypassed `executeTriggerTask`
24
+ * and accumulated no run history. `workflow.webhook` had no producer and
25
+ * was dead from the start. Both are migrated/removed on service start.
26
+ */
27
+ const LEGACY_WORKFLOW_RUN_TASK_NAME = 'workflow.run';
28
+ const LEGACY_WORKFLOW_WEBHOOK_TASK_NAME = 'workflow.webhook';
20
29
  const EMBEDDED_HOST = 'embedded://local';
21
30
  const DEFAULT_SCHEDULE_INTERVAL_MS = 60_000;
22
31
  let loadedQuickJs = null;
@@ -53,12 +62,36 @@ function responseFromWorkflow(workflow, createdAt, updatedAt, versionId) {
53
62
  versionId,
54
63
  };
55
64
  }
65
+ function revisionFromRow(row) {
66
+ return {
67
+ id: row.id,
68
+ workflowId: row.workflowId,
69
+ versionId: row.versionId,
70
+ name: row.workflow.name,
71
+ active: row.workflow.active === true,
72
+ workflow: cloneJson(row.workflow),
73
+ createdAt: row.createdAt,
74
+ updatedAt: row.updatedAt,
75
+ capturedAt: row.capturedAt,
76
+ operation: row.operation,
77
+ };
78
+ }
56
79
  function readString(value, fallback) {
57
80
  return typeof value === 'string' && value.length > 0 ? value : fallback;
58
81
  }
59
82
  function readNumber(value, fallback) {
60
83
  return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
61
84
  }
85
+ /**
86
+ * Build the per-dispatch idempotency key used to dedup back-to-back
87
+ * scheduled fires for the same workflow within the same minute. Shared
88
+ * by `armSchedules` (which writes it into the task metadata) and
89
+ * `WorkflowDispatchService.execute` (which looks it up before running).
90
+ */
91
+ export function buildScheduleIdempotencyKey(workflowId, nextRunAtMs) {
92
+ const minuteBucket = Math.floor(nextRunAtMs / 60_000);
93
+ return `${workflowId}:${minuteBucket}`;
94
+ }
62
95
  function resolveScheduleIntervalMs(parameters) {
63
96
  const explicitMs = readNumber(parameters.intervalMs, NaN);
64
97
  if (Number.isFinite(explicitMs) && explicitMs > 0)
@@ -726,8 +759,8 @@ function createMergeNode() {
726
759
  ],
727
760
  },
728
761
  async execute() {
729
- const first = this.getInputData(0) ?? [];
730
- const second = this.getInputData(1) ?? [];
762
+ const first = this.getInputData(0);
763
+ const second = this.getInputData(1);
731
764
  return [[...first, ...second]];
732
765
  },
733
766
  };
@@ -807,7 +840,7 @@ function createDateTimeNode() {
807
840
  const now = new Date().toISOString();
808
841
  return [
809
842
  inputItems.map((item, index) => ({
810
- json: { ...(item.json ?? {}), [fieldName]: now },
843
+ json: { ...item.json, [fieldName]: now },
811
844
  pairedItem: item.pairedItem ?? { item: index },
812
845
  })),
813
846
  ];
@@ -841,7 +874,7 @@ function createCryptoNode() {
841
874
  const source = raw === '' || typeof raw === 'undefined' ? JSON.stringify(item.json) : String(raw);
842
875
  return {
843
876
  json: {
844
- ...(item.json ?? {}),
877
+ ...item.json,
845
878
  [fieldName]: createHash(algorithm).update(source).digest('hex'),
846
879
  },
847
880
  pairedItem: item.pairedItem ?? { item: index },
@@ -902,6 +935,17 @@ async function runQuickJsCode(jsCode, inputItems) {
902
935
  memoryLimitBytes: 32 * 1024 * 1024,
903
936
  });
904
937
  }
938
+ /**
939
+ * Evaluate a snippet of JavaScript in the same isolated QuickJS sandbox the
940
+ * Code node uses (5s deadline, 32 MiB cap, no host/network/fs access). Optional
941
+ * `inputJson` is exposed to the snippet as `$json` / `item.json` / `$input[0]`.
942
+ * The snippet body runs inside an IIFE, so `return <value>` yields the result.
943
+ * Public entry point for the EVAL_CODE action (#8914).
944
+ */
945
+ export async function evalQuickJsCode(jsCode, inputJson) {
946
+ const items = inputJson === undefined ? [] : [{ json: (inputJson ?? {}) }];
947
+ return runQuickJsCode(jsCode, items);
948
+ }
905
949
  function resolveAutonomyService(runtime) {
906
950
  const svc = runtime.getService('AUTONOMY') ??
907
951
  runtime.getService('autonomy');
@@ -969,12 +1013,12 @@ function createRespondToEventNode() {
969
1013
  }
970
1014
  const autonomyService = resolveAutonomyService(runtime);
971
1015
  if (!autonomyService) {
972
- runtime.logger?.warn?.({ src: 'plugin:workflow:respondToEvent', nodeName: node.name, executionId }, '[respondToEvent] Autonomy service not registered — skipping injection');
1016
+ runtime.logger.warn({ src: 'plugin:workflow:respondToEvent', nodeName: node.name, executionId }, '[respondToEvent] Autonomy service not registered — skipping injection');
973
1017
  return failure('autonomy_service_unavailable');
974
1018
  }
975
1019
  const roomId = resolveAutonomyRoomId(autonomyService);
976
1020
  if (!roomId) {
977
- runtime.logger?.warn?.({ src: 'plugin:workflow:respondToEvent', nodeName: node.name, executionId }, '[respondToEvent] No autonomy room resolvable — skipping injection');
1021
+ runtime.logger.warn({ src: 'plugin:workflow:respondToEvent', nodeName: node.name, executionId }, '[respondToEvent] No autonomy room resolvable — skipping injection');
978
1022
  return failure('no_autonomy_room');
979
1023
  }
980
1024
  const event = extractEventFromInputItems(inputItems);
@@ -1118,7 +1162,6 @@ export class EmbeddedWorkflowService extends Service {
1118
1162
  static async start(runtime) {
1119
1163
  const service = new EmbeddedWorkflowService(runtime);
1120
1164
  logger.info({ src: 'plugin:workflow:embedded' }, 'Embedded workflow service registered (lazy runtime load)');
1121
- service.registerTaskWorkers();
1122
1165
  if (runtime.db) {
1123
1166
  await service.ensureSchema();
1124
1167
  await service.rehydrateSchedules();
@@ -1129,41 +1172,6 @@ export class EmbeddedWorkflowService extends Service {
1129
1172
  // Scheduling lives in core's TaskService. Tasks persist across restart;
1130
1173
  // there is nothing in-process to tear down here.
1131
1174
  }
1132
- /** Register the workflow.run + workflow.webhook task workers with the
1133
- * runtime's TaskService. Idempotent — safe to call once per service start. */
1134
- registerTaskWorkers() {
1135
- if (typeof this.runtime.registerTaskWorker !== 'function')
1136
- return;
1137
- if (!this.runtime.getTaskWorker?.(WORKFLOW_RUN_TASK_WORKER_NAME)) {
1138
- this.runtime.registerTaskWorker({
1139
- name: WORKFLOW_RUN_TASK_WORKER_NAME,
1140
- execute: async (_rt, _opts, task) => {
1141
- const workflowId = typeof task.metadata?.workflowId === 'string' ? task.metadata.workflowId : null;
1142
- if (!workflowId) {
1143
- throw new Error(`${WORKFLOW_RUN_TASK_WORKER_NAME} task ${task.id ?? '?'} missing metadata.workflowId`);
1144
- }
1145
- await this.executeWorkflow(workflowId, { mode: 'trigger' });
1146
- return undefined;
1147
- },
1148
- });
1149
- }
1150
- if (!this.runtime.getTaskWorker?.(WORKFLOW_WEBHOOK_TASK_WORKER_NAME)) {
1151
- this.runtime.registerTaskWorker({
1152
- name: WORKFLOW_WEBHOOK_TASK_WORKER_NAME,
1153
- execute: async (_rt, _opts, task) => {
1154
- const meta = task.metadata;
1155
- const path = typeof meta?.path === 'string' ? meta.path : null;
1156
- const method = typeof meta?.method === 'string' ? meta.method : 'POST';
1157
- const payload = isRecord(meta?.payload) ? meta.payload : {};
1158
- if (!path) {
1159
- throw new Error(`${WORKFLOW_WEBHOOK_TASK_WORKER_NAME} task ${task.id ?? '?'} missing metadata.path`);
1160
- }
1161
- await this.executeWebhook(path, payload, method);
1162
- return undefined;
1163
- },
1164
- });
1165
- }
1166
- }
1167
1175
  get host() {
1168
1176
  return EMBEDDED_HOST;
1169
1177
  }
@@ -1223,6 +1231,32 @@ export class EmbeddedWorkflowService extends Service {
1223
1231
  await db.execute(sql `
1224
1232
  CREATE INDEX IF NOT EXISTS "idx_embedded_workflows_updated_at"
1225
1233
  ON "workflow"."embedded_workflows" ("updated_at")
1234
+ `);
1235
+ await db.execute(sql `
1236
+ CREATE TABLE IF NOT EXISTS "workflow"."workflow_revisions" (
1237
+ "id" text PRIMARY KEY,
1238
+ "workflow_id" text NOT NULL,
1239
+ "version_id" text NOT NULL,
1240
+ "name" text NOT NULL,
1241
+ "active" boolean DEFAULT false NOT NULL,
1242
+ "workflow" jsonb NOT NULL,
1243
+ "created_at" text NOT NULL,
1244
+ "updated_at" text NOT NULL,
1245
+ "captured_at" text NOT NULL,
1246
+ "operation" text NOT NULL
1247
+ )
1248
+ `);
1249
+ await db.execute(sql `
1250
+ CREATE INDEX IF NOT EXISTS "idx_workflow_revisions_workflow_id"
1251
+ ON "workflow"."workflow_revisions" ("workflow_id")
1252
+ `);
1253
+ await db.execute(sql `
1254
+ CREATE UNIQUE INDEX IF NOT EXISTS "idx_workflow_revisions_workflow_version"
1255
+ ON "workflow"."workflow_revisions" ("workflow_id", "version_id")
1256
+ `);
1257
+ await db.execute(sql `
1258
+ CREATE INDEX IF NOT EXISTS "idx_workflow_revisions_captured_at"
1259
+ ON "workflow"."workflow_revisions" ("captured_at")
1226
1260
  `);
1227
1261
  await db.execute(sql `
1228
1262
  CREATE TABLE IF NOT EXISTS "workflow"."embedded_executions" (
@@ -1233,8 +1267,14 @@ export class EmbeddedWorkflowService extends Service {
1233
1267
  "finished" boolean DEFAULT false NOT NULL,
1234
1268
  "started_at" text NOT NULL,
1235
1269
  "stopped_at" text,
1236
- "execution" jsonb NOT NULL
1270
+ "execution" jsonb NOT NULL,
1271
+ "idempotency_key" text
1237
1272
  )
1273
+ `);
1274
+ // Online migration: add idempotency_key to pre-existing tables.
1275
+ await db.execute(sql `
1276
+ ALTER TABLE "workflow"."embedded_executions"
1277
+ ADD COLUMN IF NOT EXISTS "idempotency_key" text
1238
1278
  `);
1239
1279
  await db.execute(sql `
1240
1280
  CREATE INDEX IF NOT EXISTS "idx_embedded_executions_workflow_id"
@@ -1247,6 +1287,10 @@ export class EmbeddedWorkflowService extends Service {
1247
1287
  await db.execute(sql `
1248
1288
  CREATE INDEX IF NOT EXISTS "idx_embedded_executions_started_at"
1249
1289
  ON "workflow"."embedded_executions" ("started_at")
1290
+ `);
1291
+ await db.execute(sql `
1292
+ CREATE INDEX IF NOT EXISTS "idx_embedded_executions_idempotency_key"
1293
+ ON "workflow"."embedded_executions" ("idempotency_key")
1250
1294
  `);
1251
1295
  await db.execute(sql `
1252
1296
  CREATE TABLE IF NOT EXISTS "workflow"."embedded_credentials" (
@@ -1303,6 +1347,7 @@ export class EmbeddedWorkflowService extends Service {
1303
1347
  this.assertRegisteredNodes(workflow);
1304
1348
  const existing = await this.getStoredWorkflow(id);
1305
1349
  const db = this.getDb();
1350
+ await this.captureWorkflowRevision(id, existing, 'update');
1306
1351
  const updatedAt = nowIso();
1307
1352
  const versionId = randomUUID();
1308
1353
  const stored = normalizeWorkflowPayload(workflow, id, existing.workflow.active ?? false);
@@ -1353,6 +1398,7 @@ export class EmbeddedWorkflowService extends Service {
1353
1398
  this.clearSchedules(id);
1354
1399
  const existing = await this.getStoredWorkflow(id);
1355
1400
  const db = this.getDb();
1401
+ await this.captureWorkflowRevision(id, existing, 'delete');
1356
1402
  await db.delete(embeddedWorkflows).where(eq(embeddedWorkflows.id, id));
1357
1403
  if (!existing) {
1358
1404
  throw new WorkflowApiError(`Workflow not found: ${id}`, 404);
@@ -1362,6 +1408,7 @@ export class EmbeddedWorkflowService extends Service {
1362
1408
  const entry = await this.getStoredWorkflow(id);
1363
1409
  this.assertHostSupports(entry.workflow);
1364
1410
  const db = this.getDb();
1411
+ await this.captureWorkflowRevision(id, entry, 'activate');
1365
1412
  entry.workflow.active = true;
1366
1413
  entry.updatedAt = nowIso();
1367
1414
  entry.versionId = randomUUID();
@@ -1380,6 +1427,7 @@ export class EmbeddedWorkflowService extends Service {
1380
1427
  async deactivateWorkflow(id) {
1381
1428
  const entry = await this.getStoredWorkflow(id);
1382
1429
  const db = this.getDb();
1430
+ await this.captureWorkflowRevision(id, entry, 'deactivate');
1383
1431
  entry.workflow.active = false;
1384
1432
  entry.updatedAt = nowIso();
1385
1433
  entry.versionId = randomUUID();
@@ -1406,6 +1454,7 @@ export class EmbeddedWorkflowService extends Service {
1406
1454
  throw new WorkflowApiError(`Tag not found: ${tagId}`, 404);
1407
1455
  tags.push({ id: tag.id, name: tag.name, createdAt: tag.createdAt, updatedAt: tag.updatedAt });
1408
1456
  }
1457
+ await this.captureWorkflowRevision(id, entry, 'tags');
1409
1458
  entry.workflow.tags = cloneJson(tags);
1410
1459
  entry.updatedAt = nowIso();
1411
1460
  entry.versionId = randomUUID();
@@ -1419,6 +1468,65 @@ export class EmbeddedWorkflowService extends Service {
1419
1468
  .where(eq(embeddedWorkflows.id, id));
1420
1469
  return cloneJson(tags);
1421
1470
  }
1471
+ async listWorkflowRevisions(workflowId, limit = 20) {
1472
+ await this.ensureSchema();
1473
+ const db = this.getDb();
1474
+ const rows = await db
1475
+ .select()
1476
+ .from(workflowRevisions)
1477
+ .where(eq(workflowRevisions.workflowId, workflowId))
1478
+ .orderBy(desc(workflowRevisions.capturedAt))
1479
+ .limit(Math.min(Math.max(1, limit), 50));
1480
+ return {
1481
+ data: rows.map((row) => revisionFromRow({
1482
+ id: row.id,
1483
+ workflowId: row.workflowId,
1484
+ workflow: row.workflow,
1485
+ createdAt: row.createdAt,
1486
+ updatedAt: row.updatedAt,
1487
+ versionId: row.versionId,
1488
+ capturedAt: row.capturedAt,
1489
+ operation: row.operation,
1490
+ })),
1491
+ };
1492
+ }
1493
+ async restoreWorkflowRevision(workflowId, versionId) {
1494
+ await this.ensureSchema();
1495
+ const db = this.getDb();
1496
+ const revisionRows = await db
1497
+ .select()
1498
+ .from(workflowRevisions)
1499
+ .where(and(eq(workflowRevisions.workflowId, workflowId), eq(workflowRevisions.versionId, versionId)))
1500
+ .limit(1);
1501
+ const revision = revisionRows[0];
1502
+ if (!revision) {
1503
+ throw new WorkflowApiError(`Workflow revision not found: ${workflowId}/${versionId}`, 404);
1504
+ }
1505
+ const current = await this.getStoredWorkflow(workflowId);
1506
+ const restored = normalizeWorkflowPayload(revision.workflow, workflowId, revision.active);
1507
+ this.assertRegisteredNodes(restored);
1508
+ this.assertHostSupports(restored);
1509
+ await this.captureWorkflowRevision(workflowId, current, 'restore');
1510
+ const updatedAt = nowIso();
1511
+ const nextVersionId = randomUUID();
1512
+ await db
1513
+ .update(embeddedWorkflows)
1514
+ .set({
1515
+ name: restored.name,
1516
+ active: restored.active ?? false,
1517
+ workflow: restored,
1518
+ updatedAt,
1519
+ versionId: nextVersionId,
1520
+ })
1521
+ .where(eq(embeddedWorkflows.id, workflowId));
1522
+ if (restored.active) {
1523
+ await this.armSchedules(workflowId);
1524
+ }
1525
+ else {
1526
+ await this.clearSchedules(workflowId);
1527
+ }
1528
+ return responseFromWorkflow(restored, current.createdAt, updatedAt, nextVersionId);
1529
+ }
1422
1530
  async createCredential(credential) {
1423
1531
  await this.ensureSchema();
1424
1532
  const db = this.getDb();
@@ -1510,7 +1618,24 @@ export class EmbeddedWorkflowService extends Service {
1510
1618
  }
1511
1619
  async executeWorkflow(id, options = {}) {
1512
1620
  const entry = await this.getStoredWorkflow(id);
1513
- return this.runWorkflow(entry.workflow, options.mode ?? 'manual');
1621
+ return this.runWorkflow(entry.workflow, options.mode ?? 'manual', options.triggerData, options.idempotencyKey, options.throwOnError ?? true);
1622
+ }
1623
+ /**
1624
+ * Look up the most recent execution row tagged with this idempotency
1625
+ * key for the given workflow. Returns null when none exists. The
1626
+ * dispatch layer uses this to dedup back-to-back schedule fires that
1627
+ * share a minute bucket — see WorkflowDispatchService.execute.
1628
+ */
1629
+ async findExecutionByIdempotencyKey(workflowId, idempotencyKey) {
1630
+ await this.ensureSchema();
1631
+ const rows = await this.getDb()
1632
+ .select()
1633
+ .from(embeddedExecutions)
1634
+ .where(and(eq(embeddedExecutions.workflowId, workflowId), eq(embeddedExecutions.idempotencyKey, idempotencyKey)))
1635
+ .orderBy(desc(embeddedExecutions.startedAt))
1636
+ .limit(1);
1637
+ const row = rows[0];
1638
+ return row ? cloneJson(row.execution) : null;
1514
1639
  }
1515
1640
  async executeWebhook(path, payload, method = 'POST') {
1516
1641
  await this.ensureSchema();
@@ -1567,6 +1692,24 @@ export class EmbeddedWorkflowService extends Service {
1567
1692
  }
1568
1693
  return executions;
1569
1694
  }
1695
+ async captureWorkflowRevision(workflowId, entry, operation) {
1696
+ await this.ensureSchema();
1697
+ await this.getDb()
1698
+ .insert(workflowRevisions)
1699
+ .values({
1700
+ id: randomUUID(),
1701
+ workflowId,
1702
+ versionId: entry.versionId,
1703
+ name: entry.workflow.name,
1704
+ active: entry.workflow.active === true,
1705
+ workflow: cloneJson(entry.workflow),
1706
+ createdAt: entry.createdAt,
1707
+ updatedAt: entry.updatedAt,
1708
+ capturedAt: nowIso(),
1709
+ operation,
1710
+ })
1711
+ .onConflictDoNothing();
1712
+ }
1570
1713
  async getStoredWorkflow(id) {
1571
1714
  await this.ensureSchema();
1572
1715
  const rows = await this.getDb()
@@ -1632,9 +1775,14 @@ export class EmbeddedWorkflowService extends Service {
1632
1775
  /** Re-create core Tasks for every active workflow on service start.
1633
1776
  * Tasks themselves persist across restart; this is a reconcile step that
1634
1777
  * ensures workflows whose schedule changed (or whose tasks were never
1635
- * created in the first place) end up correctly scheduled. */
1778
+ * created in the first place) end up correctly scheduled.
1779
+ *
1780
+ * Also performs a one-shot migration: any pre-existing legacy
1781
+ * `workflow.run` / `workflow.webhook` task rows are deleted so the new
1782
+ * `TRIGGER_DISPATCH` path is the single source of scheduled runs. */
1636
1783
  async rehydrateSchedules() {
1637
1784
  await this.ensureSchema();
1785
+ await this.deleteLegacyScheduleTasks();
1638
1786
  const rows = await this.getDb()
1639
1787
  .select()
1640
1788
  .from(embeddedWorkflows)
@@ -1643,9 +1791,62 @@ export class EmbeddedWorkflowService extends Service {
1643
1791
  await this.armSchedules(row.id);
1644
1792
  }
1645
1793
  }
1646
- /** Create one recurring core Task per scheduleTrigger node on the workflow.
1647
- * Idempotent: existing tasks for this workflow are removed first so the
1648
- * task set always reflects the current workflow definition. */
1794
+ /** Remove legacy `workflow.run` / `workflow.webhook` Tasks left behind
1795
+ * by earlier service versions. Returns the count so callers (and the
1796
+ * migration log) can verify the cleanup. */
1797
+ async deleteLegacyScheduleTasks() {
1798
+ if (typeof this.runtime.getTasks !== 'function' ||
1799
+ typeof this.runtime.deleteTask !== 'function') {
1800
+ return 0;
1801
+ }
1802
+ const tasks = await this.runtime.getTasks({
1803
+ tags: [WORKFLOW_TASK_TAG],
1804
+ agentIds: [this.runtime.agentId],
1805
+ });
1806
+ if (!tasks.length)
1807
+ return 0;
1808
+ let removed = 0;
1809
+ for (const task of tasks) {
1810
+ if (!task.id)
1811
+ continue;
1812
+ if (task.name === LEGACY_WORKFLOW_RUN_TASK_NAME ||
1813
+ task.name === LEGACY_WORKFLOW_WEBHOOK_TASK_NAME) {
1814
+ await this.runtime.deleteTask(task.id);
1815
+ removed += 1;
1816
+ }
1817
+ }
1818
+ if (removed > 0) {
1819
+ logger.info({ src: 'plugin:workflow:embedded', removed }, `Removed ${removed} legacy workflow task row(s); schedules will re-arm via TRIGGER_DISPATCH`);
1820
+ }
1821
+ return removed;
1822
+ }
1823
+ /** Build a `TriggerConfig` for a workflow schedule node. The resulting
1824
+ * config is what the agent's `executeTriggerTask` reads off the task
1825
+ * metadata when the scheduler fires. */
1826
+ buildScheduleTrigger(workflowId, workflowName, intervalMs) {
1827
+ const triggerId = stringToUuid(`${workflowId}:schedule:${randomUUID()}`);
1828
+ return {
1829
+ version: TRIGGER_SCHEMA_VERSION,
1830
+ triggerId,
1831
+ displayName: `Scheduled workflow run: ${workflowName}`,
1832
+ instructions: `Run workflow ${workflowName}`,
1833
+ triggerType: 'interval',
1834
+ enabled: true,
1835
+ wakeMode: 'inject_now',
1836
+ createdBy: 'workflow.schedule',
1837
+ intervalMs,
1838
+ runCount: 0,
1839
+ kind: 'workflow',
1840
+ workflowId,
1841
+ workflowName,
1842
+ };
1843
+ }
1844
+ /** Create one recurring `TRIGGER_DISPATCH` Task per scheduleTrigger
1845
+ * node on the workflow. Idempotent: existing tasks for this workflow
1846
+ * are removed first so the task set always reflects the current
1847
+ * workflow definition. Each task carries an idempotency key derived
1848
+ * from `(workflowId, nextRunAt-minute-bucket)` so that simultaneous
1849
+ * fires within the same minute deduplicate at dispatch. */
1649
1850
  async armSchedules(workflowId) {
1650
1851
  await this.clearSchedules(workflowId);
1651
1852
  if (typeof this.runtime.createTask !== 'function')
@@ -1654,19 +1855,30 @@ export class EmbeddedWorkflowService extends Service {
1654
1855
  const scheduleNodes = entry.workflow.nodes.filter((node) => !node.disabled && node.type === 'workflows-nodes-base.scheduleTrigger');
1655
1856
  if (scheduleNodes.length === 0)
1656
1857
  return;
1858
+ const nowMs = Date.now();
1657
1859
  for (const node of scheduleNodes) {
1658
1860
  const intervalMs = resolveScheduleIntervalMs(node.parameters);
1861
+ const trigger = this.buildScheduleTrigger(workflowId, entry.workflow.name, intervalMs);
1862
+ const nextRunAtMs = nowMs + intervalMs;
1863
+ const triggerWithSchedule = {
1864
+ ...trigger,
1865
+ nextRunAtMs,
1866
+ };
1867
+ const idempotencyKey = buildScheduleIdempotencyKey(workflowId, nextRunAtMs);
1659
1868
  await this.runtime.createTask({
1660
- name: WORKFLOW_RUN_TASK_WORKER_NAME,
1661
- description: `Scheduled workflow run: ${entry.workflow.name}`,
1662
- tags: ['queue', 'repeat', WORKFLOW_TASK_TAG],
1869
+ name: TRIGGER_TASK_NAME,
1870
+ description: trigger.displayName,
1871
+ tags: [...TRIGGER_TASK_TAGS, WORKFLOW_TASK_TAG],
1663
1872
  metadata: {
1873
+ blocking: true,
1874
+ updatedAt: nowMs,
1875
+ updateInterval: intervalMs,
1876
+ baseInterval: intervalMs,
1664
1877
  kind: WORKFLOW_TASK_KIND,
1665
1878
  workflowId,
1666
1879
  scheduleNodeId: node.id,
1667
- updateInterval: intervalMs,
1668
- baseInterval: intervalMs,
1669
- updatedAt: Date.now(),
1880
+ idempotencyKey,
1881
+ trigger: triggerWithSchedule,
1670
1882
  },
1671
1883
  });
1672
1884
  }
@@ -1679,7 +1891,7 @@ export class EmbeddedWorkflowService extends Service {
1679
1891
  tags: [WORKFLOW_TASK_TAG],
1680
1892
  agentIds: [this.runtime.agentId],
1681
1893
  });
1682
- if (!tasks?.length)
1894
+ if (!tasks.length)
1683
1895
  return;
1684
1896
  for (const task of tasks) {
1685
1897
  if (task.id &&
@@ -1688,8 +1900,9 @@ export class EmbeddedWorkflowService extends Service {
1688
1900
  }
1689
1901
  }
1690
1902
  }
1691
- async saveExecution(execution) {
1903
+ async saveExecution(execution, idempotencyKey) {
1692
1904
  await this.ensureSchema();
1905
+ const key = idempotencyKey ?? null;
1693
1906
  await this.getDb()
1694
1907
  .insert(embeddedExecutions)
1695
1908
  .values({
@@ -1701,6 +1914,7 @@ export class EmbeddedWorkflowService extends Service {
1701
1914
  startedAt: execution.startedAt,
1702
1915
  stoppedAt: execution.stoppedAt ?? null,
1703
1916
  execution: cloneJson(execution),
1917
+ idempotencyKey: key,
1704
1918
  })
1705
1919
  .onConflictDoUpdate({
1706
1920
  target: embeddedExecutions.id,
@@ -1712,22 +1926,23 @@ export class EmbeddedWorkflowService extends Service {
1712
1926
  startedAt: execution.startedAt,
1713
1927
  stoppedAt: execution.stoppedAt ?? null,
1714
1928
  execution: cloneJson(execution),
1929
+ idempotencyKey: key,
1715
1930
  },
1716
1931
  });
1717
1932
  }
1718
1933
  buildIncomingConnections(workflowData) {
1719
1934
  const incoming = new Map();
1720
- for (const [source, outputsByType] of Object.entries(workflowData.connections ?? {})) {
1721
- const mainOutputs = outputsByType.main ?? [];
1935
+ for (const [source, outputsByType] of Object.entries(workflowData.connections)) {
1936
+ const mainOutputs = outputsByType.main;
1722
1937
  mainOutputs.forEach((connections, sourceOutputIndex) => {
1723
- for (const connection of connections ?? []) {
1938
+ for (const connection of connections) {
1724
1939
  if (connection.type !== 'main')
1725
1940
  continue;
1726
1941
  const destination = incoming.get(connection.node) ?? [];
1727
1942
  destination.push({
1728
1943
  source,
1729
1944
  sourceOutputIndex,
1730
- destinationInputIndex: connection.index ?? 0,
1945
+ destinationInputIndex: connection.index,
1731
1946
  });
1732
1947
  incoming.set(connection.node, destination);
1733
1948
  }
@@ -1772,17 +1987,40 @@ export class EmbeddedWorkflowService extends Service {
1772
1987
  }
1773
1988
  return start;
1774
1989
  }
1775
- collectInputData(nodeName, incoming, nodeOutputs) {
1776
- const inputData = [];
1777
- for (const connection of incoming.get(nodeName) ?? []) {
1778
- const sourceOutputs = nodeOutputs.get(connection.source) ?? [];
1779
- const sourceItems = sourceOutputs[connection.sourceOutputIndex] ?? [];
1780
- inputData[connection.destinationInputIndex] = [
1781
- ...(inputData[connection.destinationInputIndex] ?? []),
1782
- ...sourceItems,
1783
- ];
1990
+ resolveExecutionPlan(workflowData, mode) {
1991
+ const enabledNodes = workflowData.nodes.filter((node) => !node.disabled);
1992
+ const nodeByName = new Map(enabledNodes.map((node) => [node.name, node]));
1993
+ const incoming = this.buildIncomingConnections(workflowData);
1994
+ const startNodes = this.resolveStartNodes(workflowData, mode, incoming);
1995
+ const orderedNodes = [];
1996
+ const executed = new Set();
1997
+ while (executed.size < enabledNodes.length) {
1998
+ let progressed = false;
1999
+ for (const node of enabledNodes) {
2000
+ if (executed.has(node.name))
2001
+ continue;
2002
+ const incomingConnections = incoming.get(node.name)?.filter((connection) => nodeByName.has(connection.source)) ?? [];
2003
+ const isStartNode = startNodes.has(node.name);
2004
+ const dependenciesComplete = incomingConnections.every((connection) => executed.has(connection.source));
2005
+ if (!isStartNode && !dependenciesComplete)
2006
+ continue;
2007
+ orderedNodes.push(node);
2008
+ executed.add(node.name);
2009
+ progressed = true;
2010
+ }
2011
+ if (!progressed) {
2012
+ const unresolved = enabledNodes
2013
+ .filter((node) => !executed.has(node.name))
2014
+ .map((node) => node.name)
2015
+ .join(', ');
2016
+ throw new Error(`Unable to resolve workflow execution order for node(s): ${unresolved}`);
2017
+ }
1784
2018
  }
1785
- return inputData.length > 0 ? inputData : [[]];
2019
+ return {
2020
+ enabledNodes: orderedNodes,
2021
+ startNodes: [...startNodes],
2022
+ incoming: Object.fromEntries(incoming.entries()),
2023
+ };
1786
2024
  }
1787
2025
  async executeNode(node, inputData, executionId) {
1788
2026
  const nodeType = this.nodeTypes.getByNameAndVersion(node.type);
@@ -1795,7 +2033,7 @@ export class EmbeddedWorkflowService extends Service {
1795
2033
  const output = await nodeType.execute.call(context);
1796
2034
  return output.length > 0 ? output : [[]];
1797
2035
  }
1798
- async runWorkflow(workflowData, mode) {
2036
+ async runWorkflow(workflowData, mode, triggerData, idempotencyKey, throwOnError = true) {
1799
2037
  const executionId = randomUUID();
1800
2038
  const startedAt = new Date();
1801
2039
  const pending = {
@@ -1805,75 +2043,28 @@ export class EmbeddedWorkflowService extends Service {
1805
2043
  startedAt: startedAt.toISOString(),
1806
2044
  workflowId: workflowData.id ?? '',
1807
2045
  status: 'running',
2046
+ ...(triggerData || idempotencyKey
2047
+ ? {
2048
+ customData: {
2049
+ ...(triggerData ? { triggerData } : {}),
2050
+ ...(idempotencyKey ? { idempotencyKey } : {}),
2051
+ },
2052
+ }
2053
+ : {}),
1808
2054
  };
1809
- await this.saveExecution(pending);
2055
+ await this.saveExecution(pending, idempotencyKey);
1810
2056
  try {
1811
- const enabledNodes = workflowData.nodes.filter((node) => !node.disabled);
1812
- const nodeByName = new Map(enabledNodes.map((node) => [node.name, node]));
1813
- const incoming = this.buildIncomingConnections(workflowData);
1814
- const startNodes = this.resolveStartNodes(workflowData, mode, incoming);
1815
- const nodeOutputs = new Map();
1816
- const executed = new Set();
1817
- const runData = {};
1818
- let lastNodeExecuted;
1819
- while (executed.size < enabledNodes.length) {
1820
- let progressed = false;
1821
- for (const node of enabledNodes) {
1822
- if (executed.has(node.name))
1823
- continue;
1824
- const incomingConnections = incoming.get(node.name)?.filter((connection) => nodeByName.has(connection.source)) ??
1825
- [];
1826
- const isStartNode = startNodes.has(node.name);
1827
- const dependenciesComplete = incomingConnections.every((connection) => executed.has(connection.source));
1828
- if (!isStartNode && !dependenciesComplete)
1829
- continue;
1830
- const inputData = isStartNode && incomingConnections.length === 0
1831
- ? [[]]
1832
- : this.collectInputData(node.name, incoming, nodeOutputs);
1833
- const hasInputItems = inputData.some((items) => items.length > 0);
1834
- const started = Date.now();
1835
- const outputData = !isStartNode && incomingConnections.length > 0 && !hasInputItems
1836
- ? [[]]
1837
- : await this.executeNode(node, inputData, executionId);
1838
- nodeOutputs.set(node.name, outputData);
1839
- runData[node.name] = [
1840
- {
1841
- startTime: started,
1842
- executionTime: Date.now() - started,
1843
- data: { main: cloneJson(outputData) },
1844
- source: incomingConnections.map((connection) => ({
1845
- previousNode: connection.source,
1846
- previousNodeOutput: connection.sourceOutputIndex,
1847
- previousNodeRun: 0,
1848
- })),
1849
- },
1850
- ];
1851
- executed.add(node.name);
1852
- lastNodeExecuted = node.name;
1853
- progressed = true;
1854
- }
1855
- if (!progressed) {
1856
- const unresolved = enabledNodes
1857
- .filter((node) => !executed.has(node.name))
1858
- .map((node) => node.name)
1859
- .join(', ');
1860
- throw new Error(`Unable to resolve workflow execution order for node(s): ${unresolved}`);
1861
- }
1862
- }
1863
- const stoppedAt = new Date();
1864
- const execution = {
1865
- ...pending,
1866
- finished: true,
1867
- status: 'success',
1868
- stoppedAt: stoppedAt.toISOString(),
1869
- data: {
1870
- resultData: {
1871
- runData,
1872
- lastNodeExecuted,
1873
- },
1874
- },
1875
- };
1876
- await this.saveExecution(execution);
2057
+ const plan = this.resolveExecutionPlan(workflowData, mode);
2058
+ const execution = await runWorkflowWithSmithers({
2059
+ workflow: workflowData,
2060
+ executionId,
2061
+ pending,
2062
+ mode,
2063
+ triggerData,
2064
+ plan,
2065
+ runNode: (node, inputData) => this.executeNode(node, inputData, executionId),
2066
+ });
2067
+ await this.saveExecution(execution, idempotencyKey);
1877
2068
  return cloneJson(execution);
1878
2069
  }
1879
2070
  catch (error) {
@@ -1892,7 +2083,10 @@ export class EmbeddedWorkflowService extends Service {
1892
2083
  },
1893
2084
  },
1894
2085
  };
1895
- await this.saveExecution(execution);
2086
+ await this.saveExecution(execution, idempotencyKey);
2087
+ if (!throwOnError) {
2088
+ return cloneJson(execution);
2089
+ }
1896
2090
  throw error;
1897
2091
  }
1898
2092
  }