@elizaos/plugin-workflow 2.0.0-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (294) hide show
  1. package/README.md +71 -0
  2. package/auto-enable.ts +18 -0
  3. package/dist/actions/index.d.ts +2 -0
  4. package/dist/actions/index.d.ts.map +1 -0
  5. package/dist/actions/index.js +2 -0
  6. package/dist/actions/index.js.map +1 -0
  7. package/dist/actions/workflow.d.ts +23 -0
  8. package/dist/actions/workflow.d.ts.map +1 -0
  9. package/dist/actions/workflow.js +425 -0
  10. package/dist/actions/workflow.js.map +1 -0
  11. package/dist/data/defaultNodes.json +9887 -0
  12. package/dist/data/schemaIndex.json +1 -0
  13. package/dist/data/triggerSchemaIndex.json +1 -0
  14. package/dist/db/index.d.ts +2 -0
  15. package/dist/db/index.d.ts.map +1 -0
  16. package/dist/db/index.js +2 -0
  17. package/dist/db/index.js.map +1 -0
  18. package/dist/db/schema.d.ts +588 -0
  19. package/dist/db/schema.d.ts.map +1 -0
  20. package/dist/db/schema.js +59 -0
  21. package/dist/db/schema.js.map +1 -0
  22. package/dist/index.d.ts +34 -0
  23. package/dist/index.d.ts.map +1 -0
  24. package/dist/index.js +126 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/lib/automations-builder.d.ts +21 -0
  27. package/dist/lib/automations-builder.d.ts.map +1 -0
  28. package/dist/lib/automations-builder.js +557 -0
  29. package/dist/lib/automations-builder.js.map +1 -0
  30. package/dist/lib/automations-types.d.ts +153 -0
  31. package/dist/lib/automations-types.d.ts.map +1 -0
  32. package/dist/lib/automations-types.js +191 -0
  33. package/dist/lib/automations-types.js.map +1 -0
  34. package/dist/lib/index.d.ts +3 -0
  35. package/dist/lib/index.d.ts.map +1 -0
  36. package/dist/lib/index.js +3 -0
  37. package/dist/lib/index.js.map +1 -0
  38. package/dist/lib/legacy-task-migration.d.ts +20 -0
  39. package/dist/lib/legacy-task-migration.d.ts.map +1 -0
  40. package/dist/lib/legacy-task-migration.js +110 -0
  41. package/dist/lib/legacy-task-migration.js.map +1 -0
  42. package/dist/lib/legacy-text-trigger-migration.d.ts +18 -0
  43. package/dist/lib/legacy-text-trigger-migration.d.ts.map +1 -0
  44. package/dist/lib/legacy-text-trigger-migration.js +131 -0
  45. package/dist/lib/legacy-text-trigger-migration.js.map +1 -0
  46. package/dist/lib/workflow-clarification.d.ts +113 -0
  47. package/dist/lib/workflow-clarification.d.ts.map +1 -0
  48. package/dist/lib/workflow-clarification.js +425 -0
  49. package/dist/lib/workflow-clarification.js.map +1 -0
  50. package/dist/plugin-routes.d.ts +9 -0
  51. package/dist/plugin-routes.d.ts.map +1 -0
  52. package/dist/plugin-routes.js +147 -0
  53. package/dist/plugin-routes.js.map +1 -0
  54. package/dist/providers/activeWorkflows.d.ts +11 -0
  55. package/dist/providers/activeWorkflows.d.ts.map +1 -0
  56. package/dist/providers/activeWorkflows.js +72 -0
  57. package/dist/providers/activeWorkflows.js.map +1 -0
  58. package/dist/providers/index.d.ts +4 -0
  59. package/dist/providers/index.d.ts.map +1 -0
  60. package/dist/providers/index.js +4 -0
  61. package/dist/providers/index.js.map +1 -0
  62. package/dist/providers/pendingDraft.d.ts +9 -0
  63. package/dist/providers/pendingDraft.d.ts.map +1 -0
  64. package/dist/providers/pendingDraft.js +48 -0
  65. package/dist/providers/pendingDraft.js.map +1 -0
  66. package/dist/providers/workflowStatus.d.ts +3 -0
  67. package/dist/providers/workflowStatus.d.ts.map +1 -0
  68. package/dist/providers/workflowStatus.js +69 -0
  69. package/dist/providers/workflowStatus.js.map +1 -0
  70. package/dist/register-routes.d.ts +2 -0
  71. package/dist/register-routes.d.ts.map +1 -0
  72. package/dist/register-routes.js +6 -0
  73. package/dist/register-routes.js.map +1 -0
  74. package/dist/routes/_helpers.d.ts +11 -0
  75. package/dist/routes/_helpers.d.ts.map +1 -0
  76. package/dist/routes/_helpers.js +22 -0
  77. package/dist/routes/_helpers.js.map +1 -0
  78. package/dist/routes/automations.d.ts +19 -0
  79. package/dist/routes/automations.d.ts.map +1 -0
  80. package/dist/routes/automations.js +32 -0
  81. package/dist/routes/automations.js.map +1 -0
  82. package/dist/routes/embedded-webhooks.d.ts +3 -0
  83. package/dist/routes/embedded-webhooks.d.ts.map +1 -0
  84. package/dist/routes/embedded-webhooks.js +47 -0
  85. package/dist/routes/embedded-webhooks.js.map +1 -0
  86. package/dist/routes/executions.d.ts +3 -0
  87. package/dist/routes/executions.d.ts.map +1 -0
  88. package/dist/routes/executions.js +58 -0
  89. package/dist/routes/executions.js.map +1 -0
  90. package/dist/routes/index.d.ts +4 -0
  91. package/dist/routes/index.d.ts.map +1 -0
  92. package/dist/routes/index.js +14 -0
  93. package/dist/routes/index.js.map +1 -0
  94. package/dist/routes/nodes.d.ts +3 -0
  95. package/dist/routes/nodes.d.ts.map +1 -0
  96. package/dist/routes/nodes.js +168 -0
  97. package/dist/routes/nodes.js.map +1 -0
  98. package/dist/routes/validation.d.ts +3 -0
  99. package/dist/routes/validation.d.ts.map +1 -0
  100. package/dist/routes/validation.js +41 -0
  101. package/dist/routes/validation.js.map +1 -0
  102. package/dist/routes/workflow-routes.d.ts +27 -0
  103. package/dist/routes/workflow-routes.d.ts.map +1 -0
  104. package/dist/routes/workflow-routes.js +326 -0
  105. package/dist/routes/workflow-routes.js.map +1 -0
  106. package/dist/routes/workflows.d.ts +3 -0
  107. package/dist/routes/workflows.d.ts.map +1 -0
  108. package/dist/routes/workflows.js +252 -0
  109. package/dist/routes/workflows.js.map +1 -0
  110. package/dist/schemas/draftIntent.d.ts +22 -0
  111. package/dist/schemas/draftIntent.d.ts.map +1 -0
  112. package/dist/schemas/draftIntent.js +22 -0
  113. package/dist/schemas/draftIntent.js.map +1 -0
  114. package/dist/schemas/feasibility.d.ts +13 -0
  115. package/dist/schemas/feasibility.d.ts.map +1 -0
  116. package/dist/schemas/feasibility.js +9 -0
  117. package/dist/schemas/feasibility.js.map +1 -0
  118. package/dist/schemas/index.d.ts +5 -0
  119. package/dist/schemas/index.d.ts.map +1 -0
  120. package/dist/schemas/index.js +5 -0
  121. package/dist/schemas/index.js.map +1 -0
  122. package/dist/schemas/keywordExtraction.d.ts +14 -0
  123. package/dist/schemas/keywordExtraction.d.ts.map +1 -0
  124. package/dist/schemas/keywordExtraction.js +12 -0
  125. package/dist/schemas/keywordExtraction.js.map +1 -0
  126. package/dist/schemas/workflowMatching.d.ts +36 -0
  127. package/dist/schemas/workflowMatching.d.ts.map +1 -0
  128. package/dist/schemas/workflowMatching.js +30 -0
  129. package/dist/schemas/workflowMatching.js.map +1 -0
  130. package/dist/services/embedded-workflow-service.d.ts +106 -0
  131. package/dist/services/embedded-workflow-service.d.ts.map +1 -0
  132. package/dist/services/embedded-workflow-service.js +1900 -0
  133. package/dist/services/embedded-workflow-service.js.map +1 -0
  134. package/dist/services/index.d.ts +5 -0
  135. package/dist/services/index.d.ts.map +1 -0
  136. package/dist/services/index.js +5 -0
  137. package/dist/services/index.js.map +1 -0
  138. package/dist/services/workflow-credential-store.d.ts +27 -0
  139. package/dist/services/workflow-credential-store.d.ts.map +1 -0
  140. package/dist/services/workflow-credential-store.js +92 -0
  141. package/dist/services/workflow-credential-store.js.map +1 -0
  142. package/dist/services/workflow-dispatch.d.ts +41 -0
  143. package/dist/services/workflow-dispatch.d.ts.map +1 -0
  144. package/dist/services/workflow-dispatch.js +86 -0
  145. package/dist/services/workflow-dispatch.js.map +1 -0
  146. package/dist/services/workflow-service.d.ts +63 -0
  147. package/dist/services/workflow-service.d.ts.map +1 -0
  148. package/dist/services/workflow-service.js +492 -0
  149. package/dist/services/workflow-service.js.map +1 -0
  150. package/dist/trigger-routes.d.ts +153 -0
  151. package/dist/trigger-routes.d.ts.map +1 -0
  152. package/dist/trigger-routes.js +424 -0
  153. package/dist/trigger-routes.js.map +1 -0
  154. package/dist/types/index.d.ts +457 -0
  155. package/dist/types/index.d.ts.map +1 -0
  156. package/dist/types/index.js +59 -0
  157. package/dist/types/index.js.map +1 -0
  158. package/dist/utils/catalog.d.ts +16 -0
  159. package/dist/utils/catalog.d.ts.map +1 -0
  160. package/dist/utils/catalog.js +211 -0
  161. package/dist/utils/catalog.js.map +1 -0
  162. package/dist/utils/clarification.d.ts +17 -0
  163. package/dist/utils/clarification.d.ts.map +1 -0
  164. package/dist/utils/clarification.js +46 -0
  165. package/dist/utils/clarification.js.map +1 -0
  166. package/dist/utils/context.d.ts +4 -0
  167. package/dist/utils/context.d.ts.map +1 -0
  168. package/dist/utils/context.js +18 -0
  169. package/dist/utils/context.js.map +1 -0
  170. package/dist/utils/credentialResolver.d.ts +22 -0
  171. package/dist/utils/credentialResolver.d.ts.map +1 -0
  172. package/dist/utils/credentialResolver.js +146 -0
  173. package/dist/utils/credentialResolver.js.map +1 -0
  174. package/dist/utils/generation.d.ts +36 -0
  175. package/dist/utils/generation.d.ts.map +1 -0
  176. package/dist/utils/generation.js +701 -0
  177. package/dist/utils/generation.js.map +1 -0
  178. package/dist/utils/host-capabilities.d.ts +27 -0
  179. package/dist/utils/host-capabilities.d.ts.map +1 -0
  180. package/dist/utils/host-capabilities.js +59 -0
  181. package/dist/utils/host-capabilities.js.map +1 -0
  182. package/dist/utils/inferSyntheticOutputSchema.d.ts +20 -0
  183. package/dist/utils/inferSyntheticOutputSchema.d.ts.map +1 -0
  184. package/dist/utils/inferSyntheticOutputSchema.js +151 -0
  185. package/dist/utils/inferSyntheticOutputSchema.js.map +1 -0
  186. package/dist/utils/outputSchema.d.ts +26 -0
  187. package/dist/utils/outputSchema.d.ts.map +1 -0
  188. package/dist/utils/outputSchema.js +297 -0
  189. package/dist/utils/outputSchema.js.map +1 -0
  190. package/dist/utils/validateAndRepair.d.ts +41 -0
  191. package/dist/utils/validateAndRepair.d.ts.map +1 -0
  192. package/dist/utils/validateAndRepair.js +483 -0
  193. package/dist/utils/validateAndRepair.js.map +1 -0
  194. package/dist/utils/workflow-prompts/actionResponse.d.ts +2 -0
  195. package/dist/utils/workflow-prompts/actionResponse.d.ts.map +1 -0
  196. package/dist/utils/workflow-prompts/actionResponse.js +17 -0
  197. package/dist/utils/workflow-prompts/actionResponse.js.map +1 -0
  198. package/dist/utils/workflow-prompts/draftIntent.d.ts +2 -0
  199. package/dist/utils/workflow-prompts/draftIntent.d.ts.map +1 -0
  200. package/dist/utils/workflow-prompts/draftIntent.js +23 -0
  201. package/dist/utils/workflow-prompts/draftIntent.js.map +1 -0
  202. package/dist/utils/workflow-prompts/feasibilityCheck.d.ts +2 -0
  203. package/dist/utils/workflow-prompts/feasibilityCheck.d.ts.map +1 -0
  204. package/dist/utils/workflow-prompts/feasibilityCheck.js +21 -0
  205. package/dist/utils/workflow-prompts/feasibilityCheck.js.map +1 -0
  206. package/dist/utils/workflow-prompts/fieldCorrection.d.ts +3 -0
  207. package/dist/utils/workflow-prompts/fieldCorrection.d.ts.map +1 -0
  208. package/dist/utils/workflow-prompts/fieldCorrection.js +20 -0
  209. package/dist/utils/workflow-prompts/fieldCorrection.js.map +1 -0
  210. package/dist/utils/workflow-prompts/index.d.ts +8 -0
  211. package/dist/utils/workflow-prompts/index.d.ts.map +1 -0
  212. package/dist/utils/workflow-prompts/index.js +8 -0
  213. package/dist/utils/workflow-prompts/index.js.map +1 -0
  214. package/dist/utils/workflow-prompts/keywordExtraction.d.ts +2 -0
  215. package/dist/utils/workflow-prompts/keywordExtraction.d.ts.map +1 -0
  216. package/dist/utils/workflow-prompts/keywordExtraction.js +21 -0
  217. package/dist/utils/workflow-prompts/keywordExtraction.js.map +1 -0
  218. package/dist/utils/workflow-prompts/parameterCorrection.d.ts +3 -0
  219. package/dist/utils/workflow-prompts/parameterCorrection.d.ts.map +1 -0
  220. package/dist/utils/workflow-prompts/parameterCorrection.js +29 -0
  221. package/dist/utils/workflow-prompts/parameterCorrection.js.map +1 -0
  222. package/dist/utils/workflow-prompts/workflowGeneration.d.ts +2 -0
  223. package/dist/utils/workflow-prompts/workflowGeneration.d.ts.map +1 -0
  224. package/dist/utils/workflow-prompts/workflowGeneration.js +529 -0
  225. package/dist/utils/workflow-prompts/workflowGeneration.js.map +1 -0
  226. package/dist/utils/workflow-prompts/workflowMatching.d.ts +2 -0
  227. package/dist/utils/workflow-prompts/workflowMatching.d.ts.map +1 -0
  228. package/dist/utils/workflow-prompts/workflowMatching.js +23 -0
  229. package/dist/utils/workflow-prompts/workflowMatching.js.map +1 -0
  230. package/dist/utils/workflow.d.ts +62 -0
  231. package/dist/utils/workflow.d.ts.map +1 -0
  232. package/dist/utils/workflow.js +712 -0
  233. package/dist/utils/workflow.js.map +1 -0
  234. package/package.json +87 -0
  235. package/src/actions/index.ts +1 -0
  236. package/src/actions/workflow.ts +494 -0
  237. package/src/data/defaultNodes.json +9887 -0
  238. package/src/data/schemaIndex.json +1 -0
  239. package/src/data/triggerSchemaIndex.json +1 -0
  240. package/src/db/index.ts +8 -0
  241. package/src/db/schema.ts +94 -0
  242. package/src/index.ts +179 -0
  243. package/src/lib/automations-builder.ts +679 -0
  244. package/src/lib/automations-types.ts +391 -0
  245. package/src/lib/index.ts +8 -0
  246. package/src/lib/legacy-task-migration.ts +143 -0
  247. package/src/lib/legacy-text-trigger-migration.ts +178 -0
  248. package/src/lib/workflow-clarification.ts +497 -0
  249. package/src/plugin-routes.ts +164 -0
  250. package/src/providers/activeWorkflows.ts +81 -0
  251. package/src/providers/index.ts +3 -0
  252. package/src/providers/pendingDraft.ts +55 -0
  253. package/src/providers/workflowStatus.ts +88 -0
  254. package/src/register-routes.ts +6 -0
  255. package/src/routes/_helpers.ts +27 -0
  256. package/src/routes/automations.ts +46 -0
  257. package/src/routes/embedded-webhooks.ts +64 -0
  258. package/src/routes/executions.ts +75 -0
  259. package/src/routes/index.ts +16 -0
  260. package/src/routes/nodes.ts +211 -0
  261. package/src/routes/validation.ts +51 -0
  262. package/src/routes/workflow-routes.ts +469 -0
  263. package/src/routes/workflows.ts +310 -0
  264. package/src/schemas/draftIntent.ts +21 -0
  265. package/src/schemas/feasibility.ts +8 -0
  266. package/src/schemas/index.ts +4 -0
  267. package/src/schemas/keywordExtraction.ts +11 -0
  268. package/src/schemas/workflowMatching.ts +29 -0
  269. package/src/services/embedded-workflow-service.ts +2224 -0
  270. package/src/services/index.ts +17 -0
  271. package/src/services/workflow-credential-store.ts +132 -0
  272. package/src/services/workflow-dispatch.ts +121 -0
  273. package/src/services/workflow-service.ts +839 -0
  274. package/src/trigger-routes.ts +714 -0
  275. package/src/types/index.ts +562 -0
  276. package/src/utils/catalog.ts +260 -0
  277. package/src/utils/clarification.ts +52 -0
  278. package/src/utils/context.ts +22 -0
  279. package/src/utils/credentialResolver.ts +234 -0
  280. package/src/utils/generation.ts +987 -0
  281. package/src/utils/host-capabilities.ts +81 -0
  282. package/src/utils/inferSyntheticOutputSchema.ts +163 -0
  283. package/src/utils/outputSchema.ts +372 -0
  284. package/src/utils/validateAndRepair.ts +610 -0
  285. package/src/utils/workflow-prompts/actionResponse.ts +16 -0
  286. package/src/utils/workflow-prompts/draftIntent.ts +22 -0
  287. package/src/utils/workflow-prompts/feasibilityCheck.ts +20 -0
  288. package/src/utils/workflow-prompts/fieldCorrection.ts +20 -0
  289. package/src/utils/workflow-prompts/index.ts +10 -0
  290. package/src/utils/workflow-prompts/keywordExtraction.ts +20 -0
  291. package/src/utils/workflow-prompts/parameterCorrection.ts +29 -0
  292. package/src/utils/workflow-prompts/workflowGeneration.ts +528 -0
  293. package/src/utils/workflow-prompts/workflowMatching.ts +22 -0
  294. package/src/utils/workflow.ts +895 -0
