@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,5 +1,13 @@
|
|
|
1
1
|
import { createHash, randomUUID } from 'node:crypto';
|
|
2
|
-
import {
|
|
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
|
-
/**
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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: { ...
|
|
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
|
-
...
|
|
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
|
|
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
|
|
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(
|
|
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
|
-
/**
|
|
1937
|
-
*
|
|
1938
|
-
*
|
|
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:
|
|
1952
|
-
description:
|
|
1953
|
-
tags: [
|
|
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
|
-
|
|
1959
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
const
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
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
|
-
|
|
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
|
|
2126
|
-
const
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
|
|
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
|
}
|