@xyne/workflow-sdk 1.0.0

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 (310) hide show
  1. package/README.md +36 -0
  2. package/dist/agents/agent-step.d.ts +152 -0
  3. package/dist/agents/agent-step.d.ts.map +1 -0
  4. package/dist/agents/agent-step.js +403 -0
  5. package/dist/agents/agent-step.js.map +1 -0
  6. package/dist/agents/base-runtime.d.ts +38 -0
  7. package/dist/agents/base-runtime.d.ts.map +1 -0
  8. package/dist/agents/base-runtime.js +21 -0
  9. package/dist/agents/base-runtime.js.map +1 -0
  10. package/dist/agents/index.d.ts +12 -0
  11. package/dist/agents/index.d.ts.map +1 -0
  12. package/dist/agents/index.js +17 -0
  13. package/dist/agents/index.js.map +1 -0
  14. package/dist/agents/interceptor.d.ts +19 -0
  15. package/dist/agents/interceptor.d.ts.map +1 -0
  16. package/dist/agents/interceptor.js +21 -0
  17. package/dist/agents/interceptor.js.map +1 -0
  18. package/dist/agents/interceptors/approval-gate.d.ts +14 -0
  19. package/dist/agents/interceptors/approval-gate.d.ts.map +1 -0
  20. package/dist/agents/interceptors/approval-gate.js +19 -0
  21. package/dist/agents/interceptors/approval-gate.js.map +1 -0
  22. package/dist/agents/interceptors/tool-logger.d.ts +15 -0
  23. package/dist/agents/interceptors/tool-logger.d.ts.map +1 -0
  24. package/dist/agents/interceptors/tool-logger.js +23 -0
  25. package/dist/agents/interceptors/tool-logger.js.map +1 -0
  26. package/dist/agents/pi-mono-runtime.d.ts +88 -0
  27. package/dist/agents/pi-mono-runtime.d.ts.map +1 -0
  28. package/dist/agents/pi-mono-runtime.js +129 -0
  29. package/dist/agents/pi-mono-runtime.js.map +1 -0
  30. package/dist/agents/tool-registry.d.ts +25 -0
  31. package/dist/agents/tool-registry.d.ts.map +1 -0
  32. package/dist/agents/tool-registry.js +47 -0
  33. package/dist/agents/tool-registry.js.map +1 -0
  34. package/dist/agents/tool-types.d.ts +25 -0
  35. package/dist/agents/tool-types.d.ts.map +1 -0
  36. package/dist/agents/tool-types.js +9 -0
  37. package/dist/agents/tool-types.js.map +1 -0
  38. package/dist/agents/types.d.ts +91 -0
  39. package/dist/agents/types.d.ts.map +1 -0
  40. package/dist/agents/types.js +12 -0
  41. package/dist/agents/types.js.map +1 -0
  42. package/dist/builder/index.d.ts +30 -0
  43. package/dist/builder/index.d.ts.map +1 -0
  44. package/dist/builder/index.js +32 -0
  45. package/dist/builder/index.js.map +1 -0
  46. package/dist/client/index.d.ts +19 -0
  47. package/dist/client/index.d.ts.map +1 -0
  48. package/dist/client/index.js +18 -0
  49. package/dist/client/index.js.map +1 -0
  50. package/dist/client/types.d.ts +128 -0
  51. package/dist/client/types.d.ts.map +1 -0
  52. package/dist/client/types.js +10 -0
  53. package/dist/client/types.js.map +1 -0
  54. package/dist/client/workflow-client.d.ts +15 -0
  55. package/dist/client/workflow-client.d.ts.map +1 -0
  56. package/dist/client/workflow-client.js +293 -0
  57. package/dist/client/workflow-client.js.map +1 -0
  58. package/dist/common/attachment.d.ts +31 -0
  59. package/dist/common/attachment.d.ts.map +1 -0
  60. package/dist/common/attachment.js +21 -0
  61. package/dist/common/attachment.js.map +1 -0
  62. package/dist/common/executable-check.d.ts +42 -0
  63. package/dist/common/executable-check.d.ts.map +1 -0
  64. package/dist/common/executable-check.js +115 -0
  65. package/dist/common/executable-check.js.map +1 -0
  66. package/dist/common/index.d.ts +21 -0
  67. package/dist/common/index.d.ts.map +1 -0
  68. package/dist/common/index.js +19 -0
  69. package/dist/common/index.js.map +1 -0
  70. package/dist/common/resume-payload.d.ts +34 -0
  71. package/dist/common/resume-payload.d.ts.map +1 -0
  72. package/dist/common/resume-payload.js +12 -0
  73. package/dist/common/resume-payload.js.map +1 -0
  74. package/dist/engine/available-context.d.ts +29 -0
  75. package/dist/engine/available-context.d.ts.map +1 -0
  76. package/dist/engine/available-context.js +66 -0
  77. package/dist/engine/available-context.js.map +1 -0
  78. package/dist/engine/condition-evaluator.d.ts +13 -0
  79. package/dist/engine/condition-evaluator.d.ts.map +1 -0
  80. package/dist/engine/condition-evaluator.js +92 -0
  81. package/dist/engine/condition-evaluator.js.map +1 -0
  82. package/dist/engine/config-validator.d.ts +25 -0
  83. package/dist/engine/config-validator.d.ts.map +1 -0
  84. package/dist/engine/config-validator.js +316 -0
  85. package/dist/engine/config-validator.js.map +1 -0
  86. package/dist/engine/pause-step.d.ts +20 -0
  87. package/dist/engine/pause-step.d.ts.map +1 -0
  88. package/dist/engine/pause-step.js +24 -0
  89. package/dist/engine/pause-step.js.map +1 -0
  90. package/dist/engine/service-registry.d.ts +49 -0
  91. package/dist/engine/service-registry.d.ts.map +1 -0
  92. package/dist/engine/service-registry.js +76 -0
  93. package/dist/engine/service-registry.js.map +1 -0
  94. package/dist/engine/variable-resolver.d.ts +29 -0
  95. package/dist/engine/variable-resolver.d.ts.map +1 -0
  96. package/dist/engine/variable-resolver.js +130 -0
  97. package/dist/engine/variable-resolver.js.map +1 -0
  98. package/dist/engine/workflow-executor.d.ts +96 -0
  99. package/dist/engine/workflow-executor.d.ts.map +1 -0
  100. package/dist/engine/workflow-executor.js +837 -0
  101. package/dist/engine/workflow-executor.js.map +1 -0
  102. package/dist/index.d.ts +57 -0
  103. package/dist/index.d.ts.map +1 -0
  104. package/dist/index.js +56 -0
  105. package/dist/index.js.map +1 -0
  106. package/dist/persistence/in-memory-adapter.d.ts +138 -0
  107. package/dist/persistence/in-memory-adapter.d.ts.map +1 -0
  108. package/dist/persistence/in-memory-adapter.js +315 -0
  109. package/dist/persistence/in-memory-adapter.js.map +1 -0
  110. package/dist/persistence/types.d.ts +214 -0
  111. package/dist/persistence/types.d.ts.map +1 -0
  112. package/dist/persistence/types.js +8 -0
  113. package/dist/persistence/types.js.map +1 -0
  114. package/dist/router/types.d.ts +57 -0
  115. package/dist/router/types.d.ts.map +1 -0
  116. package/dist/router/types.js +8 -0
  117. package/dist/router/types.js.map +1 -0
  118. package/dist/router/workflow-router.d.ts +26 -0
  119. package/dist/router/workflow-router.d.ts.map +1 -0
  120. package/dist/router/workflow-router.js +611 -0
  121. package/dist/router/workflow-router.js.map +1 -0
  122. package/dist/runtime/execution-event-bus.d.ts +38 -0
  123. package/dist/runtime/execution-event-bus.d.ts.map +1 -0
  124. package/dist/runtime/execution-event-bus.js +87 -0
  125. package/dist/runtime/execution-event-bus.js.map +1 -0
  126. package/dist/runtime/types.d.ts +129 -0
  127. package/dist/runtime/types.d.ts.map +1 -0
  128. package/dist/runtime/types.js +5 -0
  129. package/dist/runtime/types.js.map +1 -0
  130. package/dist/runtime/workflow-runtime.d.ts +256 -0
  131. package/dist/runtime/workflow-runtime.d.ts.map +1 -0
  132. package/dist/runtime/workflow-runtime.js +699 -0
  133. package/dist/runtime/workflow-runtime.js.map +1 -0
  134. package/dist/steps/base-step.d.ts +204 -0
  135. package/dist/steps/base-step.d.ts.map +1 -0
  136. package/dist/steps/base-step.js +69 -0
  137. package/dist/steps/base-step.js.map +1 -0
  138. package/dist/steps/builtin/code.step.d.ts +95 -0
  139. package/dist/steps/builtin/code.step.d.ts.map +1 -0
  140. package/dist/steps/builtin/code.step.js +122 -0
  141. package/dist/steps/builtin/code.step.js.map +1 -0
  142. package/dist/steps/builtin/conditional.step.d.ts +146 -0
  143. package/dist/steps/builtin/conditional.step.d.ts.map +1 -0
  144. package/dist/steps/builtin/conditional.step.js +71 -0
  145. package/dist/steps/builtin/conditional.step.js.map +1 -0
  146. package/dist/steps/builtin/dedup.step.d.ts +65 -0
  147. package/dist/steps/builtin/dedup.step.d.ts.map +1 -0
  148. package/dist/steps/builtin/dedup.step.js +61 -0
  149. package/dist/steps/builtin/dedup.step.js.map +1 -0
  150. package/dist/steps/builtin/http-request.step.d.ts +928 -0
  151. package/dist/steps/builtin/http-request.step.d.ts.map +1 -0
  152. package/dist/steps/builtin/http-request.step.js +570 -0
  153. package/dist/steps/builtin/http-request.step.js.map +1 -0
  154. package/dist/steps/builtin/loop.step.d.ts +100 -0
  155. package/dist/steps/builtin/loop.step.d.ts.map +1 -0
  156. package/dist/steps/builtin/loop.step.js +79 -0
  157. package/dist/steps/builtin/loop.step.js.map +1 -0
  158. package/dist/steps/builtin/parallel.step.d.ts +208 -0
  159. package/dist/steps/builtin/parallel.step.d.ts.map +1 -0
  160. package/dist/steps/builtin/parallel.step.js +249 -0
  161. package/dist/steps/builtin/parallel.step.js.map +1 -0
  162. package/dist/steps/builtin/switch.step.d.ts +200 -0
  163. package/dist/steps/builtin/switch.step.d.ts.map +1 -0
  164. package/dist/steps/builtin/switch.step.js +92 -0
  165. package/dist/steps/builtin/switch.step.js.map +1 -0
  166. package/dist/steps/builtin/transform.step.d.ts +247 -0
  167. package/dist/steps/builtin/transform.step.d.ts.map +1 -0
  168. package/dist/steps/builtin/transform.step.js +135 -0
  169. package/dist/steps/builtin/transform.step.js.map +1 -0
  170. package/dist/steps/builtin/wait.step.d.ts +921 -0
  171. package/dist/steps/builtin/wait.step.d.ts.map +1 -0
  172. package/dist/steps/builtin/wait.step.js +211 -0
  173. package/dist/steps/builtin/wait.step.js.map +1 -0
  174. package/dist/steps/step-registry.d.ts +64 -0
  175. package/dist/steps/step-registry.d.ts.map +1 -0
  176. package/dist/steps/step-registry.js +102 -0
  177. package/dist/steps/step-registry.js.map +1 -0
  178. package/dist/storage/in-memory-adapter.d.ts +25 -0
  179. package/dist/storage/in-memory-adapter.d.ts.map +1 -0
  180. package/dist/storage/in-memory-adapter.js +41 -0
  181. package/dist/storage/in-memory-adapter.js.map +1 -0
  182. package/dist/storage/types.d.ts +53 -0
  183. package/dist/storage/types.d.ts.map +1 -0
  184. package/dist/storage/types.js +13 -0
  185. package/dist/storage/types.js.map +1 -0
  186. package/dist/testing/index.d.ts +10 -0
  187. package/dist/testing/index.d.ts.map +1 -0
  188. package/dist/testing/index.js +10 -0
  189. package/dist/testing/index.js.map +1 -0
  190. package/dist/testing/mock-context.d.ts +13 -0
  191. package/dist/testing/mock-context.d.ts.map +1 -0
  192. package/dist/testing/mock-context.js +21 -0
  193. package/dist/testing/mock-context.js.map +1 -0
  194. package/dist/testing/mock-step-context.d.ts +47 -0
  195. package/dist/testing/mock-step-context.d.ts.map +1 -0
  196. package/dist/testing/mock-step-context.js +59 -0
  197. package/dist/testing/mock-step-context.js.map +1 -0
  198. package/dist/triggers/base-trigger.d.ts +58 -0
  199. package/dist/triggers/base-trigger.d.ts.map +1 -0
  200. package/dist/triggers/base-trigger.js +37 -0
  201. package/dist/triggers/base-trigger.js.map +1 -0
  202. package/dist/triggers/builtin/default-cron-trigger.d.ts +44 -0
  203. package/dist/triggers/builtin/default-cron-trigger.d.ts.map +1 -0
  204. package/dist/triggers/builtin/default-cron-trigger.js +31 -0
  205. package/dist/triggers/builtin/default-cron-trigger.js.map +1 -0
  206. package/dist/triggers/builtin/default-event-trigger.d.ts +32 -0
  207. package/dist/triggers/builtin/default-event-trigger.d.ts.map +1 -0
  208. package/dist/triggers/builtin/default-event-trigger.js +21 -0
  209. package/dist/triggers/builtin/default-event-trigger.js.map +1 -0
  210. package/dist/triggers/builtin/default-manual-trigger.d.ts +119 -0
  211. package/dist/triggers/builtin/default-manual-trigger.d.ts.map +1 -0
  212. package/dist/triggers/builtin/default-manual-trigger.js +64 -0
  213. package/dist/triggers/builtin/default-manual-trigger.js.map +1 -0
  214. package/dist/triggers/builtin/default-webhook-trigger.d.ts +72 -0
  215. package/dist/triggers/builtin/default-webhook-trigger.d.ts.map +1 -0
  216. package/dist/triggers/builtin/default-webhook-trigger.js +91 -0
  217. package/dist/triggers/builtin/default-webhook-trigger.js.map +1 -0
  218. package/dist/triggers/cron-trigger.d.ts +52 -0
  219. package/dist/triggers/cron-trigger.d.ts.map +1 -0
  220. package/dist/triggers/cron-trigger.js +15 -0
  221. package/dist/triggers/cron-trigger.js.map +1 -0
  222. package/dist/triggers/event-trigger.d.ts +14 -0
  223. package/dist/triggers/event-trigger.d.ts.map +1 -0
  224. package/dist/triggers/event-trigger.js +13 -0
  225. package/dist/triggers/event-trigger.js.map +1 -0
  226. package/dist/triggers/manual-trigger.d.ts +11 -0
  227. package/dist/triggers/manual-trigger.d.ts.map +1 -0
  228. package/dist/triggers/manual-trigger.js +10 -0
  229. package/dist/triggers/manual-trigger.js.map +1 -0
  230. package/dist/triggers/trigger-registry.d.ts +48 -0
  231. package/dist/triggers/trigger-registry.d.ts.map +1 -0
  232. package/dist/triggers/trigger-registry.js +81 -0
  233. package/dist/triggers/trigger-registry.js.map +1 -0
  234. package/dist/triggers/webhook-trigger.d.ts +54 -0
  235. package/dist/triggers/webhook-trigger.d.ts.map +1 -0
  236. package/dist/triggers/webhook-trigger.js +13 -0
  237. package/dist/triggers/webhook-trigger.js.map +1 -0
  238. package/dist/types/attachment.d.ts +23 -0
  239. package/dist/types/attachment.d.ts.map +1 -0
  240. package/dist/types/attachment.js +2 -0
  241. package/dist/types/attachment.js.map +1 -0
  242. package/dist/types/categories.d.ts +14 -0
  243. package/dist/types/categories.d.ts.map +1 -0
  244. package/dist/types/categories.js +10 -0
  245. package/dist/types/categories.js.map +1 -0
  246. package/dist/types/context.d.ts +61 -0
  247. package/dist/types/context.d.ts.map +1 -0
  248. package/dist/types/context.js +2 -0
  249. package/dist/types/context.js.map +1 -0
  250. package/dist/types/index.d.ts +18 -0
  251. package/dist/types/index.d.ts.map +1 -0
  252. package/dist/types/index.js +12 -0
  253. package/dist/types/index.js.map +1 -0
  254. package/dist/types/known-types.d.ts +34 -0
  255. package/dist/types/known-types.d.ts.map +1 -0
  256. package/dist/types/known-types.js +49 -0
  257. package/dist/types/known-types.js.map +1 -0
  258. package/dist/types/operators.d.ts +20 -0
  259. package/dist/types/operators.d.ts.map +1 -0
  260. package/dist/types/operators.js +27 -0
  261. package/dist/types/operators.js.map +1 -0
  262. package/dist/types/pause-path.d.ts +23 -0
  263. package/dist/types/pause-path.d.ts.map +1 -0
  264. package/dist/types/pause-path.js +2 -0
  265. package/dist/types/pause-path.js.map +1 -0
  266. package/dist/types/pause-state.d.ts +100 -0
  267. package/dist/types/pause-state.d.ts.map +1 -0
  268. package/dist/types/pause-state.js +15 -0
  269. package/dist/types/pause-state.js.map +1 -0
  270. package/dist/types/resume-payload.d.ts +34 -0
  271. package/dist/types/resume-payload.d.ts.map +1 -0
  272. package/dist/types/resume-payload.js +12 -0
  273. package/dist/types/resume-payload.js.map +1 -0
  274. package/dist/types/status.d.ts +27 -0
  275. package/dist/types/status.d.ts.map +1 -0
  276. package/dist/types/status.js +41 -0
  277. package/dist/types/status.js.map +1 -0
  278. package/dist/types/step-events.d.ts +22 -0
  279. package/dist/types/step-events.d.ts.map +1 -0
  280. package/dist/types/step-events.js +8 -0
  281. package/dist/types/step-events.js.map +1 -0
  282. package/dist/types/step-types.d.ts +12 -0
  283. package/dist/types/step-types.d.ts.map +1 -0
  284. package/dist/types/step-types.js +2 -0
  285. package/dist/types/step-types.js.map +1 -0
  286. package/dist/types/trigger-types.d.ts +12 -0
  287. package/dist/types/trigger-types.d.ts.map +1 -0
  288. package/dist/types/trigger-types.js +2 -0
  289. package/dist/types/trigger-types.js.map +1 -0
  290. package/dist/types/validation.d.ts +19 -0
  291. package/dist/types/validation.d.ts.map +1 -0
  292. package/dist/types/validation.js +11 -0
  293. package/dist/types/validation.js.map +1 -0
  294. package/dist/types/workflow-config.d.ts +122 -0
  295. package/dist/types/workflow-config.d.ts.map +1 -0
  296. package/dist/types/workflow-config.js +71 -0
  297. package/dist/types/workflow-config.js.map +1 -0
  298. package/dist/util/executable-check.d.ts +42 -0
  299. package/dist/util/executable-check.d.ts.map +1 -0
  300. package/dist/util/executable-check.js +115 -0
  301. package/dist/util/executable-check.js.map +1 -0
  302. package/dist/util/schema-convert.d.ts +14 -0
  303. package/dist/util/schema-convert.d.ts.map +1 -0
  304. package/dist/util/schema-convert.js +16 -0
  305. package/dist/util/schema-convert.js.map +1 -0
  306. package/dist/util/variable-ref.d.ts +52 -0
  307. package/dist/util/variable-ref.d.ts.map +1 -0
  308. package/dist/util/variable-ref.js +89 -0
  309. package/dist/util/variable-ref.js.map +1 -0
  310. package/package.json +97 -0