@@ -0,0 +1,2224 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { type IAgentRuntime, logger, Service, type Task, type UUID } from '@elizaos/core';
3
+ import { and, desc, eq, sql } from 'drizzle-orm';
4
+ import type { NodePgDatabase } from 'drizzle-orm/node-postgres';
5
+ import {
6
+ embeddedCredentials,
7
+ embeddedExecutions,
8
+ embeddedTags,
9
+ embeddedWorkflows,
10
+ } from '../db/schema';
11
+ import type {
12
+ WorkflowCredential,
13
+ WorkflowDefinition,
14
+ WorkflowDefinitionResponse,
15
+ WorkflowExecution,
16
+ WorkflowNode,
17
+ WorkflowTag,
18
+ } from '../types/index';
19
+ import { WorkflowApiError } from '../types/index';
20
+ import { detectHostCapabilities } from '../utils/host-capabilities';
21
+
22
+ export const EMBEDDED_WORKFLOW_SERVICE_TYPE = 'embedded_workflow_service';
23
+
24
+ /** TaskWorker name for scheduled workflow runs. Tasks created with this name
25
+ * carry metadata.workflowId + metadata.kind = 'workflow' and get fired by
26
+ * the core TaskService on the configured updateInterval. */
27
+ export const WORKFLOW_RUN_TASK_WORKER_NAME = 'workflow.run';
28
+
29
+ /** TaskWorker name for one-shot webhook-triggered workflow runs. A future
30
+ * webhook trigger provider creates a one-shot Task pointing at this worker;
31
+ * payload travels in metadata.payload. */
32
+ export const WORKFLOW_WEBHOOK_TASK_WORKER_NAME = 'workflow.webhook';
33
+
34
+ /** Discriminator on TaskMetadata so the UI can route workflow tasks. */
35
+ export const WORKFLOW_TASK_KIND = 'workflow';
36
+
37
+ /** Stable tag used on every workflow-backed Task so we can list+delete them. */
38
+ const WORKFLOW_TASK_TAG = 'workflow';
39
+
40
+ type WorkflowExecuteMode = WorkflowExecution['mode'];
41
+
42
+ interface INodeExecutionData {
43
+ json: Record<string, unknown>;
44
+ binary?: Record<string, unknown>;
45
+ pairedItem?: { item: number } | Array<{ item: number }>;
46
+ }
47
+
48
+ interface IExecuteFunctions {
49
+ getInputData(inputIndex?: number): INodeExecutionData[];
50
+ getNode(): WorkflowNode;
51
+ /** Agent runtime, present when the workflow runs inside an EmbeddedWorkflowService.
52
+ * Nodes that need to interact with the agent (e.g. respondToEvent injecting a
53
+ * memory into the autonomy room) read it from here. Optional because some
54
+ * nodes are pure data transforms and never touch the runtime. */
55
+ getRuntime?(): IAgentRuntime | null;
56
+ /** Identifier of the in-progress workflow execution, used by nodes that emit
57
+ * audit metadata (e.g. respondToEvent records it on the injected memory). */
58
+ getExecutionId?(): string | null;
59
+ }
60
+
61
+ interface NodeCapabilities {
62
+ requiresFs?: boolean;
63
+ requiresInbound?: boolean;
64
+ requiresLongRunning?: boolean;
65
+ requiresChildProcess?: boolean;
66
+ requiresNet?: boolean;
67
+ }
68
+
69
+ interface INodeTypeDescription {
70
+ displayName: string;
71
+ name: string;
72
+ group: string[];
73
+ version: number | number[];
74
+ description: string;
75
+ defaults: { name: string };
76
+ inputs: unknown[];
77
+ outputs: unknown[];
78
+ properties: unknown[];
79
+ capabilities?: NodeCapabilities;
80
+ }
81
+
82
+ interface INodeType {
83
+ description: INodeTypeDescription;
84
+ execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
85
+ trigger?(): Promise<unknown>;
86
+ }
87
+
88
+ interface INodeTypes {
89
+ getByName(nodeType: string): INodeType;
90
+ getByNameAndVersion(nodeType: string): INodeType;
91
+ getKnownTypes(): Record<string, { sourcePath: string; className: string }>;
92
+ }
93
+
94
+ interface StoredCredential extends WorkflowCredential {
95
+ data?: Record<string, unknown>;
96
+ }
97
+
98
+ interface StoredWorkflowRow {
99
+ workflow: WorkflowDefinition;
100
+ createdAt: string;
101
+ updatedAt: string;
102
+ versionId: string;
103
+ }
104
+
105
+ interface ExecuteOptions {
106
+ mode?: WorkflowExecuteMode;
107
+ }
108
+
109
+ interface IncomingConnection {
110
+ source: string;
111
+ sourceOutputIndex: number;
112
+ destinationInputIndex: number;
113
+ }
114
+
115
+ const EMBEDDED_HOST = 'embedded://local';
116
+ const DEFAULT_SCHEDULE_INTERVAL_MS = 60_000;
117
+
118
+ let loadedQuickJs: Promise<typeof import('quickjs-emscripten')> | null = null;
119
+
120
+ async function loadQuickJs(): Promise<typeof import('quickjs-emscripten')> {
121
+ loadedQuickJs ??= import('quickjs-emscripten');
122
+ return loadedQuickJs;
123
+ }
124
+
125
+ function cloneJson<T>(value: T): T {
126
+ return JSON.parse(JSON.stringify(value)) as T;
127
+ }
128
+
129
+ function nowIso(): string {
130
+ return new Date().toISOString();
131
+ }
132
+
133
+ function isRecord(value: unknown): value is Record<string, unknown> {
134
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
135
+ }
136
+
137
+ function normalizeWorkflowPayload(
138
+ workflow: WorkflowDefinition,
139
+ id: string,
140
+ active: boolean
141
+ ): WorkflowDefinition {
142
+ return {
143
+ ...cloneJson(workflow),
144
+ id,
145
+ active,
146
+ settings: {
147
+ executionOrder: 'v1',
148
+ ...(workflow.settings ?? {}),
149
+ },
150
+ };
151
+ }
152
+
153
+ function responseFromWorkflow(
154
+ workflow: WorkflowDefinition,
155
+ createdAt: string,
156
+ updatedAt: string,
157
+ versionId: string
158
+ ): WorkflowDefinitionResponse {
159
+ return {
160
+ ...cloneJson(workflow),
161
+ id: workflow.id ?? randomUUID(),
162
+ createdAt,
163
+ updatedAt,
164
+ versionId,
165
+ };
166
+ }
167
+
168
+ function readString(value: unknown, fallback: string): string {
169
+ return typeof value === 'string' && value.length > 0 ? value : fallback;
170
+ }
171
+
172
+ function readNumber(value: unknown, fallback: number): number {
173
+ return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
174
+ }
175
+
176
+ function resolveScheduleIntervalMs(parameters: Record<string, unknown>): number {
177
+ const explicitMs = readNumber(parameters.intervalMs, NaN);
178
+ if (Number.isFinite(explicitMs) && explicitMs > 0) return explicitMs;
179
+
180
+ const explicitSeconds = readNumber(parameters.intervalSeconds, NaN);
181
+ if (Number.isFinite(explicitSeconds) && explicitSeconds > 0) return explicitSeconds * 1000;
182
+
183
+ const rule = isRecord(parameters.rule) ? parameters.rule : null;
184
+ const intervals = Array.isArray(rule?.interval) ? rule.interval : [];
185
+ const first = intervals.find(isRecord);
186
+ if (!first) return DEFAULT_SCHEDULE_INTERVAL_MS;
187
+
188
+ const unit = readString(first.field, 'minutes');
189
+ if (unit === 'seconds') return readNumber(first.secondsInterval, 60) * 1000;
190
+ if (unit === 'minutes') return readNumber(first.minutesInterval, 1) * 60_000;
191
+ if (unit === 'hours') return readNumber(first.hoursInterval, 1) * 3_600_000;
192
+ if (unit === 'days') return readNumber(first.daysInterval, 1) * 86_400_000;
193
+
194
+ return DEFAULT_SCHEDULE_INTERVAL_MS;
195
+ }
196
+
197
+ function normalizeWebhookPath(path: unknown): string {
198
+ return readString(path, '')
199
+ .trim()
200
+ .replace(/^\/+|\/+$/g, '');
201
+ }
202
+
203
+ function normalizeHeaderEntries(value: unknown): Record<string, string> {
204
+ const headers: Record<string, string> = {};
205
+ if (isRecord(value)) {
206
+ for (const [key, headerValue] of Object.entries(value)) {
207
+ if (typeof headerValue !== 'undefined') headers[key] = String(headerValue);
208
+ }
209
+ return headers;
210
+ }
211
+
212
+ if (Array.isArray(value)) {
213
+ for (const entry of value) {
214
+ if (!isRecord(entry)) continue;
215
+ const name = readString(entry.name, '');
216
+ if (name) headers[name] = String(entry.value ?? '');
217
+ }
218
+ }
219
+ return headers;
220
+ }
221
+
222
+ function collectParametersList(value: unknown): Record<string, unknown> {
223
+ const out: Record<string, unknown> = {};
224
+ if (Array.isArray(value)) {
225
+ for (const entry of value) {
226
+ if (!isRecord(entry)) continue;
227
+ const name = readString(entry.name, '');
228
+ if (name) out[name] = entry.value ?? '';
229
+ }
230
+ }
231
+ return out;
232
+ }
233
+
234
+ function normalizeExecutionItem(
235
+ item: unknown,
236
+ pairedItem?: INodeExecutionData['pairedItem']
237
+ ): INodeExecutionData {
238
+ if (isRecord(item) && 'json' in item) {
239
+ return {
240
+ json: item.json as INodeExecutionData['json'],
241
+ ...(item.pairedItem
242
+ ? { pairedItem: item.pairedItem as INodeExecutionData['pairedItem'] }
243
+ : {}),
244
+ };
245
+ }
246
+ return {
247
+ json: (isRecord(item) ? item : { value: item }) as INodeExecutionData['json'],
248
+ ...(pairedItem ? { pairedItem } : {}),
249
+ };
250
+ }
251
+
252
+ function normalizeExecutionItems(
253
+ value: unknown,
254
+ fallback: INodeExecutionData[]
255
+ ): INodeExecutionData[] {
256
+ if (typeof value === 'undefined') return fallback.map((item) => normalizeExecutionItem(item));
257
+ if (Array.isArray(value)) {
258
+ return value.map((item, index) => normalizeExecutionItem(item, { item: index }));
259
+ }
260
+ if (isRecord(value) && Array.isArray(value.items)) {
261
+ return value.items.map((item, index) => normalizeExecutionItem(item, { item: index }));
262
+ }
263
+ return [normalizeExecutionItem(value)];
264
+ }
265
+
266
+ function readPath(source: unknown, path: string): unknown {
267
+ const parts = path
268
+ .replace(/\[(?:'([^']+)'|"([^"]+)"|(\d+))\]/g, '.$1$2$3')
269
+ .split('.')
270
+ .map((part) => part.trim())
271
+ .filter(Boolean);
272
+ let current = source;
273
+ for (const part of parts) {
274
+ if (!isRecord(current) && !Array.isArray(current)) return undefined;
275
+ current = (current as Record<string, unknown>)[part];
276
+ }
277
+ return current;
278
+ }
279
+
280
+ function resolveParameterValue(value: unknown, item: INodeExecutionData): unknown {
281
+ if (typeof value !== 'string') return value;
282
+ const trimmed = value.trim();
283
+ const expression =
284
+ trimmed.startsWith('={{') && trimmed.endsWith('}}')
285
+ ? trimmed.slice(3, -2).trim()
286
+ : trimmed.startsWith('{{') && trimmed.endsWith('}}')
287
+ ? trimmed.slice(2, -2).trim()
288
+ : trimmed.startsWith('=')
289
+ ? trimmed.slice(1).trim()
290
+ : trimmed;
291
+ const jsonPath = expression.match(/^\$json(?:\.|\[['"]?)(.+?)(?:['"]?\])?$/);
292
+ if (jsonPath?.[1]) {
293
+ return readPath(item.json, jsonPath[1]);
294
+ }
295
+ const itemJsonPath = expression.match(/^\$input\.item\.json(?:\.|\[['"]?)(.+?)(?:['"]?\])?$/);
296
+ if (itemJsonPath?.[1]) {
297
+ return readPath(item.json, itemJsonPath[1]);
298
+ }
299
+ return value;
300
+ }
301
+
302
+ function isEmptyValue(value: unknown): boolean {
303
+ return (
304
+ value === null ||
305
+ typeof value === 'undefined' ||
306
+ value === '' ||
307
+ (Array.isArray(value) && value.length === 0) ||
308
+ (isRecord(value) && Object.keys(value).length === 0)
309
+ );
310
+ }
311
+
312
+ function compareCondition(
313
+ left: unknown,
314
+ operation: string,
315
+ right: unknown,
316
+ item: INodeExecutionData
317
+ ): boolean {
318
+ const resolvedLeft = resolveParameterValue(left, item);
319
+ const resolvedRight = resolveParameterValue(right, item);
320
+ const op = operation.toLowerCase();
321
+
322
+ if (op === 'exists') return typeof resolvedLeft !== 'undefined' && resolvedLeft !== null;
323
+ if (op === 'notexists') return typeof resolvedLeft === 'undefined' || resolvedLeft === null;
324
+ if (op === 'empty') return isEmptyValue(resolvedLeft);
325
+ if (op === 'notempty') return !isEmptyValue(resolvedLeft);
326
+ if (op === 'true') return resolvedLeft === true || resolvedLeft === 'true';
327
+ if (op === 'false') return resolvedLeft === false || resolvedLeft === 'false';
328
+ if (op === 'contains') return String(resolvedLeft ?? '').includes(String(resolvedRight ?? ''));
329
+ if (op === 'notcontains')
330
+ return !String(resolvedLeft ?? '').includes(String(resolvedRight ?? ''));
331
+ if (op === 'startswith')
332
+ return String(resolvedLeft ?? '').startsWith(String(resolvedRight ?? ''));
333
+ if (op === 'endswith') return String(resolvedLeft ?? '').endsWith(String(resolvedRight ?? ''));
334
+ if (op === 'larger' || op === 'largerorequal' || op === 'gt' || op === 'gte') {
335
+ return op.includes('equal') || op === 'gte'
336
+ ? Number(resolvedLeft) >= Number(resolvedRight)
337
+ : Number(resolvedLeft) > Number(resolvedRight);
338
+ }
339
+ if (op === 'smaller' || op === 'smallerorequal' || op === 'lt' || op === 'lte') {
340
+ return op.includes('equal') || op === 'lte'
341
+ ? Number(resolvedLeft) <= Number(resolvedRight)
342
+ : Number(resolvedLeft) < Number(resolvedRight);
343
+ }
344
+ if (op === 'notequal' || op === 'notequals') return resolvedLeft !== resolvedRight;
345
+ return (
346
+ resolvedLeft === resolvedRight || String(resolvedLeft ?? '') === String(resolvedRight ?? '')
347
+ );
348
+ }
349
+
350
+ function collectConditionEntries(parameters: Record<string, unknown>): Array<{
351
+ left: unknown;
352
+ operation: string;
353
+ right: unknown;
354
+ }> {
355
+ const conditions = isRecord(parameters.conditions) ? parameters.conditions : {};
356
+ const modern = Array.isArray(conditions.conditions) ? conditions.conditions : [];
357
+ const out: Array<{ left: unknown; operation: string; right: unknown }> = [];
358
+
359
+ for (const condition of modern) {
360
+ if (!isRecord(condition)) continue;
361
+ const operator = isRecord(condition.operator) ? condition.operator : {};
362
+ out.push({
363
+ left: condition.leftValue ?? condition.value1,
364
+ operation: readString(operator.operation ?? condition.operation, 'equals'),
365
+ right: condition.rightValue ?? condition.value2,
366
+ });
367
+ }
368
+
369
+ for (const group of Object.values(conditions)) {
370
+ if (!Array.isArray(group)) continue;
371
+ for (const condition of group) {
372
+ if (!isRecord(condition)) continue;
373
+ out.push({
374
+ left: condition.value1 ?? condition.leftValue,
375
+ operation: readString(condition.operation, 'equals'),
376
+ right: condition.value2 ?? condition.rightValue,
377
+ });
378
+ }
379
+ }
380
+
381
+ return out;
382
+ }
383
+
384
+ function evaluateConditions(
385
+ parameters: Record<string, unknown>,
386
+ item: INodeExecutionData
387
+ ): boolean {
388
+ const conditions = collectConditionEntries(parameters);
389
+ if (conditions.length === 0) return true;
390
+ const combinator = readString(
391
+ isRecord(parameters.conditions) ? parameters.conditions.combinator : undefined,
392
+ 'and'
393
+ ).toLowerCase();
394
+ const results = conditions.map((condition) =>
395
+ compareCondition(condition.left, condition.operation, condition.right, item)
396
+ );
397
+ return combinator === 'or' ? results.some(Boolean) : results.every(Boolean);
398
+ }
399
+
400
+ async function parseResponseBody(response: Response): Promise<unknown> {
401
+ const text = await response.text();
402
+ if (!text) return null;
403
+ const contentType = response.headers.get('content-type') ?? '';
404
+ if (contentType.includes('application/json')) {
405
+ try {
406
+ return JSON.parse(text);
407
+ } catch {
408
+ return text;
409
+ }
410
+ }
411
+ return text;
412
+ }
413
+
414
+ function createScheduleTriggerNode(): INodeType {
415
+ return {
416
+ description: {
417
+ displayName: 'Schedule Trigger',
418
+ name: 'workflows-nodes-base.scheduleTrigger',
419
+ group: ['trigger'],
420
+ version: [1, 1.1, 1.2],
421
+ description: 'Starts the workflow on a schedule.',
422
+ defaults: { name: 'Schedule Trigger' },
423
+ inputs: [],
424
+ outputs: ['main'] as never,
425
+ properties: [],
426
+ capabilities: { requiresLongRunning: true },
427
+ },
428
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
429
+ return [
430
+ [
431
+ {
432
+ json: {
433
+ firedAt: new Date().toISOString(),
434
+ trigger: 'schedule',
435
+ },
436
+ },
437
+ ],
438
+ ];
439
+ },
440
+ async trigger() {
441
+ return {};
442
+ },
443
+ };
444
+ }
445
+
446
+ function createSetNode(): INodeType {
447
+ return {
448
+ description: {
449
+ displayName: 'Edit Fields (Set)',
450
+ name: 'workflows-nodes-base.set',
451
+ group: ['transform'],
452
+ version: [1, 2, 3, 3.1, 3.2, 3.3, 3.4],
453
+ description: 'Sets values on the current item.',
454
+ defaults: { name: 'Edit Fields' },
455
+ inputs: ['main'] as never,
456
+ outputs: ['main'] as never,
457
+ properties: [
458
+ {
459
+ displayName: 'Include Other Fields',
460
+ name: 'includeOtherFields',
461
+ type: 'boolean',
462
+ default: true,
463
+ },
464
+ {
465
+ displayName: 'Assignments',
466
+ name: 'assignments',
467
+ type: 'fixedCollection',
468
+ typeOptions: { multipleValues: true },
469
+ default: {},
470
+ options: [
471
+ {
472
+ displayName: 'Assignment',
473
+ name: 'assignments',
474
+ values: [
475
+ { displayName: 'Name', name: 'name', type: 'string', default: '' },
476
+ { displayName: 'Value', name: 'value', type: 'string', default: '' },
477
+ ],
478
+ },
479
+ ],
480
+ },
481
+ {
482
+ displayName: 'Values',
483
+ name: 'values',
484
+ type: 'json',
485
+ default: {},
486
+ },
487
+ {
488
+ displayName: 'Fields',
489
+ name: 'fields',
490
+ type: 'json',
491
+ default: {},
492
+ },
493
+ ] as never,
494
+ },
495
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
496
+ const inputItems = this.getInputData();
497
+ const sourceItems = inputItems.length > 0 ? inputItems : [{ json: {} }];
498
+ const output: INodeExecutionData[] = [];
499
+ const nodeParameters = this.getNode().parameters as Record<string, unknown>;
500
+
501
+ for (let itemIndex = 0; itemIndex < sourceItems.length; itemIndex++) {
502
+ const includeOtherFields = nodeParameters.includeOtherFields !== false;
503
+ const base: Record<string, unknown> = includeOtherFields
504
+ ? { ...(sourceItems[itemIndex]?.json ?? {}) }
505
+ : {};
506
+
507
+ const assignmentContainer = isRecord(nodeParameters.assignments)
508
+ ? nodeParameters.assignments
509
+ : {};
510
+ const assignments = Array.isArray(assignmentContainer.assignments)
511
+ ? (assignmentContainer.assignments as Array<{ name?: unknown; value?: unknown }>)
512
+ : [];
513
+ for (const assignment of assignments) {
514
+ const name = readString(assignment.name, '');
515
+ if (name) base[name] = assignment.value;
516
+ }
517
+
518
+ const values = isRecord(nodeParameters.values) ? nodeParameters.values : {};
519
+ for (const group of Object.values(values)) {
520
+ if (!Array.isArray(group)) continue;
521
+ for (const entry of group) {
522
+ if (!isRecord(entry)) continue;
523
+ const name = readString(entry.name, '');
524
+ if (name) base[name] = entry.value;
525
+ }
526
+ }
527
+
528
+ const fields = isRecord(nodeParameters.fields) ? nodeParameters.fields : {};
529
+ if (isRecord(fields)) {
530
+ Object.assign(base, fields);
531
+ }
532
+
533
+ output.push({
534
+ json: base as INodeExecutionData['json'],
535
+ pairedItem: { item: itemIndex },
536
+ });
537
+ }
538
+
539
+ return [output];
540
+ },
541
+ };
542
+ }
543
+
544
+ function createHttpRequestNode(): INodeType {
545
+ return {
546
+ description: {
547
+ displayName: 'HTTP Request',
548
+ name: 'workflows-nodes-base.httpRequest',
549
+ group: ['output'],
550
+ version: [1, 2, 3, 4, 4.1, 4.2],
551
+ description: 'Makes an HTTP request.',
552
+ defaults: { name: 'HTTP Request' },
553
+ inputs: ['main'] as never,
554
+ outputs: ['main'] as never,
555
+ properties: [
556
+ {
557
+ displayName: 'Method',
558
+ name: 'method',
559
+ type: 'options',
560
+ default: 'GET',
561
+ options: ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD'].map((method) => ({
562
+ name: method,
563
+ value: method,
564
+ })),
565
+ },
566
+ {
567
+ displayName: 'URL',
568
+ name: 'url',
569
+ type: 'string',
570
+ default: '',
571
+ },
572
+ {
573
+ displayName: 'Headers',
574
+ name: 'headers',
575
+ type: 'json',
576
+ default: {},
577
+ },
578
+ {
579
+ displayName: 'Header Parameters',
580
+ name: 'headerParameters',
581
+ type: 'fixedCollection',
582
+ typeOptions: { multipleValues: true },
583
+ default: {},
584
+ options: [
585
+ {
586
+ displayName: 'Parameter',
587
+ name: 'parameters',
588
+ values: [
589
+ { displayName: 'Name', name: 'name', type: 'string', default: '' },
590
+ { displayName: 'Value', name: 'value', type: 'string', default: '' },
591
+ ],
592
+ },
593
+ ],
594
+ },
595
+ {
596
+ displayName: 'Body',
597
+ name: 'body',
598
+ type: 'string',
599
+ default: '',
600
+ },
601
+ {
602
+ displayName: 'JSON Body',
603
+ name: 'jsonBody',
604
+ type: 'json',
605
+ default: {},
606
+ },
607
+ {
608
+ displayName: 'Body Parameters',
609
+ name: 'bodyParameters',
610
+ type: 'fixedCollection',
611
+ typeOptions: { multipleValues: true },
612
+ default: {},
613
+ options: [
614
+ {
615
+ displayName: 'Parameter',
616
+ name: 'parameters',
617
+ values: [
618
+ { displayName: 'Name', name: 'name', type: 'string', default: '' },
619
+ { displayName: 'Value', name: 'value', type: 'string', default: '' },
620
+ ],
621
+ },
622
+ ],
623
+ },
624
+ ] as never,
625
+ },
626
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
627
+ const inputItems = this.getInputData();
628
+ const sourceItems = inputItems.length > 0 ? inputItems : [{ json: {} }];
629
+ const output: INodeExecutionData[] = [];
630
+ const nodeParameters = this.getNode().parameters as Record<string, unknown>;
631
+
632
+ for (let itemIndex = 0; itemIndex < sourceItems.length; itemIndex++) {
633
+ const url = readString(nodeParameters.url, '');
634
+ if (!url) {
635
+ throw new Error(
636
+ `HTTP Request node requires a url parameter; got ${JSON.stringify(nodeParameters)}`
637
+ );
638
+ }
639
+
640
+ const method = readString(nodeParameters.method, 'GET').toUpperCase().trim();
641
+
642
+ const headerContainer = isRecord(nodeParameters.headerParameters)
643
+ ? nodeParameters.headerParameters
644
+ : {};
645
+ const headerParameters = headerContainer.parameters ?? [];
646
+ const headers = {
647
+ ...normalizeHeaderEntries(nodeParameters.headers),
648
+ ...normalizeHeaderEntries(headerParameters),
649
+ };
650
+
651
+ const requestOptions: RequestInit = { method, headers };
652
+ const bodyContainer = isRecord(nodeParameters.bodyParameters)
653
+ ? nodeParameters.bodyParameters
654
+ : {};
655
+ const bodyParameters = bodyContainer.parameters ?? [];
656
+ const bodyObject = collectParametersList(bodyParameters);
657
+ const jsonBody = nodeParameters.jsonBody;
658
+ const rawBody = nodeParameters.body;
659
+
660
+ if (!['GET', 'HEAD'].includes(method)) {
661
+ if (typeof rawBody === 'string' && rawBody.length > 0) {
662
+ requestOptions.body = rawBody;
663
+ } else if (isRecord(jsonBody) || Object.keys(bodyObject).length > 0) {
664
+ requestOptions.body = JSON.stringify(isRecord(jsonBody) ? jsonBody : bodyObject);
665
+ headers['content-type'] = headers['content-type'] ?? 'application/json';
666
+ }
667
+ }
668
+
669
+ const response = await fetch(url, requestOptions);
670
+ const body = await parseResponseBody(response);
671
+ output.push({
672
+ json: {
673
+ statusCode: response.status,
674
+ headers: Object.fromEntries(response.headers.entries()),
675
+ body,
676
+ } as INodeExecutionData['json'],
677
+ pairedItem: { item: itemIndex },
678
+ });
679
+ }
680
+
681
+ return [output];
682
+ },
683
+ };
684
+ }
685
+
686
+ function createManualTriggerNode(): INodeType {
687
+ return {
688
+ description: {
689
+ displayName: 'Manual Trigger',
690
+ name: 'workflows-nodes-base.manualTrigger',
691
+ group: ['trigger'],
692
+ version: [1],
693
+ description: 'Starts the workflow manually.',
694
+ defaults: { name: 'Manual Trigger' },
695
+ inputs: [],
696
+ outputs: ['main'] as never,
697
+ properties: [],
698
+ },
699
+ async execute(): Promise<INodeExecutionData[][]> {
700
+ return [[{ json: { firedAt: new Date().toISOString(), trigger: 'manual' } }]];
701
+ },
702
+ async trigger() {
703
+ return {};
704
+ },
705
+ };
706
+ }
707
+
708
+ function createWebhookNode(): INodeType {
709
+ return {
710
+ description: {
711
+ displayName: 'Webhook',
712
+ name: 'workflows-nodes-base.webhook',
713
+ group: ['trigger'],
714
+ version: [1, 2],
715
+ description: 'Starts the workflow from an HTTP webhook.',
716
+ defaults: { name: 'Webhook' },
717
+ inputs: [],
718
+ outputs: ['main'] as never,
719
+ properties: [
720
+ { displayName: 'Path', name: 'path', type: 'string', default: '' },
721
+ { displayName: 'HTTP Method', name: 'httpMethod', type: 'string', default: 'POST' },
722
+ { displayName: 'Embedded Payload', name: '__embeddedPayload', type: 'json', default: {} },
723
+ ] as never,
724
+ capabilities: { requiresInbound: true },
725
+ },
726
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
727
+ const parameters = this.getNode().parameters as Record<string, unknown>;
728
+ const payload = isRecord(parameters.__embeddedPayload)
729
+ ? parameters.__embeddedPayload
730
+ : { firedAt: new Date().toISOString(), trigger: 'webhook' };
731
+ return [[{ json: cloneJson(payload) as INodeExecutionData['json'] }]];
732
+ },
733
+ async trigger() {
734
+ return {};
735
+ },
736
+ };
737
+ }
738
+
739
+ function createRespondToWebhookNode(): INodeType {
740
+ return {
741
+ description: {
742
+ displayName: 'Respond to Webhook',
743
+ name: 'workflows-nodes-base.respondToWebhook',
744
+ group: ['output'],
745
+ version: [1],
746
+ description: 'Returns the current item as a webhook response.',
747
+ defaults: { name: 'Respond to Webhook' },
748
+ inputs: ['main'] as never,
749
+ outputs: ['main'] as never,
750
+ properties: [
751
+ { displayName: 'Response Body', name: 'responseBody', type: 'json', default: {} },
752
+ ] as never,
753
+ capabilities: { requiresInbound: true },
754
+ },
755
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
756
+ const inputItems = this.getInputData();
757
+ const parameters = this.getNode().parameters as Record<string, unknown>;
758
+ if (isRecord(parameters.responseBody) && Object.keys(parameters.responseBody).length > 0) {
759
+ return [[{ json: cloneJson(parameters.responseBody) as INodeExecutionData['json'] }]];
760
+ }
761
+ return [inputItems.length > 0 ? inputItems : [{ json: {} }]];
762
+ },
763
+ };
764
+ }
765
+
766
+ function createNoOpNode(): INodeType {
767
+ return {
768
+ description: {
769
+ displayName: 'No Operation, do nothing',
770
+ name: 'workflows-nodes-base.noOp',
771
+ group: ['transform'],
772
+ version: [1],
773
+ description: 'Passes input data through unchanged.',
774
+ defaults: { name: 'NoOp' },
775
+ inputs: ['main'] as never,
776
+ outputs: ['main'] as never,
777
+ properties: [],
778
+ },
779
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
780
+ const inputItems = this.getInputData();
781
+ return [inputItems.length > 0 ? inputItems : [{ json: {} }]];
782
+ },
783
+ };
784
+ }
785
+
786
+ function createIfNode(): INodeType {
787
+ return {
788
+ description: {
789
+ displayName: 'If',
790
+ name: 'workflows-nodes-base.if',
791
+ group: ['transform'],
792
+ version: [1, 2],
793
+ description: 'Routes items based on conditions.',
794
+ defaults: { name: 'If' },
795
+ inputs: ['main'] as never,
796
+ outputs: ['main', 'main'] as never,
797
+ properties: [
798
+ { displayName: 'Conditions', name: 'conditions', type: 'fixedCollection', default: {} },
799
+ ] as never,
800
+ },
801
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
802
+ const parameters = this.getNode().parameters as Record<string, unknown>;
803
+ const inputItems = this.getInputData();
804
+ const trueItems: INodeExecutionData[] = [];
805
+ const falseItems: INodeExecutionData[] = [];
806
+ inputItems.forEach((item, index) => {
807
+ const out = evaluateConditions(parameters, item) ? trueItems : falseItems;
808
+ out.push({ ...item, pairedItem: item.pairedItem ?? { item: index } });
809
+ });
810
+ return [trueItems, falseItems];
811
+ },
812
+ };
813
+ }
814
+
815
+ function createFilterNode(): INodeType {
816
+ return {
817
+ description: {
818
+ displayName: 'Filter',
819
+ name: 'workflows-nodes-base.filter',
820
+ group: ['transform'],
821
+ version: [1, 2],
822
+ description: 'Keeps items that match conditions.',
823
+ defaults: { name: 'Filter' },
824
+ inputs: ['main'] as never,
825
+ outputs: ['main'] as never,
826
+ properties: [
827
+ { displayName: 'Conditions', name: 'conditions', type: 'fixedCollection', default: {} },
828
+ ] as never,
829
+ },
830
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
831
+ const parameters = this.getNode().parameters as Record<string, unknown>;
832
+ return [this.getInputData().filter((item) => evaluateConditions(parameters, item))];
833
+ },
834
+ };
835
+ }
836
+
837
+ function createSwitchNode(): INodeType {
838
+ return {
839
+ description: {
840
+ displayName: 'Switch',
841
+ name: 'workflows-nodes-base.switch',
842
+ group: ['transform'],
843
+ version: [1, 2, 3],
844
+ description: 'Routes items to multiple outputs.',
845
+ defaults: { name: 'Switch' },
846
+ inputs: ['main'] as never,
847
+ outputs: ['main', 'main', 'main', 'main', 'main'] as never,
848
+ properties: [
849
+ { displayName: 'Rules', name: 'rules', type: 'fixedCollection', default: {} },
850
+ ] as never,
851
+ },
852
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
853
+ const parameters = this.getNode().parameters as Record<string, unknown>;
854
+ const rulesContainer = isRecord(parameters.rules) ? parameters.rules : {};
855
+ const rules = Array.isArray(rulesContainer.rules) ? rulesContainer.rules : [];
856
+ const outputs: INodeExecutionData[][] = [[], [], [], [], []];
857
+ this.getInputData().forEach((item, itemIndex) => {
858
+ const matchedIndex = rules.findIndex((rule) =>
859
+ isRecord(rule) ? evaluateConditions({ conditions: rule.conditions ?? rule }, item) : false
860
+ );
861
+ const outputIndex = matchedIndex >= 0 ? Math.min(matchedIndex, 3) : 4;
862
+ outputs[outputIndex].push({ ...item, pairedItem: item.pairedItem ?? { item: itemIndex } });
863
+ });
864
+ return outputs;
865
+ },
866
+ };
867
+ }
868
+
869
+ function createMergeNode(): INodeType {
870
+ return {
871
+ description: {
872
+ displayName: 'Merge',
873
+ name: 'workflows-nodes-base.merge',
874
+ group: ['transform'],
875
+ version: [1, 2, 3],
876
+ description: 'Combines items from multiple inputs.',
877
+ defaults: { name: 'Merge' },
878
+ inputs: ['main', 'main'] as never,
879
+ outputs: ['main'] as never,
880
+ properties: [
881
+ { displayName: 'Mode', name: 'mode', type: 'string', default: 'append' },
882
+ ] as never,
883
+ },
884
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
885
+ const first = this.getInputData(0) ?? [];
886
+ const second = this.getInputData(1) ?? [];
887
+ return [[...first, ...second]];
888
+ },
889
+ };
890
+ }
891
+
892
+ function createSplitInBatchesNode(): INodeType {
893
+ return {
894
+ description: {
895
+ displayName: 'Split In Batches',
896
+ name: 'workflows-nodes-base.splitInBatches',
897
+ group: ['transform'],
898
+ version: [1, 2, 3],
899
+ description: 'Emits the next batch of items.',
900
+ defaults: { name: 'Split In Batches' },
901
+ inputs: ['main'] as never,
902
+ outputs: ['main', 'main'] as never,
903
+ properties: [
904
+ { displayName: 'Batch Size', name: 'batchSize', type: 'number', default: 1 },
905
+ ] as never,
906
+ },
907
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
908
+ const inputItems = this.getInputData();
909
+ const batchSize = Math.max(
910
+ 1,
911
+ readNumber(this.getNode().parameters.batchSize, inputItems.length)
912
+ );
913
+ return [inputItems.slice(0, batchSize), inputItems.slice(batchSize)];
914
+ },
915
+ };
916
+ }
917
+
918
+ function createWaitNode(): INodeType {
919
+ return {
920
+ description: {
921
+ displayName: 'Wait',
922
+ name: 'workflows-nodes-base.wait',
923
+ group: ['transform'],
924
+ version: [1, 1.1],
925
+ description: 'Pauses execution for a duration.',
926
+ defaults: { name: 'Wait' },
927
+ inputs: ['main'] as never,
928
+ outputs: ['main'] as never,
929
+ properties: [
930
+ { displayName: 'Amount', name: 'amount', type: 'number', default: 1 },
931
+ { displayName: 'Unit', name: 'unit', type: 'string', default: 'seconds' },
932
+ ] as never,
933
+ },
934
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
935
+ const parameters = this.getNode().parameters as Record<string, unknown>;
936
+ const amount = Math.max(0, readNumber(parameters.amount, 1));
937
+ const unit = readString(parameters.unit, 'seconds');
938
+ const multiplier =
939
+ unit === 'milliseconds'
940
+ ? 1
941
+ : unit === 'minutes'
942
+ ? 60_000
943
+ : unit === 'hours'
944
+ ? 3_600_000
945
+ : 1000;
946
+ await new Promise((resolve) => setTimeout(resolve, amount * multiplier));
947
+ return [this.getInputData()];
948
+ },
949
+ };
950
+ }
951
+
952
+ function createDateTimeNode(): INodeType {
953
+ return {
954
+ description: {
955
+ displayName: 'Date & Time',
956
+ name: 'workflows-nodes-base.dateTime',
957
+ group: ['transform'],
958
+ version: [1, 2],
959
+ description: 'Adds date/time values to items.',
960
+ defaults: { name: 'Date & Time' },
961
+ inputs: ['main'] as never,
962
+ outputs: ['main'] as never,
963
+ properties: [
964
+ { displayName: 'Field Name', name: 'fieldName', type: 'string', default: 'dateTime' },
965
+ ] as never,
966
+ },
967
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
968
+ const inputItems = this.getInputData();
969
+ const fieldName = readString(this.getNode().parameters.fieldName, 'dateTime');
970
+ const now = new Date().toISOString();
971
+ return [
972
+ inputItems.map((item, index) => ({
973
+ json: { ...(item.json ?? {}), [fieldName]: now } as INodeExecutionData['json'],
974
+ pairedItem: item.pairedItem ?? { item: index },
975
+ })),
976
+ ];
977
+ },
978
+ };
979
+ }
980
+
981
+ function createCryptoNode(): INodeType {
982
+ return {
983
+ description: {
984
+ displayName: 'Crypto',
985
+ name: 'workflows-nodes-base.crypto',
986
+ group: ['transform'],
987
+ version: [1],
988
+ description: 'Hashes data.',
989
+ defaults: { name: 'Crypto' },
990
+ inputs: ['main'] as never,
991
+ outputs: ['main'] as never,
992
+ properties: [
993
+ { displayName: 'Value', name: 'value', type: 'string', default: '' },
994
+ { displayName: 'Algorithm', name: 'algorithm', type: 'string', default: 'sha256' },
995
+ { displayName: 'Field Name', name: 'fieldName', type: 'string', default: 'hash' },
996
+ ] as never,
997
+ },
998
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
999
+ const parameters = this.getNode().parameters as Record<string, unknown>;
1000
+ const algorithm = readString(parameters.algorithm, 'sha256');
1001
+ const fieldName = readString(parameters.fieldName, 'hash');
1002
+ return [
1003
+ this.getInputData().map((item, index) => {
1004
+ const raw = resolveParameterValue(parameters.value, item);
1005
+ const source =
1006
+ raw === '' || typeof raw === 'undefined' ? JSON.stringify(item.json) : String(raw);
1007
+ return {
1008
+ json: {
1009
+ ...(item.json ?? {}),
1010
+ [fieldName]: createHash(algorithm).update(source).digest('hex'),
1011
+ } as INodeExecutionData['json'],
1012
+ pairedItem: item.pairedItem ?? { item: index },
1013
+ };
1014
+ }),
1015
+ ];
1016
+ },
1017
+ };
1018
+ }
1019
+
1020
+ function createItemListsNode(): INodeType {
1021
+ return {
1022
+ description: {
1023
+ displayName: 'Item Lists',
1024
+ name: 'workflows-nodes-base.itemLists',
1025
+ group: ['transform'],
1026
+ version: [1, 2, 3],
1027
+ description: 'Transforms item lists.',
1028
+ defaults: { name: 'Item Lists' },
1029
+ inputs: ['main'] as never,
1030
+ outputs: ['main'] as never,
1031
+ properties: [
1032
+ { displayName: 'Operation', name: 'operation', type: 'string', default: 'passthrough' },
1033
+ { displayName: 'Limit', name: 'limit', type: 'number', default: 0 },
1034
+ ] as never,
1035
+ },
1036
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
1037
+ const parameters = this.getNode().parameters as Record<string, unknown>;
1038
+ const inputItems = this.getInputData();
1039
+ const operation = readString(parameters.operation, 'passthrough');
1040
+ if (operation === 'limit') {
1041
+ const limit = Math.max(0, readNumber(parameters.limit, inputItems.length));
1042
+ return [inputItems.slice(0, limit)];
1043
+ }
1044
+ return [inputItems];
1045
+ },
1046
+ };
1047
+ }
1048
+
1049
+ async function runQuickJsCode(jsCode: string, inputItems: INodeExecutionData[]): Promise<unknown> {
1050
+ const { getQuickJS, shouldInterruptAfterDeadline } = await loadQuickJs();
1051
+ const QuickJS = await getQuickJS();
1052
+ const embeddedInput = JSON.stringify(inputItems.map((item) => normalizeExecutionItem(item)));
1053
+ const source = `
1054
+ "use strict";
1055
+ const $input = ${embeddedInput};
1056
+ const items = $input;
1057
+ const item = $input[0] ?? { json: {} };
1058
+ const $json = item.json ?? {};
1059
+ const $now = new Date("${new Date().toISOString()}");
1060
+ const $workflow = {};
1061
+ const $env = {};
1062
+ const console = { log() {}, warn() {}, error() {}, info() {} };
1063
+ (function embeddedWorkflowCodeNode() {
1064
+ ${jsCode}
1065
+ })()
1066
+ `;
1067
+ return QuickJS.evalCode(source, {
1068
+ shouldInterrupt: shouldInterruptAfterDeadline(Date.now() + 5_000),
1069
+ memoryLimitBytes: 32 * 1024 * 1024,
1070
+ });
1071
+ }
1072
+
1073
+ type AutonomyServiceLike = Service & {
1074
+ getAutonomousRoomId?(): UUID | undefined;
1075
+ getTargetRoomId?(): UUID | undefined;
1076
+ };
1077
+
1078
+ function resolveAutonomyService(runtime: IAgentRuntime): AutonomyServiceLike | null {
1079
+ const svc =
1080
+ runtime.getService<AutonomyServiceLike>('AUTONOMY') ??
1081
+ runtime.getService<AutonomyServiceLike>('autonomy');
1082
+ return svc ?? null;
1083
+ }
1084
+
1085
+ function resolveAutonomyRoomId(svc: AutonomyServiceLike): UUID | null {
1086
+ const fromAutonomous =
1087
+ typeof svc.getAutonomousRoomId === 'function' ? svc.getAutonomousRoomId() : undefined;
1088
+ if (fromAutonomous) return fromAutonomous;
1089
+ const fromTarget = typeof svc.getTargetRoomId === 'function' ? svc.getTargetRoomId() : undefined;
1090
+ return fromTarget ?? null;
1091
+ }
1092
+
1093
+ function extractEventFromInputItems(inputItems: INodeExecutionData[]): {
1094
+ kind?: string;
1095
+ payload?: Record<string, unknown>;
1096
+ } | null {
1097
+ for (const item of inputItems) {
1098
+ const json = item.json;
1099
+ if (!isRecord(json)) continue;
1100
+ const kind = typeof json.eventKind === 'string' ? json.eventKind : undefined;
1101
+ const payload = isRecord(json.eventPayload) ? json.eventPayload : undefined;
1102
+ if (kind || payload) return { kind, payload };
1103
+ }
1104
+ return null;
1105
+ }
1106
+
1107
+ function createRespondToEventNode(): INodeType {
1108
+ return {
1109
+ description: {
1110
+ displayName: 'Respond to Event',
1111
+ name: 'workflows-nodes-base.respondToEvent',
1112
+ group: ['transform'],
1113
+ version: [1],
1114
+ description: "Inject an instruction into the agent's autonomy room.",
1115
+ defaults: { name: 'Respond to Event' },
1116
+ inputs: ['main'] as never,
1117
+ outputs: ['main'] as never,
1118
+ properties: [
1119
+ { displayName: 'Instructions', name: 'instructions', type: 'string', default: '' },
1120
+ { displayName: 'Display Name', name: 'displayName', type: 'string', default: '' },
1121
+ { displayName: 'Wake Mode', name: 'wakeMode', type: 'string', default: 'inject_now' },
1122
+ ] as never,
1123
+ },
1124
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
1125
+ const node = this.getNode();
1126
+ const inputItems = this.getInputData();
1127
+ const parameters = node.parameters as Record<string, unknown>;
1128
+ const instructions = readString(parameters.instructions, '');
1129
+ const displayName = readString(parameters.displayName, node.name);
1130
+ const wakeMode = readString(parameters.wakeMode, 'inject_now');
1131
+ const runtime = this.getRuntime?.() ?? null;
1132
+ const executionId = this.getExecutionId?.() ?? null;
1133
+
1134
+ const failure = (reason: string): INodeExecutionData[][] => [
1135
+ [
1136
+ {
1137
+ json: {
1138
+ instructionInjected: false,
1139
+ reason,
1140
+ nodeName: node.name,
1141
+ } as INodeExecutionData['json'],
1142
+ },
1143
+ ],
1144
+ ];
1145
+
1146
+ if (!runtime) {
1147
+ logger.warn(
1148
+ { src: 'plugin:workflow:respondToEvent', nodeName: node.name },
1149
+ '[respondToEvent] No agent runtime available in execution context — skipping injection'
1150
+ );
1151
+ return failure('runtime_unavailable');
1152
+ }
1153
+
1154
+ const autonomyService = resolveAutonomyService(runtime);
1155
+ if (!autonomyService) {
1156
+ runtime.logger?.warn?.(
1157
+ { src: 'plugin:workflow:respondToEvent', nodeName: node.name, executionId },
1158
+ '[respondToEvent] Autonomy service not registered — skipping injection'
1159
+ );
1160
+ return failure('autonomy_service_unavailable');
1161
+ }
1162
+
1163
+ const roomId = resolveAutonomyRoomId(autonomyService);
1164
+ if (!roomId) {
1165
+ runtime.logger?.warn?.(
1166
+ { src: 'plugin:workflow:respondToEvent', nodeName: node.name, executionId },
1167
+ '[respondToEvent] No autonomy room resolvable — skipping injection'
1168
+ );
1169
+ return failure('no_autonomy_room');
1170
+ }
1171
+
1172
+ const event = extractEventFromInputItems(inputItems);
1173
+ const eventText = event
1174
+ ? `\n\nEvent: ${event.kind ?? 'unknown'}\nPayload: ${JSON.stringify(event.payload ?? {})}`
1175
+ : '';
1176
+ const instructionText = `[${displayName}]\n${instructions}${eventText}`;
1177
+
1178
+ await runtime.createMemory(
1179
+ {
1180
+ entityId: runtime.agentId,
1181
+ roomId,
1182
+ content: {
1183
+ text: instructionText,
1184
+ source: 'workflow:respondToEvent',
1185
+ metadata: {
1186
+ workflowExecutionId: executionId,
1187
+ nodeName: node.name,
1188
+ wakeMode,
1189
+ isAutonomousInstruction: true,
1190
+ },
1191
+ },
1192
+ },
1193
+ 'messages'
1194
+ );
1195
+
1196
+ return [
1197
+ [
1198
+ {
1199
+ json: {
1200
+ instructionInjected: true,
1201
+ roomId,
1202
+ nodeName: node.name,
1203
+ wakeMode,
1204
+ } as INodeExecutionData['json'],
1205
+ },
1206
+ ],
1207
+ ];
1208
+ },
1209
+ };
1210
+ }
1211
+
1212
+ function createCodeNode(): INodeType {
1213
+ return {
1214
+ description: {
1215
+ displayName: 'Code',
1216
+ name: 'workflows-nodes-base.code',
1217
+ group: ['transform'],
1218
+ version: [1, 2],
1219
+ description: 'Runs JavaScript in a QuickJS sandbox.',
1220
+ defaults: { name: 'Code' },
1221
+ inputs: ['main'] as never,
1222
+ outputs: ['main'] as never,
1223
+ properties: [
1224
+ {
1225
+ displayName: 'JavaScript Code',
1226
+ name: 'jsCode',
1227
+ type: 'string',
1228
+ default: 'return items;',
1229
+ },
1230
+ { displayName: 'Mode', name: 'mode', type: 'string', default: 'runOnceForAllItems' },
1231
+ ] as never,
1232
+ },
1233
+ async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
1234
+ const inputItems = this.getInputData();
1235
+ const sourceItems = inputItems.length > 0 ? inputItems : [{ json: {} }];
1236
+ const parameters = this.getNode().parameters as Record<string, unknown>;
1237
+ const jsCode = readString(parameters.jsCode, 'return items;');
1238
+ const mode = readString(parameters.mode, 'runOnceForAllItems');
1239
+ if (mode === 'runOnceForEachItem') {
1240
+ const out: INodeExecutionData[] = [];
1241
+ for (const item of sourceItems) {
1242
+ const result = await runQuickJsCode(jsCode, [item]);
1243
+ out.push(...normalizeExecutionItems(result, [item]));
1244
+ }
1245
+ return [out];
1246
+ }
1247
+ const result = await runQuickJsCode(jsCode, sourceItems);
1248
+ return [normalizeExecutionItems(result, sourceItems)];
1249
+ },
1250
+ };
1251
+ }
1252
+
1253
+ class EmbeddedNodeTypes implements INodeTypes {
1254
+ private readonly nodes = new Map<string, INodeType>();
1255
+
1256
+ constructor() {
1257
+ for (const node of [
1258
+ createScheduleTriggerNode(),
1259
+ createManualTriggerNode(),
1260
+ createWebhookNode(),
1261
+ createRespondToWebhookNode(),
1262
+ createRespondToEventNode(),
1263
+ createSetNode(),
1264
+ createHttpRequestNode(),
1265
+ createNoOpNode(),
1266
+ createIfNode(),
1267
+ createFilterNode(),
1268
+ createSwitchNode(),
1269
+ createMergeNode(),
1270
+ createSplitInBatchesNode(),
1271
+ createWaitNode(),
1272
+ createDateTimeNode(),
1273
+ createCryptoNode(),
1274
+ createItemListsNode(),
1275
+ createCodeNode(),
1276
+ ]) {
1277
+ const canonical = node.description.name;
1278
+ this.nodes.set(canonical, node);
1279
+ }
1280
+ }
1281
+
1282
+ getByName(nodeType: string): INodeType {
1283
+ return this.getByNameAndVersion(nodeType);
1284
+ }
1285
+
1286
+ getByNameAndVersion(nodeType: string): INodeType {
1287
+ const node = this.nodes.get(nodeType);
1288
+ if (!node) {
1289
+ throw new Error(`Node type not available in embedded workflow runtime: ${nodeType}`);
1290
+ }
1291
+ return node;
1292
+ }
1293
+
1294
+ getKnownTypes(): Record<string, { sourcePath: string; className: string }> {
1295
+ return Object.fromEntries(
1296
+ [...this.nodes.keys()].map((name) => [
1297
+ name,
1298
+ { sourcePath: 'embedded', className: name.split('.').at(-1) ?? name },
1299
+ ])
1300
+ );
1301
+ }
1302
+
1303
+ has(nodeType: string): boolean {
1304
+ return this.nodes.has(nodeType);
1305
+ }
1306
+
1307
+ names(): string[] {
1308
+ return [...this.nodes.keys()];
1309
+ }
1310
+
1311
+ versions(): Map<string, number[]> {
1312
+ const out = new Map<string, number[]>();
1313
+ for (const [name, node] of this.nodes) {
1314
+ const version = node.description.version;
1315
+ out.set(name, Array.isArray(version) ? version : [version]);
1316
+ }
1317
+ return out;
1318
+ }
1319
+ }
1320
+
1321
+ export class EmbeddedWorkflowService extends Service {
1322
+ static override readonly serviceType = EMBEDDED_WORKFLOW_SERVICE_TYPE;
1323
+
1324
+ override capabilityDescription =
1325
+ 'Feature-flagged embedded workflow runtime for local plugin-owned workflow execution.';
1326
+
1327
+ private readonly nodeTypes = new EmbeddedNodeTypes();
1328
+ private readonly hostCapabilities = detectHostCapabilities();
1329
+ private schemaReady: Promise<void> | null = null;
1330
+
1331
+ static async start(runtime: IAgentRuntime): Promise<EmbeddedWorkflowService> {
1332
+ const service = new EmbeddedWorkflowService(runtime);
1333
+ logger.info(
1334
+ { src: 'plugin:workflow:embedded' },
1335
+ 'Embedded workflow service registered (lazy runtime load)'
1336
+ );
1337
+ service.registerTaskWorkers();
1338
+ if (runtime.db) {
1339
+ await service.ensureSchema();
1340
+ await service.rehydrateSchedules();
1341
+ }
1342
+ return service;
1343
+ }
1344
+
1345
+ override async stop(): Promise<void> {
1346
+ // Scheduling lives in core's TaskService. Tasks persist across restart;
1347
+ // there is nothing in-process to tear down here.
1348
+ }
1349
+
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
+ get host(): string {
1393
+ return EMBEDDED_HOST;
1394
+ }
1395
+
1396
+ getRuntimeNodeTypeVersions(): Map<string, number[]> {
1397
+ return this.nodeTypes.versions();
1398
+ }
1399
+
1400
+ getRegisteredNodeTypes(): string[] {
1401
+ return this.nodeTypes.names();
1402
+ }
1403
+
1404
+ supportsWorkflow(workflow: WorkflowDefinition): { supported: boolean; missing: string[] } {
1405
+ const missing = workflow.nodes
1406
+ .filter((node) => !node.disabled && !this.nodeTypes.has(node.type))
1407
+ .map((node) => node.type);
1408
+ return { supported: missing.length === 0, missing: [...new Set(missing)] };
1409
+ }
1410
+
1411
+ private getDb(): NodePgDatabase {
1412
+ const db = this.runtime.db;
1413
+ if (!db) {
1414
+ throw new Error(
1415
+ 'Database not available for EmbeddedWorkflowService. Embedded workflow requires plugin-sql/PGlite/Postgres persistence.'
1416
+ );
1417
+ }
1418
+ return db as NodePgDatabase;
1419
+ }
1420
+
1421
+ private async ensureSchema(): Promise<void> {
1422
+ if (!this.schemaReady) {
1423
+ this.schemaReady = (async () => {
1424
+ const db = this.getDb();
1425
+ await db.execute(sql`CREATE SCHEMA IF NOT EXISTS "workflow"`);
1426
+ await db.execute(sql`
1427
+ CREATE TABLE IF NOT EXISTS "workflow"."credential_mappings" (
1428
+ "id" uuid PRIMARY KEY DEFAULT gen_random_uuid(),
1429
+ "user_id" text NOT NULL,
1430
+ "cred_type" text NOT NULL,
1431
+ "workflow_credential_id" text NOT NULL,
1432
+ "created_at" timestamp DEFAULT now() NOT NULL,
1433
+ "updated_at" timestamp DEFAULT now() NOT NULL
1434
+ )
1435
+ `);
1436
+ await db.execute(sql`
1437
+ CREATE UNIQUE INDEX IF NOT EXISTS "idx_user_cred"
1438
+ ON "workflow"."credential_mappings" ("user_id", "cred_type")
1439
+ `);
1440
+ await db.execute(sql`
1441
+ CREATE TABLE IF NOT EXISTS "workflow"."embedded_workflows" (
1442
+ "id" text PRIMARY KEY,
1443
+ "name" text NOT NULL,
1444
+ "active" boolean DEFAULT false NOT NULL,
1445
+ "workflow" jsonb NOT NULL,
1446
+ "created_at" text NOT NULL,
1447
+ "updated_at" text NOT NULL,
1448
+ "version_id" text NOT NULL
1449
+ )
1450
+ `);
1451
+ await db.execute(sql`
1452
+ CREATE INDEX IF NOT EXISTS "idx_embedded_workflows_active"
1453
+ ON "workflow"."embedded_workflows" ("active")
1454
+ `);
1455
+ await db.execute(sql`
1456
+ CREATE INDEX IF NOT EXISTS "idx_embedded_workflows_updated_at"
1457
+ ON "workflow"."embedded_workflows" ("updated_at")
1458
+ `);
1459
+ await db.execute(sql`
1460
+ CREATE TABLE IF NOT EXISTS "workflow"."embedded_executions" (
1461
+ "id" text PRIMARY KEY,
1462
+ "workflow_id" text NOT NULL,
1463
+ "status" text NOT NULL,
1464
+ "mode" text NOT NULL,
1465
+ "finished" boolean DEFAULT false NOT NULL,
1466
+ "started_at" text NOT NULL,
1467
+ "stopped_at" text,
1468
+ "execution" jsonb NOT NULL
1469
+ )
1470
+ `);
1471
+ await db.execute(sql`
1472
+ CREATE INDEX IF NOT EXISTS "idx_embedded_executions_workflow_id"
1473
+ ON "workflow"."embedded_executions" ("workflow_id")
1474
+ `);
1475
+ await db.execute(sql`
1476
+ CREATE INDEX IF NOT EXISTS "idx_embedded_executions_status"
1477
+ ON "workflow"."embedded_executions" ("status")
1478
+ `);
1479
+ await db.execute(sql`
1480
+ CREATE INDEX IF NOT EXISTS "idx_embedded_executions_started_at"
1481
+ ON "workflow"."embedded_executions" ("started_at")
1482
+ `);
1483
+ await db.execute(sql`
1484
+ CREATE TABLE IF NOT EXISTS "workflow"."embedded_credentials" (
1485
+ "id" text PRIMARY KEY,
1486
+ "name" text NOT NULL,
1487
+ "type" text NOT NULL,
1488
+ "data" jsonb NOT NULL,
1489
+ "is_resolvable" boolean DEFAULT true NOT NULL,
1490
+ "created_at" text NOT NULL,
1491
+ "updated_at" text NOT NULL
1492
+ )
1493
+ `);
1494
+ await db.execute(sql`
1495
+ CREATE INDEX IF NOT EXISTS "idx_embedded_credentials_type"
1496
+ ON "workflow"."embedded_credentials" ("type")
1497
+ `);
1498
+ await db.execute(sql`
1499
+ CREATE TABLE IF NOT EXISTS "workflow"."embedded_tags" (
1500
+ "id" text PRIMARY KEY,
1501
+ "name" text NOT NULL,
1502
+ "created_at" text NOT NULL,
1503
+ "updated_at" text NOT NULL
1504
+ )
1505
+ `);
1506
+ await db.execute(sql`
1507
+ CREATE UNIQUE INDEX IF NOT EXISTS "idx_embedded_tags_name"
1508
+ ON "workflow"."embedded_tags" ("name")
1509
+ `);
1510
+ })();
1511
+ }
1512
+ await this.schemaReady;
1513
+ }
1514
+
1515
+ async createWorkflow(workflow: WorkflowDefinition): Promise<WorkflowDefinitionResponse> {
1516
+ this.assertRegisteredNodes(workflow);
1517
+ this.assertHostSupports(workflow);
1518
+ await this.ensureSchema();
1519
+ const db = this.getDb();
1520
+ const id = workflow.id || randomUUID();
1521
+ const createdAt = nowIso();
1522
+ const versionId = randomUUID();
1523
+ const stored = normalizeWorkflowPayload(workflow, id, false);
1524
+ await db.insert(embeddedWorkflows).values({
1525
+ id,
1526
+ name: stored.name,
1527
+ active: false,
1528
+ createdAt,
1529
+ updatedAt: createdAt,
1530
+ versionId,
1531
+ workflow: stored,
1532
+ });
1533
+ return responseFromWorkflow(stored, createdAt, createdAt, versionId);
1534
+ }
1535
+
1536
+ async updateWorkflow(
1537
+ id: string,
1538
+ workflow: WorkflowDefinition
1539
+ ): Promise<WorkflowDefinitionResponse> {
1540
+ this.assertRegisteredNodes(workflow);
1541
+ const existing = await this.getStoredWorkflow(id);
1542
+ const db = this.getDb();
1543
+ const updatedAt = nowIso();
1544
+ const versionId = randomUUID();
1545
+ const stored = normalizeWorkflowPayload(workflow, id, existing.workflow.active ?? false);
1546
+ await db
1547
+ .update(embeddedWorkflows)
1548
+ .set({
1549
+ name: stored.name,
1550
+ active: stored.active ?? false,
1551
+ workflow: stored,
1552
+ updatedAt,
1553
+ versionId,
1554
+ })
1555
+ .where(eq(embeddedWorkflows.id, id));
1556
+ if (stored.active) await this.armSchedules(id);
1557
+ return responseFromWorkflow(stored, existing.createdAt, updatedAt, versionId);
1558
+ }
1559
+
1560
+ async listWorkflows(params?: {
1561
+ active?: boolean;
1562
+ tags?: string[];
1563
+ limit?: number;
1564
+ cursor?: string;
1565
+ }): Promise<{ data: WorkflowDefinitionResponse[]; nextCursor?: string }> {
1566
+ await this.ensureSchema();
1567
+ const db = this.getDb();
1568
+ const rows = await db
1569
+ .select()
1570
+ .from(embeddedWorkflows)
1571
+ .orderBy(desc(embeddedWorkflows.updatedAt));
1572
+ const data = rows
1573
+ .map((row) => ({
1574
+ workflow: cloneJson(row.workflow),
1575
+ createdAt: row.createdAt,
1576
+ updatedAt: row.updatedAt,
1577
+ versionId: row.versionId,
1578
+ }))
1579
+ .filter((entry) => params?.active === undefined || entry.workflow.active === params.active)
1580
+ .filter((entry) => {
1581
+ if (!params?.tags?.length) return true;
1582
+ const tagIds = new Set(entry.workflow.tags?.map((tag) => tag.id) ?? []);
1583
+ return params.tags.every((tag) => tagIds.has(tag));
1584
+ })
1585
+ .map((entry) =>
1586
+ responseFromWorkflow(entry.workflow, entry.createdAt, entry.updatedAt, entry.versionId)
1587
+ );
1588
+ return { data: typeof params?.limit === 'number' ? data.slice(0, params.limit) : data };
1589
+ }
1590
+
1591
+ async getWorkflow(id: string): Promise<WorkflowDefinitionResponse> {
1592
+ const entry = await this.getStoredWorkflow(id);
1593
+ return responseFromWorkflow(entry.workflow, entry.createdAt, entry.updatedAt, entry.versionId);
1594
+ }
1595
+
1596
+ async deleteWorkflow(id: string): Promise<void> {
1597
+ await this.ensureSchema();
1598
+ this.clearSchedules(id);
1599
+ const existing = await this.getStoredWorkflow(id);
1600
+ const db = this.getDb();
1601
+ await db.delete(embeddedWorkflows).where(eq(embeddedWorkflows.id, id));
1602
+ if (!existing) {
1603
+ throw new WorkflowApiError(`Workflow not found: ${id}`, 404);
1604
+ }
1605
+ }
1606
+
1607
+ async activateWorkflow(id: string): Promise<WorkflowDefinitionResponse> {
1608
+ const entry = await this.getStoredWorkflow(id);
1609
+ this.assertHostSupports(entry.workflow);
1610
+ const db = this.getDb();
1611
+ entry.workflow.active = true;
1612
+ entry.updatedAt = nowIso();
1613
+ entry.versionId = randomUUID();
1614
+ await db
1615
+ .update(embeddedWorkflows)
1616
+ .set({
1617
+ active: true,
1618
+ workflow: entry.workflow,
1619
+ updatedAt: entry.updatedAt,
1620
+ versionId: entry.versionId,
1621
+ })
1622
+ .where(eq(embeddedWorkflows.id, id));
1623
+ await this.armSchedules(id);
1624
+ return responseFromWorkflow(entry.workflow, entry.createdAt, entry.updatedAt, entry.versionId);
1625
+ }
1626
+
1627
+ async deactivateWorkflow(id: string): Promise<WorkflowDefinitionResponse> {
1628
+ const entry = await this.getStoredWorkflow(id);
1629
+ const db = this.getDb();
1630
+ entry.workflow.active = false;
1631
+ entry.updatedAt = nowIso();
1632
+ entry.versionId = randomUUID();
1633
+ this.clearSchedules(id);
1634
+ await db
1635
+ .update(embeddedWorkflows)
1636
+ .set({
1637
+ active: false,
1638
+ workflow: entry.workflow,
1639
+ updatedAt: entry.updatedAt,
1640
+ versionId: entry.versionId,
1641
+ })
1642
+ .where(eq(embeddedWorkflows.id, id));
1643
+ return responseFromWorkflow(entry.workflow, entry.createdAt, entry.updatedAt, entry.versionId);
1644
+ }
1645
+
1646
+ async updateWorkflowTags(id: string, tagIds: string[]): Promise<WorkflowTag[]> {
1647
+ const entry = await this.getStoredWorkflow(id);
1648
+ const db = this.getDb();
1649
+ const tags: WorkflowTag[] = [];
1650
+ for (const tagId of tagIds) {
1651
+ const rows = await db.select().from(embeddedTags).where(eq(embeddedTags.id, tagId)).limit(1);
1652
+ const tag = rows[0];
1653
+ if (!tag) throw new WorkflowApiError(`Tag not found: ${tagId}`, 404);
1654
+ tags.push({ id: tag.id, name: tag.name, createdAt: tag.createdAt, updatedAt: tag.updatedAt });
1655
+ }
1656
+ entry.workflow.tags = cloneJson(tags);
1657
+ entry.updatedAt = nowIso();
1658
+ entry.versionId = randomUUID();
1659
+ await db
1660
+ .update(embeddedWorkflows)
1661
+ .set({
1662
+ workflow: entry.workflow,
1663
+ updatedAt: entry.updatedAt,
1664
+ versionId: entry.versionId,
1665
+ })
1666
+ .where(eq(embeddedWorkflows.id, id));
1667
+ return cloneJson(tags);
1668
+ }
1669
+
1670
+ async createCredential(credential: {
1671
+ name: string;
1672
+ type: string;
1673
+ data: Record<string, unknown>;
1674
+ }): Promise<WorkflowCredential> {
1675
+ await this.ensureSchema();
1676
+ const db = this.getDb();
1677
+ const id = randomUUID();
1678
+ const timestamp = nowIso();
1679
+ const stored: StoredCredential = {
1680
+ id,
1681
+ name: credential.name,
1682
+ type: credential.type,
1683
+ data: cloneJson(credential.data),
1684
+ isResolvable: true,
1685
+ createdAt: timestamp,
1686
+ updatedAt: timestamp,
1687
+ };
1688
+ await db.insert(embeddedCredentials).values({
1689
+ id,
1690
+ name: stored.name,
1691
+ type: stored.type,
1692
+ data: cloneJson(credential.data),
1693
+ isResolvable: true,
1694
+ createdAt: timestamp,
1695
+ updatedAt: timestamp,
1696
+ });
1697
+ const { data: _data, ...response } = stored;
1698
+ return cloneJson(response);
1699
+ }
1700
+
1701
+ async deleteCredential(id: string): Promise<void> {
1702
+ await this.ensureSchema();
1703
+ await this.getDb().delete(embeddedCredentials).where(eq(embeddedCredentials.id, id));
1704
+ }
1705
+
1706
+ async listExecutions(params?: {
1707
+ workflowId?: string;
1708
+ status?: WorkflowExecution['status'];
1709
+ limit?: number;
1710
+ cursor?: string;
1711
+ }): Promise<{ data: WorkflowExecution[]; nextCursor?: string }> {
1712
+ await this.ensureSchema();
1713
+ const rows = await this.getDb()
1714
+ .select()
1715
+ .from(embeddedExecutions)
1716
+ .where(
1717
+ params?.workflowId && params?.status
1718
+ ? and(
1719
+ eq(embeddedExecutions.workflowId, params.workflowId),
1720
+ eq(embeddedExecutions.status, params.status)
1721
+ )
1722
+ : params?.workflowId
1723
+ ? eq(embeddedExecutions.workflowId, params.workflowId)
1724
+ : params?.status
1725
+ ? eq(embeddedExecutions.status, params.status)
1726
+ : undefined
1727
+ )
1728
+ .orderBy(desc(embeddedExecutions.startedAt));
1729
+ const data = rows.map((row) => cloneJson(row.execution));
1730
+ return { data: typeof params?.limit === 'number' ? data.slice(0, params.limit) : data };
1731
+ }
1732
+
1733
+ async getExecution(id: string): Promise<WorkflowExecution> {
1734
+ await this.ensureSchema();
1735
+ const rows = await this.getDb()
1736
+ .select()
1737
+ .from(embeddedExecutions)
1738
+ .where(eq(embeddedExecutions.id, id))
1739
+ .limit(1);
1740
+ const execution = rows[0]?.execution;
1741
+ if (!execution) throw new WorkflowApiError(`Execution not found: ${id}`, 404);
1742
+ return cloneJson(execution);
1743
+ }
1744
+
1745
+ async deleteExecution(id: string): Promise<void> {
1746
+ await this.ensureSchema();
1747
+ await this.getDb().delete(embeddedExecutions).where(eq(embeddedExecutions.id, id));
1748
+ }
1749
+
1750
+ async listTags(): Promise<{ data: WorkflowTag[] }> {
1751
+ await this.ensureSchema();
1752
+ const rows = await this.getDb().select().from(embeddedTags).orderBy(embeddedTags.name);
1753
+ return { data: rows.map((row) => cloneJson(row)) };
1754
+ }
1755
+
1756
+ async createTag(name: string): Promise<WorkflowTag> {
1757
+ await this.ensureSchema();
1758
+ const db = this.getDb();
1759
+ const existingRows = await db
1760
+ .select()
1761
+ .from(embeddedTags)
1762
+ .where(eq(embeddedTags.name, name))
1763
+ .limit(1);
1764
+ const existing = existingRows[0];
1765
+ if (existing) return cloneJson(existing);
1766
+ const timestamp = nowIso();
1767
+ const tag = { id: randomUUID(), name, createdAt: timestamp, updatedAt: timestamp };
1768
+ await db.insert(embeddedTags).values(tag);
1769
+ return cloneJson(tag);
1770
+ }
1771
+
1772
+ async getOrCreateTag(name: string): Promise<WorkflowTag> {
1773
+ await this.ensureSchema();
1774
+ const rows = await this.getDb().select().from(embeddedTags);
1775
+ const existing = rows.find((tag) => tag.name.toLowerCase() === name.toLowerCase());
1776
+ return existing ? cloneJson(existing) : this.createTag(name);
1777
+ }
1778
+
1779
+ async executeWorkflow(id: string, options: ExecuteOptions = {}): Promise<WorkflowExecution> {
1780
+ const entry = await this.getStoredWorkflow(id);
1781
+ return this.runWorkflow(entry.workflow, options.mode ?? 'manual');
1782
+ }
1783
+
1784
+ async executeWebhook(
1785
+ path: string,
1786
+ payload: Record<string, unknown>,
1787
+ method = 'POST'
1788
+ ): Promise<WorkflowExecution> {
1789
+ await this.ensureSchema();
1790
+ const normalizedPath = normalizeWebhookPath(path);
1791
+ const normalizedMethod = method.toUpperCase();
1792
+ const rows = await this.getDb()
1793
+ .select()
1794
+ .from(embeddedWorkflows)
1795
+ .where(eq(embeddedWorkflows.active, true));
1796
+
1797
+ for (const row of rows) {
1798
+ const workflow = cloneJson(row.workflow);
1799
+ const webhookNode = workflow.nodes.find((node) => {
1800
+ if (node.disabled || node.type !== 'workflows-nodes-base.webhook') return false;
1801
+ const nodePath = normalizeWebhookPath(node.parameters.path);
1802
+ const nodeMethod = readString(node.parameters.httpMethod, 'POST').toUpperCase();
1803
+ return nodePath === normalizedPath && nodeMethod === normalizedMethod;
1804
+ });
1805
+ if (!webhookNode) continue;
1806
+ webhookNode.parameters = {
1807
+ ...webhookNode.parameters,
1808
+ __embeddedPayload: {
1809
+ ...payload,
1810
+ headers: isRecord(payload.headers) ? payload.headers : {},
1811
+ method: normalizedMethod,
1812
+ path: normalizedPath,
1813
+ },
1814
+ };
1815
+ return this.runWorkflow(workflow, 'webhook');
1816
+ }
1817
+
1818
+ throw new WorkflowApiError(`Webhook not found: ${normalizedMethod} /${normalizedPath}`, 404);
1819
+ }
1820
+
1821
+ async triggerSchedulesOnce(workflowId?: string): Promise<WorkflowExecution[]> {
1822
+ // Fire scheduled workflows once on demand (used by tests / debug). Reads
1823
+ // active workflows directly from the DB rather than from in-process state
1824
+ // because scheduling state now lives in core's task table.
1825
+ const executions: WorkflowExecution[] = [];
1826
+ if (workflowId) {
1827
+ const entry = await this.getStoredWorkflow(workflowId);
1828
+ if (!entry.workflow.active) return executions;
1829
+ executions.push(await this.runWorkflow(entry.workflow, 'trigger'));
1830
+ return executions;
1831
+ }
1832
+ await this.ensureSchema();
1833
+ const rows = await this.getDb()
1834
+ .select()
1835
+ .from(embeddedWorkflows)
1836
+ .where(eq(embeddedWorkflows.active, true));
1837
+ for (const row of rows) {
1838
+ const wf = cloneJson(row.workflow);
1839
+ executions.push(await this.runWorkflow(wf, 'trigger'));
1840
+ }
1841
+ return executions;
1842
+ }
1843
+
1844
+ private async getStoredWorkflow(id: string): Promise<StoredWorkflowRow> {
1845
+ await this.ensureSchema();
1846
+ const rows = await this.getDb()
1847
+ .select()
1848
+ .from(embeddedWorkflows)
1849
+ .where(eq(embeddedWorkflows.id, id))
1850
+ .limit(1);
1851
+ const row = rows[0];
1852
+ if (!row) throw new WorkflowApiError(`Workflow not found: ${id}`, 404);
1853
+ return {
1854
+ workflow: cloneJson(row.workflow),
1855
+ createdAt: row.createdAt,
1856
+ updatedAt: row.updatedAt,
1857
+ versionId: row.versionId,
1858
+ };
1859
+ }
1860
+
1861
+ private assertRegisteredNodes(workflow: WorkflowDefinition): void {
1862
+ const missing = workflow.nodes
1863
+ .filter((node) => !node.disabled && !this.nodeTypes.has(node.type))
1864
+ .map((node) => `${node.name} (${node.type})`);
1865
+ if (missing.length > 0) {
1866
+ throw new WorkflowApiError(
1867
+ `Embedded workflow runtime does not support node(s): ${missing.join(', ')}`,
1868
+ 400
1869
+ );
1870
+ }
1871
+ }
1872
+
1873
+ /**
1874
+ * Verify the host can host every active node's capability requirements
1875
+ * (fs, inbound, longRunning, childProcess, net). On failure, throw a
1876
+ * 400 with one actionable line per offending node.
1877
+ */
1878
+ private assertHostSupports(workflow: WorkflowDefinition): void {
1879
+ const host = this.hostCapabilities;
1880
+ const issues: string[] = [];
1881
+ for (const node of workflow.nodes) {
1882
+ if (node.disabled) continue;
1883
+ if (!this.nodeTypes.has(node.type)) continue;
1884
+ const nodeType = this.nodeTypes.getByNameAndVersion(node.type);
1885
+ const caps = (nodeType.description as { capabilities?: NodeCapabilities }).capabilities;
1886
+ if (!caps) continue;
1887
+ if (caps.requiresFs && !host.fs) {
1888
+ issues.push(
1889
+ `${node.name} (${node.type}) needs filesystem access; host '${host.label}' has no fs — run on a server agent`
1890
+ );
1891
+ }
1892
+ if (caps.requiresInbound && !host.inbound) {
1893
+ issues.push(
1894
+ `${node.name} needs an inbound public webhook; host '${host.label}' can't receive — pair Eliza Cloud or enable plugin-tunnel`
1895
+ );
1896
+ }
1897
+ if (caps.requiresLongRunning && !host.longRunning) {
1898
+ issues.push(
1899
+ `${node.name} needs a long-running process; host '${host.label}' is short-lived — schedule via the cloud cron handler`
1900
+ );
1901
+ }
1902
+ if (caps.requiresChildProcess && !host.childProcess) {
1903
+ issues.push(
1904
+ `${node.name} spawns a child process; not allowed on '${host.label}' — run on a server agent`
1905
+ );
1906
+ }
1907
+ if (caps.requiresNet && !host.net) {
1908
+ issues.push(
1909
+ `${node.name} needs raw sockets; not available on '${host.label}' — use the HTTP Request node or run on a server agent`
1910
+ );
1911
+ }
1912
+ }
1913
+ if (issues.length > 0) {
1914
+ throw new WorkflowApiError(
1915
+ `Workflow incompatible with host '${host.label}':\n - ${issues.join('\n - ')}`,
1916
+ 400
1917
+ );
1918
+ }
1919
+ }
1920
+
1921
+ /** Re-create core Tasks for every active workflow on service start.
1922
+ * Tasks themselves persist across restart; this is a reconcile step that
1923
+ * ensures workflows whose schedule changed (or whose tasks were never
1924
+ * created in the first place) end up correctly scheduled. */
1925
+ private async rehydrateSchedules(): Promise<void> {
1926
+ await this.ensureSchema();
1927
+ const rows = await this.getDb()
1928
+ .select()
1929
+ .from(embeddedWorkflows)
1930
+ .where(eq(embeddedWorkflows.active, true));
1931
+ for (const row of rows) {
1932
+ await this.armSchedules(row.id);
1933
+ }
1934
+ }
1935
+
1936
+ /** Create one recurring core Task per scheduleTrigger node on the workflow.
1937
+ * Idempotent: existing tasks for this workflow are removed first so the
1938
+ * task set always reflects the current workflow definition. */
1939
+ private async armSchedules(workflowId: string): Promise<void> {
1940
+ await this.clearSchedules(workflowId);
1941
+ if (typeof this.runtime.createTask !== 'function') return;
1942
+ const entry = await this.getStoredWorkflow(workflowId);
1943
+ const scheduleNodes = entry.workflow.nodes.filter(
1944
+ (node) => !node.disabled && node.type === 'workflows-nodes-base.scheduleTrigger'
1945
+ );
1946
+ if (scheduleNodes.length === 0) return;
1947
+
1948
+ for (const node of scheduleNodes) {
1949
+ const intervalMs = resolveScheduleIntervalMs(node.parameters);
1950
+ await this.runtime.createTask({
1951
+ name: WORKFLOW_RUN_TASK_WORKER_NAME,
1952
+ description: `Scheduled workflow run: ${entry.workflow.name}`,
1953
+ tags: ['queue', 'repeat', WORKFLOW_TASK_TAG],
1954
+ metadata: {
1955
+ kind: WORKFLOW_TASK_KIND,
1956
+ workflowId,
1957
+ scheduleNodeId: node.id,
1958
+ updateInterval: intervalMs,
1959
+ baseInterval: intervalMs,
1960
+ updatedAt: Date.now(),
1961
+ },
1962
+ });
1963
+ }
1964
+ }
1965
+
1966
+ /** Remove every core Task tagged for this workflow. */
1967
+ private async clearSchedules(workflowId: string): Promise<void> {
1968
+ if (typeof this.runtime.getTasks !== 'function') return;
1969
+ const tasks = await this.runtime.getTasks({
1970
+ tags: [WORKFLOW_TASK_TAG],
1971
+ agentIds: [this.runtime.agentId],
1972
+ });
1973
+ if (!tasks?.length) return;
1974
+ for (const task of tasks) {
1975
+ if (
1976
+ task.id &&
1977
+ (task.metadata as Record<string, unknown> | undefined)?.workflowId === workflowId
1978
+ ) {
1979
+ await this.runtime.deleteTask(task.id as UUID);
1980
+ }
1981
+ }
1982
+ }
1983
+
1984
+ private async saveExecution(execution: WorkflowExecution): Promise<void> {
1985
+ await this.ensureSchema();
1986
+ await this.getDb()
1987
+ .insert(embeddedExecutions)
1988
+ .values({
1989
+ id: execution.id,
1990
+ workflowId: execution.workflowId,
1991
+ status: execution.status,
1992
+ mode: execution.mode,
1993
+ finished: execution.finished,
1994
+ startedAt: execution.startedAt,
1995
+ stoppedAt: execution.stoppedAt ?? null,
1996
+ execution: cloneJson(execution),
1997
+ })
1998
+ .onConflictDoUpdate({
1999
+ target: embeddedExecutions.id,
2000
+ set: {
2001
+ workflowId: execution.workflowId,
2002
+ status: execution.status,
2003
+ mode: execution.mode,
2004
+ finished: execution.finished,
2005
+ startedAt: execution.startedAt,
2006
+ stoppedAt: execution.stoppedAt ?? null,
2007
+ execution: cloneJson(execution),
2008
+ },
2009
+ });
2010
+ }
2011
+
2012
+ private buildIncomingConnections(
2013
+ workflowData: WorkflowDefinition
2014
+ ): Map<string, IncomingConnection[]> {
2015
+ const incoming = new Map<string, IncomingConnection[]>();
2016
+ for (const [source, outputsByType] of Object.entries(workflowData.connections ?? {})) {
2017
+ const mainOutputs = outputsByType.main ?? [];
2018
+ mainOutputs.forEach((connections, sourceOutputIndex) => {
2019
+ for (const connection of connections ?? []) {
2020
+ if (connection.type !== 'main') continue;
2021
+ const destination = incoming.get(connection.node) ?? [];
2022
+ destination.push({
2023
+ source,
2024
+ sourceOutputIndex,
2025
+ destinationInputIndex: connection.index ?? 0,
2026
+ });
2027
+ incoming.set(connection.node, destination);
2028
+ }
2029
+ });
2030
+ }
2031
+ return incoming;
2032
+ }
2033
+
2034
+ private resolveStartNodes(
2035
+ workflowData: WorkflowDefinition,
2036
+ mode: WorkflowExecuteMode,
2037
+ incoming: Map<string, IncomingConnection[]>
2038
+ ): Set<string> {
2039
+ const enabledNodes = workflowData.nodes.filter((node) => !node.disabled);
2040
+ const start = new Set<string>();
2041
+
2042
+ if (mode === 'webhook') {
2043
+ for (const node of enabledNodes) {
2044
+ if (
2045
+ node.type === 'workflows-nodes-base.webhook' &&
2046
+ isRecord(node.parameters.__embeddedPayload)
2047
+ ) {
2048
+ start.add(node.name);
2049
+ }
2050
+ }
2051
+ if (start.size === 0) {
2052
+ for (const node of enabledNodes) {
2053
+ if (node.type === 'workflows-nodes-base.webhook') start.add(node.name);
2054
+ }
2055
+ }
2056
+ } else if (mode === 'trigger') {
2057
+ for (const node of enabledNodes) {
2058
+ if (node.type === 'workflows-nodes-base.scheduleTrigger') start.add(node.name);
2059
+ }
2060
+ } else {
2061
+ for (const node of enabledNodes) {
2062
+ if (node.type === 'workflows-nodes-base.manualTrigger') start.add(node.name);
2063
+ }
2064
+ }
2065
+
2066
+ if (start.size === 0) {
2067
+ for (const node of enabledNodes) {
2068
+ if ((incoming.get(node.name) ?? []).length === 0) start.add(node.name);
2069
+ }
2070
+ }
2071
+
2072
+ return start;
2073
+ }
2074
+
2075
+ private collectInputData(
2076
+ nodeName: string,
2077
+ incoming: Map<string, IncomingConnection[]>,
2078
+ nodeOutputs: Map<string, INodeExecutionData[][]>
2079
+ ): INodeExecutionData[][] {
2080
+ const inputData: INodeExecutionData[][] = [];
2081
+ for (const connection of incoming.get(nodeName) ?? []) {
2082
+ const sourceOutputs = nodeOutputs.get(connection.source) ?? [];
2083
+ const sourceItems = sourceOutputs[connection.sourceOutputIndex] ?? [];
2084
+ inputData[connection.destinationInputIndex] = [
2085
+ ...(inputData[connection.destinationInputIndex] ?? []),
2086
+ ...sourceItems,
2087
+ ];
2088
+ }
2089
+ return inputData.length > 0 ? inputData : [[]];
2090
+ }
2091
+
2092
+ private async executeNode(
2093
+ node: WorkflowNode,
2094
+ inputData: INodeExecutionData[][],
2095
+ executionId: string
2096
+ ): Promise<INodeExecutionData[][]> {
2097
+ const nodeType = this.nodeTypes.getByNameAndVersion(node.type);
2098
+ const context: IExecuteFunctions = {
2099
+ getNode: () => node,
2100
+ getInputData: (inputIndex = 0) => inputData[inputIndex] ?? [],
2101
+ getRuntime: () => this.runtime,
2102
+ getExecutionId: () => executionId,
2103
+ };
2104
+ const output = await nodeType.execute.call(context);
2105
+ return output.length > 0 ? output : [[]];
2106
+ }
2107
+
2108
+ private async runWorkflow(
2109
+ workflowData: WorkflowDefinition,
2110
+ mode: WorkflowExecuteMode
2111
+ ): Promise<WorkflowExecution> {
2112
+ const executionId = randomUUID();
2113
+ const startedAt = new Date();
2114
+ const pending: WorkflowExecution = {
2115
+ id: executionId,
2116
+ finished: false,
2117
+ mode,
2118
+ startedAt: startedAt.toISOString(),
2119
+ workflowId: workflowData.id ?? '',
2120
+ status: 'running',
2121
+ };
2122
+ await this.saveExecution(pending);
2123
+
2124
+ try {
2125
+ const enabledNodes = workflowData.nodes.filter((node) => !node.disabled);
2126
+ const nodeByName = new Map(enabledNodes.map((node) => [node.name, node]));
2127
+ const incoming = this.buildIncomingConnections(workflowData);
2128
+ const startNodes = this.resolveStartNodes(workflowData, mode, incoming);
2129
+ const nodeOutputs = new Map<string, INodeExecutionData[][]>();
2130
+ const executed = new Set<string>();
2131
+ const runData: Record<string, unknown[]> = {};
2132
+ let lastNodeExecuted: string | undefined;
2133
+
2134
+ while (executed.size < enabledNodes.length) {
2135
+ let progressed = false;
2136
+
2137
+ for (const node of enabledNodes) {
2138
+ if (executed.has(node.name)) continue;
2139
+
2140
+ const incomingConnections =
2141
+ incoming.get(node.name)?.filter((connection) => nodeByName.has(connection.source)) ??
2142
+ [];
2143
+ const isStartNode = startNodes.has(node.name);
2144
+ const dependenciesComplete = incomingConnections.every((connection) =>
2145
+ executed.has(connection.source)
2146
+ );
2147
+
2148
+ if (!isStartNode && !dependenciesComplete) continue;
2149
+
2150
+ const inputData =
2151
+ isStartNode && incomingConnections.length === 0
2152
+ ? [[]]
2153
+ : this.collectInputData(node.name, incoming, nodeOutputs);
2154
+ const hasInputItems = inputData.some((items) => items.length > 0);
2155
+ const started = Date.now();
2156
+
2157
+ const outputData =
2158
+ !isStartNode && incomingConnections.length > 0 && !hasInputItems
2159
+ ? [[]]
2160
+ : await this.executeNode(node, inputData, executionId);
2161
+
2162
+ nodeOutputs.set(node.name, outputData);
2163
+ runData[node.name] = [
2164
+ {
2165
+ startTime: started,
2166
+ executionTime: Date.now() - started,
2167
+ data: { main: cloneJson(outputData) },
2168
+ source: incomingConnections.map((connection) => ({
2169
+ previousNode: connection.source,
2170
+ previousNodeOutput: connection.sourceOutputIndex,
2171
+ previousNodeRun: 0,
2172
+ })),
2173
+ },
2174
+ ];
2175
+ executed.add(node.name);
2176
+ lastNodeExecuted = node.name;
2177
+ progressed = true;
2178
+ }
2179
+
2180
+ if (!progressed) {
2181
+ const unresolved = enabledNodes
2182
+ .filter((node) => !executed.has(node.name))
2183
+ .map((node) => node.name)
2184
+ .join(', ');
2185
+ throw new Error(`Unable to resolve workflow execution order for node(s): ${unresolved}`);
2186
+ }
2187
+ }
2188
+
2189
+ const stoppedAt = new Date();
2190
+ const execution: WorkflowExecution = {
2191
+ ...pending,
2192
+ finished: true,
2193
+ status: 'success',
2194
+ stoppedAt: stoppedAt.toISOString(),
2195
+ data: {
2196
+ resultData: {
2197
+ runData,
2198
+ lastNodeExecuted,
2199
+ },
2200
+ },
2201
+ };
2202
+ await this.saveExecution(execution);
2203
+ return cloneJson(execution);
2204
+ } catch (error) {
2205
+ const stoppedAt = new Date();
2206
+ const execution: WorkflowExecution = {
2207
+ ...pending,
2208
+ finished: true,
2209
+ status: 'error',
2210
+ stoppedAt: stoppedAt.toISOString(),
2211
+ data: {
2212
+ resultData: {
2213
+ error: {
2214
+ message: error instanceof Error ? error.message : String(error),
2215
+ stack: error instanceof Error ? error.stack : undefined,
2216
+ },
2217
+ },
2218
+ },
2219
+ };
2220
+ await this.saveExecution(execution);
2221
+ throw error;
2222
+ }
2223
+ }
2224
+ }