@elizaos/plugin-workflow 2.0.0-beta.1 → 2.0.3-beta.6
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.
- package/LICENSE +21 -0
- package/README.md +28 -26
- package/dist/actions/eval-code.d.ts +12 -0
- package/dist/actions/eval-code.d.ts.map +1 -0
- package/dist/actions/eval-code.js +59 -0
- package/dist/actions/eval-code.js.map +1 -0
- package/dist/actions/index.d.ts +1 -0
- package/dist/actions/index.d.ts.map +1 -1
- package/dist/actions/index.js +1 -0
- package/dist/actions/index.js.map +1 -1
- package/dist/actions/workflow.d.ts +7 -0
- package/dist/actions/workflow.d.ts.map +1 -1
- package/dist/actions/workflow.js +462 -10
- package/dist/actions/workflow.js.map +1 -1
- package/dist/db/schema.d.ts +196 -0
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +23 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +9 -64
- package/dist/index.js.map +1 -1
- package/dist/lib/automations-builder.d.ts.map +1 -1
- package/dist/lib/automations-builder.js +10 -35
- package/dist/lib/automations-builder.js.map +1 -1
- package/dist/lib/automations-types.d.ts +2 -2
- package/dist/lib/automations-types.d.ts.map +1 -1
- package/dist/lib/automations-types.js.map +1 -1
- package/dist/lib/index.d.ts +0 -2
- package/dist/lib/index.d.ts.map +1 -1
- package/dist/lib/index.js +1 -2
- package/dist/lib/index.js.map +1 -1
- package/dist/lib/workflow-clarification.d.ts +2 -2
- package/dist/lib/workflow-clarification.d.ts.map +1 -1
- package/dist/lib/workflow-clarification.js +15 -11
- package/dist/lib/workflow-clarification.js.map +1 -1
- package/dist/plugin-routes.d.ts.map +1 -1
- package/dist/plugin-routes.js +6 -0
- package/dist/plugin-routes.js.map +1 -1
- package/dist/providers/activeWorkflows.js +2 -2
- package/dist/providers/activeWorkflows.js.map +1 -1
- package/dist/providers/workflowStatus.js +1 -1
- package/dist/providers/workflowStatus.js.map +1 -1
- package/dist/routes/workflow-routes.d.ts.map +1 -1
- package/dist/routes/workflow-routes.js +68 -2
- package/dist/routes/workflow-routes.js.map +1 -1
- package/dist/routes/workflows.d.ts.map +1 -1
- package/dist/routes/workflows.js +5 -1
- package/dist/routes/workflows.js.map +1 -1
- package/dist/services/embedded-workflow-service.d.ts +74 -17
- package/dist/services/embedded-workflow-service.d.ts.map +1 -1
- package/dist/services/embedded-workflow-service.js +343 -149
- package/dist/services/embedded-workflow-service.js.map +1 -1
- package/dist/services/smithers-runtime.d.ts +47 -0
- package/dist/services/smithers-runtime.d.ts.map +1 -0
- package/dist/services/smithers-runtime.js +444 -0
- package/dist/services/smithers-runtime.js.map +1 -0
- package/dist/services/workflow-credential-store.js +1 -1
- package/dist/services/workflow-credential-store.js.map +1 -1
- package/dist/services/workflow-dispatch.d.ts +31 -1
- package/dist/services/workflow-dispatch.d.ts.map +1 -1
- package/dist/services/workflow-dispatch.js +75 -10
- package/dist/services/workflow-dispatch.js.map +1 -1
- package/dist/services/workflow-service.d.ts +27 -1
- package/dist/services/workflow-service.d.ts.map +1 -1
- package/dist/services/workflow-service.js +133 -11
- package/dist/services/workflow-service.js.map +1 -1
- package/dist/trigger-routes.d.ts +2 -18
- package/dist/trigger-routes.d.ts.map +1 -1
- package/dist/trigger-routes.js +11 -39
- package/dist/trigger-routes.js.map +1 -1
- package/dist/types/index.d.ts +82 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/workflow-contracts.d.ts +118 -0
- package/dist/types/workflow-contracts.d.ts.map +1 -0
- package/dist/types/workflow-contracts.js +2 -0
- package/dist/types/workflow-contracts.js.map +1 -0
- package/dist/utils/catalog.js +2 -2
- package/dist/utils/catalog.js.map +1 -1
- package/dist/utils/clarification.d.ts +1 -1
- package/dist/utils/clarification.d.ts.map +1 -1
- package/dist/utils/clarification.js +15 -4
- package/dist/utils/clarification.js.map +1 -1
- package/dist/utils/context.js +1 -1
- package/dist/utils/context.js.map +1 -1
- package/dist/utils/evaluation-samples.d.ts +6 -0
- package/dist/utils/evaluation-samples.d.ts.map +1 -0
- package/dist/utils/evaluation-samples.js +216 -0
- package/dist/utils/evaluation-samples.js.map +1 -0
- package/dist/utils/execution-diagnostics.d.ts +26 -0
- package/dist/utils/execution-diagnostics.d.ts.map +1 -0
- package/dist/utils/execution-diagnostics.js +159 -0
- package/dist/utils/execution-diagnostics.js.map +1 -0
- package/dist/utils/generation.d.ts.map +1 -1
- package/dist/utils/generation.js +134 -19
- package/dist/utils/generation.js.map +1 -1
- package/dist/utils/host-capabilities.d.ts.map +1 -1
- package/dist/utils/host-capabilities.js +20 -5
- package/dist/utils/host-capabilities.js.map +1 -1
- package/dist/utils/inferSyntheticOutputSchema.js +3 -3
- package/dist/utils/inferSyntheticOutputSchema.js.map +1 -1
- package/dist/utils/outputSchema.js +1 -1
- package/dist/utils/outputSchema.js.map +1 -1
- package/dist/utils/validateAndRepair.js +10 -10
- package/dist/utils/validateAndRepair.js.map +1 -1
- package/dist/utils/workflow-prompts/draftIntent.d.ts +1 -1
- package/dist/utils/workflow-prompts/draftIntent.d.ts.map +1 -1
- package/dist/utils/workflow-prompts/draftIntent.js +1 -1
- package/dist/utils/workflow-prompts/keywordExtraction.d.ts +1 -1
- package/dist/utils/workflow-prompts/keywordExtraction.d.ts.map +1 -1
- package/dist/utils/workflow-prompts/keywordExtraction.js +1 -1
- package/dist/utils/workflow-prompts/workflowGeneration.d.ts +1 -1
- package/dist/utils/workflow-prompts/workflowGeneration.d.ts.map +1 -1
- package/dist/utils/workflow-prompts/workflowGeneration.js +4 -4
- package/dist/utils/workflow-prompts/workflowMatching.d.ts +1 -1
- package/dist/utils/workflow-prompts/workflowMatching.d.ts.map +1 -1
- package/dist/utils/workflow-prompts/workflowMatching.js +1 -1
- package/dist/utils/workflow.d.ts +1 -0
- package/dist/utils/workflow.d.ts.map +1 -1
- package/dist/utils/workflow.js +44 -8
- package/dist/utils/workflow.js.map +1 -1
- package/package.json +27 -8
- package/registry-entry.json +25 -0
- package/src/actions/eval-code.ts +81 -0
- package/src/actions/index.ts +1 -0
- package/src/actions/workflow.ts +518 -10
- package/src/db/schema.ts +31 -0
- package/src/index.ts +9 -82
- package/src/lib/automations-builder.ts +11 -35
- package/src/lib/automations-types.ts +1 -2
- package/src/lib/index.ts +0 -8
- package/src/lib/workflow-clarification.ts +18 -13
- package/src/plugin-routes.ts +6 -0
- package/src/providers/activeWorkflows.ts +2 -2
- package/src/providers/workflowStatus.ts +1 -1
- package/src/routes/workflow-routes.ts +100 -2
- package/src/routes/workflows.ts +5 -1
- package/src/services/embedded-workflow-service.ts +447 -172
- package/src/services/smithers-runtime.ts +526 -0
- package/src/services/workflow-credential-store.ts +1 -1
- package/src/services/workflow-dispatch.ts +116 -13
- package/src/services/workflow-service.ts +186 -10
- package/src/trigger-routes.ts +12 -70
- package/src/types/index.ts +94 -2
- package/src/types/workflow-contracts.ts +166 -0
- package/src/utils/catalog.ts +2 -2
- package/src/utils/clarification.ts +19 -5
- package/src/utils/context.ts +1 -1
- package/src/utils/evaluation-samples.ts +239 -0
- package/src/utils/execution-diagnostics.ts +192 -0
- package/src/utils/generation.ts +224 -32
- package/src/utils/host-capabilities.ts +21 -5
- package/src/utils/inferSyntheticOutputSchema.ts +3 -3
- package/src/utils/outputSchema.ts +1 -1
- package/src/utils/validateAndRepair.ts +10 -10
- package/src/utils/workflow-prompts/draftIntent.ts +1 -1
- package/src/utils/workflow-prompts/keywordExtraction.ts +1 -1
- package/src/utils/workflow-prompts/workflowGeneration.ts +4 -4
- package/src/utils/workflow-prompts/workflowMatching.ts +1 -1
- package/src/utils/workflow.ts +56 -8
- package/dist/lib/legacy-task-migration.d.ts +0 -20
- package/dist/lib/legacy-task-migration.d.ts.map +0 -1
- package/dist/lib/legacy-task-migration.js +0 -110
- package/dist/lib/legacy-task-migration.js.map +0 -1
- package/dist/lib/legacy-text-trigger-migration.d.ts +0 -18
- package/dist/lib/legacy-text-trigger-migration.d.ts.map +0 -1
- package/dist/lib/legacy-text-trigger-migration.js +0 -131
- package/dist/lib/legacy-text-trigger-migration.js.map +0 -1
- package/src/lib/legacy-task-migration.ts +0 -143
- 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
|
-
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
export const
|
|
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: { ...
|
|
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
|
-
...
|
|
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
|
|
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
|
|
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
|
-
/**
|
|
1647
|
-
*
|
|
1648
|
-
*
|
|
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:
|
|
1661
|
-
description:
|
|
1662
|
-
tags: [
|
|
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
|
-
|
|
1668
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
1776
|
-
const
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
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
|
|
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
|
|
1812
|
-
const
|
|
1813
|
-
|
|
1814
|
-
|
|
1815
|
-
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
1819
|
-
|
|
1820
|
-
|
|
1821
|
-
|
|
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
|
}
|