@@ -0,0 +1,699 @@
1
+ /**
2
+ * WorkflowRuntime — the central orchestrator.
3
+ *
4
+ * Ties together: persistence, queue, scheduler, executor, triggers, and steps.
5
+ * The host creates one runtime instance and uses it for all workflow operations:
6
+ *
7
+ * - Activate/deactivate triggers (cron, webhook)
8
+ * - Dispatch domain events → matching workflows
9
+ * - Handle incoming webhooks
10
+ * - Fire manual triggers
11
+ * - Resume paused executions
12
+ * - Process queued jobs (the worker loop body)
13
+ */
14
+ import { ConfigValidator } from '../engine/config-validator.js';
15
+ import { EventTrigger } from '../triggers/event-trigger.js';
16
+ import { WebhookTrigger } from '../triggers/webhook-trigger.js';
17
+ import { CronTrigger } from '../triggers/cron-trigger.js';
18
+ import { PauseStep } from '../engine/pause-step.js';
19
+ // ─── Helpers ───
20
+ const noopLogger = {
21
+ info: () => { },
22
+ warn: () => { },
23
+ error: () => { },
24
+ };
25
+ function generateSecret() {
26
+ const bytes = new Uint8Array(24);
27
+ crypto.getRandomValues(bytes);
28
+ return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
29
+ }
30
+ function parseWorkflowConfig(raw) {
31
+ return JSON.parse(raw ?? '{}');
32
+ }
33
+ /**
34
+ * Match a MIME type against an accept-list that supports wildcards.
35
+ * - "image/png" matches exact "image/png"
36
+ * - "image/*" matches anything under "image/"
37
+ * - "*" matches anything
38
+ */
39
+ function mimeAccepted(mime, accept) {
40
+ for (const a of accept) {
41
+ if (a === '*' || a === '*/*' || a === mime)
42
+ return true;
43
+ if (a.endsWith('/*')) {
44
+ const prefix = a.slice(0, -1); // "image/"
45
+ if (mime.startsWith(prefix))
46
+ return true;
47
+ }
48
+ }
49
+ return false;
50
+ }
51
+ // ─── WorkflowRuntime ───
52
+ export class WorkflowRuntime {
53
+ persistence;
54
+ queue;
55
+ scheduler;
56
+ services;
57
+ steps;
58
+ triggers;
59
+ executor;
60
+ log;
61
+ baseUrl;
62
+ /** Event bus for real-time SSE streaming. Exposed for the router. */
63
+ eventBus;
64
+ /** Storage adapter for file attachments. Exposed for the router. */
65
+ storage;
66
+ /** When true, the upload route accepts executable MIME types / extensions. */
67
+ allowExecutableUploads;
68
+ /** IANA timezone applied to every cron-triggered workflow. */
69
+ defaultCronTimezone;
70
+ constructor(opts) {
71
+ this.persistence = opts.persistence;
72
+ this.queue = opts.queue;
73
+ this.scheduler = opts.scheduler;
74
+ this.services = opts.services;
75
+ this.steps = opts.steps;
76
+ this.triggers = opts.triggers;
77
+ this.executor = opts.executor;
78
+ this.log = opts.logger ?? noopLogger;
79
+ this.eventBus = opts.eventBus;
80
+ this.storage = opts.storage;
81
+ const config = opts.config ?? {};
82
+ this.baseUrl = config.baseUrl ?? '';
83
+ this.allowExecutableUploads = config.allowExecutableUploads ?? false;
84
+ this.defaultCronTimezone = config.defaultCronTimezone ?? 'Asia/Kolkata';
85
+ }
86
+ // ─── Validation ───
87
+ /**
88
+ * Validate a workflow config before saving.
89
+ *
90
+ * The host's save handler should call this and reject the save if
91
+ * `result.valid` is false. Returns structured issues the frontend
92
+ * can render inline in the workflow builder.
93
+ */
94
+ validateConfig(config) {
95
+ const validator = new ConfigValidator(this.triggers, this.steps);
96
+ return validator.validate(config);
97
+ }
98
+ // ─── Workflow Lifecycle ───
99
+ /**
100
+ * Activate a workflow's trigger.
101
+ *
102
+ * - WebhookTrigger: generates a unique path, stores it, calls onActivate()
103
+ * - CronTrigger: registers with the scheduler
104
+ * - EventTrigger / ManualTrigger: no activation needed (status change only)
105
+ */
106
+ async activateWorkflow(workflowId) {
107
+ const workflow = await this.persistence.getWorkflow(workflowId);
108
+ if (!workflow) {
109
+ throw new Error(`Workflow ${workflowId} not found`);
110
+ }
111
+ const config = parseWorkflowConfig(workflow.config);
112
+ if (!this.triggers.has(config.trigger.type)) {
113
+ throw new Error(`Trigger type "${config.trigger.type}" is not registered`);
114
+ }
115
+ const trigger = this.triggers.get(config.trigger.type);
116
+ if (trigger instanceof WebhookTrigger) {
117
+ const secret = generateSecret();
118
+ const path = `/webhooks/${workflowId}/${secret}`;
119
+ await this.persistence.storeWebhookPath(workflowId, path, secret);
120
+ if (trigger.onActivate) {
121
+ const credentials = await this.resolveCredentialsForTrigger(workflow);
122
+ await trigger.onActivate(config.trigger.config, {
123
+ webhookUrl: this.baseUrl + path,
124
+ credentials,
125
+ });
126
+ }
127
+ this.log.info(`activated webhook trigger for workflow ${workflowId}`);
128
+ }
129
+ else if (trigger instanceof CronTrigger) {
130
+ const { expression } = trigger.getSchedule(config.trigger.config);
131
+ await this.scheduler.schedule(workflowId, expression, this.defaultCronTimezone);
132
+ this.log.info(`activated cron trigger for workflow ${workflowId}: ${expression} (${this.defaultCronTimezone})`);
133
+ }
134
+ // EventTrigger and ManualTrigger require no activation
135
+ }
136
+ /**
137
+ * Deactivate a workflow's trigger.
138
+ */
139
+ async deactivateWorkflow(workflowId) {
140
+ const workflow = await this.persistence.getWorkflow(workflowId);
141
+ if (!workflow) {
142
+ throw new Error(`Workflow ${workflowId} not found`);
143
+ }
144
+ const config = parseWorkflowConfig(workflow.config);
145
+ if (!this.triggers.has(config.trigger.type))
146
+ return;
147
+ const trigger = this.triggers.get(config.trigger.type);
148
+ if (trigger instanceof WebhookTrigger) {
149
+ if (trigger.onDeactivate) {
150
+ const credentials = await this.resolveCredentialsForTrigger(workflow);
151
+ await trigger.onDeactivate(config.trigger.config, {
152
+ webhookUrl: '',
153
+ credentials,
154
+ });
155
+ }
156
+ await this.persistence.removeWebhookPath(workflowId);
157
+ this.log.info(`deactivated webhook trigger for workflow ${workflowId}`);
158
+ }
159
+ else if (trigger instanceof CronTrigger) {
160
+ await this.scheduler.unschedule(workflowId);
161
+ this.log.info(`deactivated cron trigger for workflow ${workflowId}`);
162
+ }
163
+ }
164
+ // ─── Workflow callback secret ───
165
+ /**
166
+ * Lazy-allocate the workflow's stable wait-callback secret. Idempotent —
167
+ * if a secret already exists, returns the existing record.
168
+ *
169
+ * Host calls this when saving a workflow that uses wait-step webhook
170
+ * mode, then surfaces the URL in the UI so the author can paste it into
171
+ * the external system's configuration.
172
+ */
173
+ async ensureWorkflowCallback(workflowId) {
174
+ const existing = await this.persistence.getWorkflowCallback(workflowId);
175
+ if (existing)
176
+ return existing;
177
+ const record = {
178
+ workflowId,
179
+ secret: generateSecret(),
180
+ createdAt: new Date(),
181
+ };
182
+ await this.persistence.storeWorkflowCallback(record);
183
+ return record;
184
+ }
185
+ /**
186
+ * Rotate the workflow's wait-callback secret. The old URL stops working
187
+ * immediately. Author has to update external configurations.
188
+ */
189
+ async rotateWorkflowCallback(workflowId) {
190
+ return this.persistence.rotateWorkflowCallback(workflowId, generateSecret());
191
+ }
192
+ /**
193
+ * Build the wait-callback URL template the author should paste into
194
+ * their external system. `{executionId}` is the placeholder the HTTP
195
+ * step substitutes via `{{ runtime.executionId }}`.
196
+ */
197
+ buildWaitCallbackUrl(secret, executionId) {
198
+ const base = `${this.baseUrl}/wait/callback/${secret}`;
199
+ return executionId ? `${base}/${executionId}` : `${base}/{executionId}`;
200
+ }
201
+ // ─── Trigger Firing ───
202
+ /**
203
+ * Dispatch a domain event. Finds all active workflows that match,
204
+ * hydrates payloads, applies filters, and enqueues executions.
205
+ *
206
+ * Returns the execution IDs that were created.
207
+ */
208
+ async dispatchEvent(event) {
209
+ const workflows = await this.persistence.findActiveWorkflows(event.type, event.metadata);
210
+ const executionIds = [];
211
+ for (const workflow of workflows) {
212
+ const config = parseWorkflowConfig(workflow.config);
213
+ if (!this.triggers.has(config.trigger.type))
214
+ continue;
215
+ const trigger = this.triggers.get(config.trigger.type);
216
+ if (!(trigger instanceof EventTrigger))
217
+ continue;
218
+ // Hydrate lightweight payload → full context
219
+ let payload = { ...event.payload };
220
+ if (trigger.hydratePayload) {
221
+ try {
222
+ payload = await trigger.hydratePayload(payload);
223
+ }
224
+ catch (err) {
225
+ this.log.warn(`dispatchEvent: hydratePayload failed for workflow ${workflow.id}: ${err instanceof Error ? err.message : String(err)}`);
226
+ continue;
227
+ }
228
+ }
229
+ // Filter — does this event match this workflow's trigger config?
230
+ if (!trigger.matchFilters(config.trigger.config, payload)) {
231
+ continue;
232
+ }
233
+ const execId = await this.createAndEnqueue(workflow, {
234
+ type: event.type,
235
+ ...payload,
236
+ });
237
+ executionIds.push(execId);
238
+ }
239
+ return executionIds;
240
+ }
241
+ /**
242
+ * Handle an incoming webhook request.
243
+ *
244
+ * Looks up the workflow by webhook path, delegates to the trigger's
245
+ * `handleRequest()`, and enqueues an execution.
246
+ */
247
+ async handleWebhook(path, request) {
248
+ const webhookRecord = await this.persistence.getWebhookByPath(path);
249
+ if (!webhookRecord) {
250
+ return {
251
+ executionId: '',
252
+ httpResponse: { status: 404, body: { error: 'Webhook not found' } },
253
+ };
254
+ }
255
+ const workflow = await this.persistence.getWorkflow(webhookRecord.workflowId);
256
+ if (!workflow || workflow.status !== 'ACTIVE') {
257
+ return {
258
+ executionId: '',
259
+ httpResponse: { status: 404, body: { error: 'Workflow not active' } },
260
+ };
261
+ }
262
+ const config = parseWorkflowConfig(workflow.config);
263
+ const trigger = this.triggers.get(config.trigger.type);
264
+ if (!(trigger instanceof WebhookTrigger)) {
265
+ return {
266
+ executionId: '',
267
+ httpResponse: {
268
+ status: 400,
269
+ body: { error: 'Workflow does not use a webhook trigger' },
270
+ },
271
+ };
272
+ }
273
+ const { triggerData, httpResponse } = await trigger.handleRequest(request, config.trigger.config);
274
+ const execId = await this.createAndEnqueue(workflow, {
275
+ type: config.trigger.type,
276
+ ...triggerData,
277
+ });
278
+ return { executionId: execId, httpResponse };
279
+ }
280
+ /**
281
+ * Handle an incoming wait-callback at `POST /wait/callback/<secret>/<executionId>`.
282
+ *
283
+ * The `secret` is the workflow's stable callback secret (generated at
284
+ * workflow create). The `executionId` is the in-flight execution that
285
+ * paused on a wait-webhook step — the workflow's HTTP step interpolated
286
+ * it into the URL it gave the external system via
287
+ * `{{ runtime.executionId }}`.
288
+ *
289
+ * Validation:
290
+ * 1. Secret resolves to a workflow (404 on miss, same hash-map security
291
+ * as trigger webhooks — wrong/unknown secrets are indistinguishable).
292
+ * 2. Execution exists, is EXTERNAL_WAIT, and belongs to that workflow
293
+ * (404 otherwise; same 404 so attackers can't probe for valid
294
+ * execution IDs by guessing).
295
+ *
296
+ * On success, the callback body becomes the resume payload and the
297
+ * execution is re-enqueued. The wait step's `onResume` sees it as
298
+ * `resumeData`.
299
+ */
300
+ async handleWaitCallback(secret, executionId, body) {
301
+ const callback = await this.persistence.getWorkflowCallbackBySecret(secret);
302
+ if (!callback) {
303
+ return { status: 404, body: { error: 'Wait callback not found' } };
304
+ }
305
+ const exec = await this.persistence.getExecution(executionId);
306
+ if (!exec ||
307
+ exec.workflowId !== callback.workflowId ||
308
+ exec.status !== 'EXTERNAL_WAIT') {
309
+ // Either the execution is wrong workflow, terminal, or unknown. Same
310
+ // 404 either way — don't leak which case.
311
+ return { status: 404, body: { error: 'Wait callback not found' } };
312
+ }
313
+ // Wrap the caller's body as a structured resume payload. Objects pass
314
+ // through; primitives get wrapped under `{ body }` so step code always
315
+ // sees a record.
316
+ const data = body && typeof body === 'object' && !Array.isArray(body)
317
+ ? body
318
+ : { body };
319
+ await this.resume(executionId, {
320
+ data: { action: 'approve', data },
321
+ });
322
+ return { status: 202, body: { received: true, executionId } };
323
+ }
324
+ /**
325
+ * Fire a workflow manually via API.
326
+ *
327
+ * `attachments` is validated against the trigger's `attachments` config
328
+ * (mime accept-list, maxCount, required). Validated refs are merged into
329
+ * the trigger payload as `context.trigger.attachments`.
330
+ *
331
+ * Returns the execution ID.
332
+ */
333
+ async triggerManual(workflowId, payload, attachments) {
334
+ const workflow = await this.persistence.getWorkflow(workflowId);
335
+ if (!workflow) {
336
+ throw new Error(`Workflow ${workflowId} not found`);
337
+ }
338
+ const config = parseWorkflowConfig(workflow.config);
339
+ const triggerCfg = (config.trigger.config ?? {});
340
+ const att = (triggerCfg['attachments'] ?? null);
341
+ if (att) {
342
+ const provided = attachments ?? [];
343
+ if (att.required && provided.length === 0) {
344
+ throw new Error('At least one attachment is required for this workflow');
345
+ }
346
+ if (att.maxCount !== undefined && provided.length > att.maxCount) {
347
+ throw new Error(`Too many attachments: ${String(provided.length)} > ${String(att.maxCount)}`);
348
+ }
349
+ for (const a of provided) {
350
+ if (att.accept && att.accept.length > 0 && !mimeAccepted(a.mimeType, att.accept)) {
351
+ throw new Error(`Attachment "${a.name}" mime "${a.mimeType}" is not in accept list`);
352
+ }
353
+ if (att.maxSize !== undefined && a.size !== undefined && a.size > att.maxSize) {
354
+ throw new Error(`Attachment "${a.name}" exceeds maxSize (${String(a.size)} > ${String(att.maxSize)})`);
355
+ }
356
+ }
357
+ }
358
+ else if (attachments && attachments.length > 0) {
359
+ // Workflow didn't declare an attachments slot — reject silently? Accept?
360
+ // We accept and pass through; the trigger schema simply doesn't constrain
361
+ // attachments. The host can choose to ignore them downstream.
362
+ }
363
+ const triggerPayload = {
364
+ type: config.trigger.type,
365
+ ...payload,
366
+ };
367
+ if (attachments && attachments.length > 0) {
368
+ triggerPayload['attachments'] = attachments;
369
+ }
370
+ return this.createAndEnqueue(workflow, triggerPayload);
371
+ }
372
+ /**
373
+ * Resume a paused execution.
374
+ *
375
+ * Stores the resume payload and re-enqueues the execution.
376
+ */
377
+ async resume(executionId, input) {
378
+ const exec = await this.persistence.getExecution(executionId);
379
+ if (!exec) {
380
+ throw new Error(`Execution ${executionId} not found`);
381
+ }
382
+ if (exec.status !== 'EXTERNAL_WAIT') {
383
+ throw new Error(`Execution ${executionId} is not paused (status: ${exec.status})`);
384
+ }
385
+ // Unified abort: either the explicit `abort` flag OR `data.action === 'abort'`.
386
+ const isAbort = input?.abort === true || input?.data?.action === 'abort';
387
+ if (isAbort) {
388
+ await this.persistence.updateExecutionStatus(executionId, 'CANCELLED');
389
+ this.eventBus?.emit(executionId, { event: 'execution_cancelled', data: {} });
390
+ this.log.info(`execution ${executionId} aborted by user`);
391
+ return;
392
+ }
393
+ if (input?.data) {
394
+ await this.persistence.persistResumePayload(executionId, input.data);
395
+ }
396
+ // Keep status as EXTERNAL_WAIT — the executor detects this as a resume
397
+ // and reads the saved pause path to continue from the correct step.
398
+ await this.queue.enqueue({ executionId });
399
+ this.log.info(`execution ${executionId} resume enqueued`);
400
+ }
401
+ /**
402
+ * Rerun a workflow from a specific step, using the source execution's
403
+ * cached outputs for all prior steps. Creates a new execution.
404
+ *
405
+ * Used for:
406
+ * - "Rerun from step N" — replay downstream with same upstream data
407
+ * - "Modify input and rerun" — pass `configOverrides` to change the
408
+ * entry step's input
409
+ * - Agent feedback after execution completed — pass `configOverrides`
410
+ * with new `userMessage`; agent receives `priorState.messages` and
411
+ * continues the conversation
412
+ *
413
+ * Returns the new execution ID.
414
+ */
415
+ async rerunFromStep(sourceExecutionId, fromStepId, options) {
416
+ const newExecutionId = await this.executor.rerunFromStep(sourceExecutionId, fromStepId, options);
417
+ await this.queue.enqueue({ executionId: newExecutionId });
418
+ return newExecutionId;
419
+ }
420
+ // ─── CRUD (used by router) ───
421
+ /**
422
+ * List all workflows visible to the given auth metadata.
423
+ */
424
+ async listWorkflows(metadata) {
425
+ return this.persistence.listWorkflows(metadata);
426
+ }
427
+ /**
428
+ * Get a single workflow by ID.
429
+ */
430
+ async getWorkflow(workflowId) {
431
+ return this.persistence.getWorkflow(workflowId);
432
+ }
433
+ /**
434
+ * Create a new workflow definition.
435
+ *
436
+ * Validates the config, persists it, and optionally activates the trigger.
437
+ * Returns the created WorkflowRecord.
438
+ */
439
+ async createWorkflow(input) {
440
+ const validation = this.validateConfig(input.config);
441
+ if (!validation.valid) {
442
+ throw new Error(`Invalid workflow config: ${validation.issues.map((i) => i.message).join('; ')}`);
443
+ }
444
+ const metadata = { ...input.metadata, name: input.name };
445
+ const id = await this.persistence.createWorkflow({
446
+ status: 'ACTIVE',
447
+ config: JSON.stringify(input.config),
448
+ metadata: JSON.stringify(metadata),
449
+ eventType: input.config.trigger.type,
450
+ });
451
+ const record = await this.persistence.getWorkflow(id);
452
+ if (!record)
453
+ throw new Error('Failed to read created workflow');
454
+ // Auto-activate the trigger
455
+ try {
456
+ await this.activateWorkflow(id);
457
+ }
458
+ catch {
459
+ // Non-fatal — workflow is created but trigger may need manual activation
460
+ }
461
+ this.log.info(`created workflow ${id} (${input.name})`);
462
+ return record;
463
+ }
464
+ /**
465
+ * Update an existing workflow definition.
466
+ *
467
+ * Accepts partial updates: config, metadata, or both.
468
+ * If config changes, validates it and updates the trigger type.
469
+ */
470
+ async updateWorkflow(workflowId, input) {
471
+ const existing = await this.persistence.getWorkflow(workflowId);
472
+ if (!existing) {
473
+ throw new Error(`Workflow ${workflowId} not found`);
474
+ }
475
+ const update = {};
476
+ // Update config if provided
477
+ if (input.config) {
478
+ const validation = this.validateConfig(input.config);
479
+ if (!validation.valid) {
480
+ throw new Error(`Invalid workflow config: ${validation.issues.map((i) => i.message).join('; ')}`);
481
+ }
482
+ update.config = JSON.stringify(input.config);
483
+ update.eventType = input.config.trigger.type;
484
+ }
485
+ // Update metadata if name or metadata provided
486
+ if (input.name !== undefined || input.metadata !== undefined) {
487
+ const existingMeta = JSON.parse(existing.metadata ?? '{}');
488
+ if (input.metadata)
489
+ Object.assign(existingMeta, input.metadata);
490
+ if (input.name !== undefined)
491
+ existingMeta['name'] = input.name;
492
+ update.metadata = JSON.stringify(existingMeta);
493
+ }
494
+ await this.persistence.updateWorkflow(workflowId, update);
495
+ const updated = await this.persistence.getWorkflow(workflowId);
496
+ if (!updated)
497
+ throw new Error('Failed to read updated workflow');
498
+ this.log.info(`updated workflow ${workflowId}`);
499
+ return updated;
500
+ }
501
+ /**
502
+ * List executions with optional filters and cursor pagination.
503
+ */
504
+ async listExecutions(params) {
505
+ return this.persistence.listExecutions(params);
506
+ }
507
+ /**
508
+ * Get execution detail including step records.
509
+ */
510
+ async getExecutionDetail(executionId) {
511
+ const exec = await this.persistence.getExecution(executionId);
512
+ if (!exec)
513
+ return null;
514
+ const stepRows = await this.persistence.getStepRows(executionId);
515
+ // Try to attach the workflow config snapshot
516
+ const result = { execution: exec, steps: stepRows };
517
+ const workflow = await this.persistence.getWorkflow(exec.workflowId);
518
+ if (workflow?.config) {
519
+ try {
520
+ result.config = JSON.parse(workflow.config);
521
+ }
522
+ catch {
523
+ // config is optional — skip if corrupt
524
+ }
525
+ }
526
+ // Extract trigger data from execution state context
527
+ try {
528
+ const state = await this.persistence.getExecutionState(executionId);
529
+ if (state?.context) {
530
+ const ctx = JSON.parse(state.context);
531
+ if (ctx.trigger) {
532
+ result.triggerData = ctx.trigger;
533
+ }
534
+ }
535
+ }
536
+ catch {
537
+ // trigger data is optional — skip if missing or corrupt
538
+ }
539
+ return result;
540
+ }
541
+ /**
542
+ * Cancel a running or paused execution.
543
+ */
544
+ async cancelExecution(executionId) {
545
+ const exec = await this.persistence.getExecution(executionId);
546
+ if (!exec) {
547
+ throw new Error(`Execution ${executionId} not found`);
548
+ }
549
+ if (exec.status === 'COMPLETED' || exec.status === 'FAILED' || exec.status === 'CANCELLED') {
550
+ throw new Error(`Execution ${executionId} is already terminal (status: ${exec.status})`);
551
+ }
552
+ await this.persistence.updateExecutionStatus(executionId, 'CANCELLED');
553
+ this.eventBus?.emit(executionId, { event: 'execution_cancelled', data: {} });
554
+ this.log.info(`execution ${executionId} cancelled`);
555
+ }
556
+ /**
557
+ * List credentials visible to the given auth metadata.
558
+ */
559
+ async listCredentials(metadata) {
560
+ return this.persistence.listCredentials(metadata);
561
+ }
562
+ /**
563
+ * Store a credential.
564
+ */
565
+ async createCredential(metadata, name, credential) {
566
+ await this.persistence.storeCredential(metadata, name, credential);
567
+ return { name, type: credential.type };
568
+ }
569
+ /**
570
+ * Delete a credential by name.
571
+ */
572
+ async deleteCredential(metadata, name) {
573
+ await this.persistence.deleteCredential(metadata, name);
574
+ }
575
+ // ─── Job Processing ───
576
+ /**
577
+ * The queue consumer body. The host's queue worker calls this for each job.
578
+ *
579
+ * ```ts
580
+ * workflowQueue.process((job) => runtime.processJob(job.data.executionId));
581
+ * ```
582
+ */
583
+ async processJob(executionId) {
584
+ const exec = await this.persistence.getExecution(executionId);
585
+ if (!exec)
586
+ return { status: 'not_found' };
587
+ if (!['PENDING', 'EXTERNAL_WAIT'].includes(exec.status)) {
588
+ return {
589
+ status: 'skipped',
590
+ reason: `execution status is ${exec.status}`,
591
+ };
592
+ }
593
+ try {
594
+ await this.executor.runExecution(executionId);
595
+ // Check final status after execution
596
+ const final = await this.persistence.getExecution(executionId);
597
+ if (!final)
598
+ return { status: 'not_found' };
599
+ if (final.status === 'EXTERNAL_WAIT') {
600
+ return { status: 'paused', reason: 'Step requested pause' };
601
+ }
602
+ if (final.status === 'COMPLETED') {
603
+ return { status: 'completed' };
604
+ }
605
+ if (final.status === 'FAILED') {
606
+ return { status: 'failed', error: 'Execution failed' };
607
+ }
608
+ if (final.status === 'SKIPPED') {
609
+ return { status: 'skipped', reason: 'Trigger filter mismatch' };
610
+ }
611
+ return { status: 'completed' };
612
+ }
613
+ catch (err) {
614
+ if (PauseStep.is(err)) {
615
+ // The executor already persisted the pause state
616
+ return { status: 'paused', reason: err.message };
617
+ }
618
+ const errMsg = err instanceof Error ? err.message : String(err);
619
+ this.log.error(`processJob ${executionId} failed: ${errMsg}`);
620
+ return { status: 'failed', error: errMsg };
621
+ }
622
+ }
623
+ /**
624
+ * Process a cron tick for a workflow.
625
+ *
626
+ * Called by the scheduler when a cron job fires. Checks poll() if available,
627
+ * otherwise fires unconditionally.
628
+ */
629
+ async processCronTick(workflowId) {
630
+ const workflow = await this.persistence.getWorkflow(workflowId);
631
+ if (!workflow || workflow.status !== 'ACTIVE')
632
+ return null;
633
+ const config = parseWorkflowConfig(workflow.config);
634
+ if (!this.triggers.has(config.trigger.type))
635
+ return null;
636
+ const trigger = this.triggers.get(config.trigger.type);
637
+ if (!(trigger instanceof CronTrigger))
638
+ return null;
639
+ const metadata = JSON.parse(workflow.metadata ?? '{}');
640
+ if (trigger.poll) {
641
+ const data = await trigger.poll(config.trigger.config, {
642
+ staticData: {
643
+ get: (key) => this.persistence.getStaticData(workflowId, key),
644
+ set: (key, value) => this.persistence.setStaticData(workflowId, key, value),
645
+ delete: (key) => this.persistence.deleteStaticData(workflowId, key),
646
+ },
647
+ services: this.services,
648
+ metadata,
649
+ });
650
+ if (data === null)
651
+ return null; // nothing new, skip
652
+ return this.createAndEnqueue(workflow, {
653
+ type: config.trigger.type,
654
+ ...data,
655
+ });
656
+ }
657
+ // Pure cron — always fire
658
+ const payload = trigger.buildPayload
659
+ ? trigger.buildPayload(config.trigger.config)
660
+ : { firedAt: new Date().toISOString() };
661
+ return this.createAndEnqueue(workflow, {
662
+ type: config.trigger.type,
663
+ ...payload,
664
+ });
665
+ }
666
+ // ─── Internal ───
667
+ async createAndEnqueue(workflow, triggerPayload) {
668
+ const metadata = JSON.parse(workflow.metadata ?? '{}');
669
+ const initialContext = {
670
+ workflow: {
671
+ id: workflow.id,
672
+ metadata,
673
+ },
674
+ trigger: triggerPayload,
675
+ steps: {},
676
+ };
677
+ const execId = await this.persistence.createExecution({
678
+ workflowId: workflow.id,
679
+ status: 'PENDING',
680
+ context: JSON.stringify(initialContext),
681
+ });
682
+ await this.queue.enqueue({ executionId: execId });
683
+ this.log.info(`created execution ${execId} for workflow ${workflow.id}`);
684
+ return execId;
685
+ }
686
+ async resolveCredentialsForTrigger(workflow) {
687
+ const metadata = JSON.parse(workflow.metadata ?? '{}');
688
+ const creds = await this.persistence.listCredentials(metadata);
689
+ const resolved = {};
690
+ for (const cred of creds) {
691
+ const full = await this.persistence.resolveCredential(metadata, cred.name);
692
+ if (full) {
693
+ resolved[cred.name] = full;
694
+ }
695
+ }
696
+ return resolved;
697
+ }
698
+ }
699
+ //# sourceMappingURL=workflow-runtime.js.map