@xyne/workflow-sdk 3.2.36 → 3.2.38

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 (261) hide show
  1. package/dist/agents/agent-step.d.ts +2 -2
  2. package/dist/agents/host-agent-step.d.ts +1 -1
  3. package/dist/builder/index.d.ts +2 -2
  4. package/dist/builder/index.d.ts.map +1 -1
  5. package/dist/builder/index.js +1 -1
  6. package/dist/builder/index.js.map +1 -1
  7. package/dist/client/events-client.d.ts +57 -0
  8. package/dist/client/events-client.d.ts.map +1 -0
  9. package/dist/client/events-client.js +107 -0
  10. package/dist/client/events-client.js.map +1 -0
  11. package/dist/client/index.d.ts +2 -0
  12. package/dist/client/index.d.ts.map +1 -1
  13. package/dist/client/sse.d.ts +30 -0
  14. package/dist/client/sse.d.ts.map +1 -0
  15. package/dist/client/sse.js +76 -0
  16. package/dist/client/sse.js.map +1 -0
  17. package/dist/client/types.d.ts +31 -6
  18. package/dist/client/types.d.ts.map +1 -1
  19. package/dist/client/url.d.ts +11 -0
  20. package/dist/client/url.d.ts.map +1 -0
  21. package/dist/client/url.js +28 -0
  22. package/dist/client/url.js.map +1 -0
  23. package/dist/client/workflow-client.d.ts +1 -1
  24. package/dist/client/workflow-client.d.ts.map +1 -1
  25. package/dist/client/workflow-client.js +32 -20
  26. package/dist/client/workflow-client.js.map +1 -1
  27. package/dist/common/citation-ref.d.ts +4 -4
  28. package/dist/common/credentials.d.ts +107 -0
  29. package/dist/common/credentials.d.ts.map +1 -1
  30. package/dist/common/credentials.js +67 -0
  31. package/dist/common/credentials.js.map +1 -1
  32. package/dist/common/index.d.ts +2 -2
  33. package/dist/common/index.d.ts.map +1 -1
  34. package/dist/common/index.js +2 -2
  35. package/dist/common/index.js.map +1 -1
  36. package/dist/connectors/builtin/builtin-connector.d.ts +32 -0
  37. package/dist/connectors/builtin/builtin-connector.d.ts.map +1 -0
  38. package/dist/connectors/builtin/builtin-connector.js +62 -0
  39. package/dist/connectors/builtin/builtin-connector.js.map +1 -0
  40. package/dist/connectors/builtin/credentials.d.ts +18 -0
  41. package/dist/connectors/builtin/credentials.d.ts.map +1 -0
  42. package/dist/connectors/builtin/credentials.js +50 -0
  43. package/dist/connectors/builtin/credentials.js.map +1 -0
  44. package/dist/connectors/builtin/steps/code.step.d.ts +99 -0
  45. package/dist/connectors/builtin/steps/code.step.d.ts.map +1 -0
  46. package/dist/connectors/builtin/steps/code.step.js +152 -0
  47. package/dist/connectors/builtin/steps/code.step.js.map +1 -0
  48. package/dist/connectors/builtin/steps/conditional.step.d.ts +182 -0
  49. package/dist/connectors/builtin/steps/conditional.step.d.ts.map +1 -0
  50. package/dist/connectors/builtin/steps/conditional.step.js +82 -0
  51. package/dist/connectors/builtin/steps/conditional.step.js.map +1 -0
  52. package/dist/connectors/builtin/steps/dedup.step.d.ts +65 -0
  53. package/dist/connectors/builtin/steps/dedup.step.d.ts.map +1 -0
  54. package/dist/connectors/builtin/steps/dedup.step.js +61 -0
  55. package/dist/connectors/builtin/steps/dedup.step.js.map +1 -0
  56. package/dist/connectors/builtin/steps/http-request.step.d.ts +1027 -0
  57. package/dist/connectors/builtin/steps/http-request.step.d.ts.map +1 -0
  58. package/dist/connectors/builtin/steps/http-request.step.js +497 -0
  59. package/dist/connectors/builtin/steps/http-request.step.js.map +1 -0
  60. package/dist/connectors/builtin/steps/loop.step.d.ts +100 -0
  61. package/dist/connectors/builtin/steps/loop.step.d.ts.map +1 -0
  62. package/dist/connectors/builtin/steps/loop.step.js +73 -0
  63. package/dist/connectors/builtin/steps/loop.step.js.map +1 -0
  64. package/dist/connectors/builtin/steps/map.step.d.ts +148 -0
  65. package/dist/connectors/builtin/steps/map.step.d.ts.map +1 -0
  66. package/dist/connectors/builtin/steps/map.step.js +111 -0
  67. package/dist/connectors/builtin/steps/map.step.js.map +1 -0
  68. package/dist/connectors/builtin/steps/parallel.step.d.ts +246 -0
  69. package/dist/connectors/builtin/steps/parallel.step.d.ts.map +1 -0
  70. package/dist/connectors/builtin/steps/parallel.step.js +175 -0
  71. package/dist/connectors/builtin/steps/parallel.step.js.map +1 -0
  72. package/dist/connectors/builtin/steps/ssrf-guard.d.ts +9 -0
  73. package/dist/connectors/builtin/steps/ssrf-guard.d.ts.map +1 -0
  74. package/dist/connectors/builtin/steps/ssrf-guard.js +115 -0
  75. package/dist/connectors/builtin/steps/ssrf-guard.js.map +1 -0
  76. package/dist/connectors/builtin/steps/switch.step.d.ts +235 -0
  77. package/dist/connectors/builtin/steps/switch.step.d.ts.map +1 -0
  78. package/dist/connectors/builtin/steps/switch.step.js +101 -0
  79. package/dist/connectors/builtin/steps/switch.step.js.map +1 -0
  80. package/dist/connectors/builtin/steps/wait.step.d.ts +1072 -0
  81. package/dist/connectors/builtin/steps/wait.step.d.ts.map +1 -0
  82. package/dist/connectors/builtin/steps/wait.step.js +254 -0
  83. package/dist/connectors/builtin/steps/wait.step.js.map +1 -0
  84. package/dist/connectors/builtin/triggers/default-cron-trigger.d.ts +44 -0
  85. package/dist/connectors/builtin/triggers/default-cron-trigger.d.ts.map +1 -0
  86. package/dist/connectors/builtin/triggers/default-cron-trigger.js +31 -0
  87. package/dist/connectors/builtin/triggers/default-cron-trigger.js.map +1 -0
  88. package/dist/connectors/builtin/triggers/default-event-trigger.d.ts +32 -0
  89. package/dist/connectors/builtin/triggers/default-event-trigger.d.ts.map +1 -0
  90. package/dist/connectors/builtin/triggers/default-event-trigger.js +21 -0
  91. package/dist/connectors/builtin/triggers/default-event-trigger.js.map +1 -0
  92. package/dist/connectors/builtin/triggers/default-manual-trigger.d.ts +45 -0
  93. package/dist/connectors/builtin/triggers/default-manual-trigger.d.ts.map +1 -0
  94. package/dist/connectors/builtin/triggers/default-manual-trigger.js +51 -0
  95. package/dist/connectors/builtin/triggers/default-manual-trigger.js.map +1 -0
  96. package/dist/connectors/builtin/triggers/default-webhook-v2-trigger.d.ts +31 -0
  97. package/dist/connectors/builtin/triggers/default-webhook-v2-trigger.d.ts.map +1 -0
  98. package/dist/connectors/builtin/triggers/default-webhook-v2-trigger.js +40 -0
  99. package/dist/connectors/builtin/triggers/default-webhook-v2-trigger.js.map +1 -0
  100. package/dist/connectors/core/base-connector.d.ts +45 -0
  101. package/dist/connectors/core/base-connector.d.ts.map +1 -0
  102. package/dist/connectors/core/base-connector.js +3 -0
  103. package/dist/connectors/core/base-connector.js.map +1 -0
  104. package/dist/connectors/core/connector-registry.d.ts +130 -0
  105. package/dist/connectors/core/connector-registry.d.ts.map +1 -0
  106. package/dist/connectors/core/connector-registry.js +357 -0
  107. package/dist/connectors/core/connector-registry.js.map +1 -0
  108. package/dist/connectors/core/credentials.d.ts +104 -0
  109. package/dist/connectors/core/credentials.d.ts.map +1 -0
  110. package/dist/connectors/core/credentials.js +67 -0
  111. package/dist/connectors/core/credentials.js.map +1 -0
  112. package/dist/connectors/core/index.d.ts +7 -0
  113. package/dist/connectors/core/index.d.ts.map +1 -0
  114. package/dist/connectors/core/index.js +5 -0
  115. package/dist/connectors/core/index.js.map +1 -0
  116. package/dist/engine/available-context.d.ts +3 -4
  117. package/dist/engine/available-context.d.ts.map +1 -1
  118. package/dist/engine/available-context.js +17 -13
  119. package/dist/engine/available-context.js.map +1 -1
  120. package/dist/engine/config-validator.d.ts +13 -5
  121. package/dist/engine/config-validator.d.ts.map +1 -1
  122. package/dist/engine/config-validator.js +89 -11
  123. package/dist/engine/config-validator.js.map +1 -1
  124. package/dist/engine/credential-resolver.d.ts +42 -0
  125. package/dist/engine/credential-resolver.d.ts.map +1 -0
  126. package/dist/engine/credential-resolver.js +63 -0
  127. package/dist/engine/credential-resolver.js.map +1 -0
  128. package/dist/engine/workflow-executor.d.ts +15 -9
  129. package/dist/engine/workflow-executor.d.ts.map +1 -1
  130. package/dist/engine/workflow-executor.js +31 -40
  131. package/dist/engine/workflow-executor.js.map +1 -1
  132. package/dist/events/in-memory-adapter.d.ts +15 -0
  133. package/dist/events/in-memory-adapter.d.ts.map +1 -0
  134. package/dist/events/in-memory-adapter.js +50 -0
  135. package/dist/events/in-memory-adapter.js.map +1 -0
  136. package/dist/events/types.d.ts +154 -0
  137. package/dist/events/types.d.ts.map +1 -0
  138. package/dist/events/types.js +34 -0
  139. package/dist/events/types.js.map +1 -0
  140. package/dist/index.d.ts +50 -31
  141. package/dist/index.d.ts.map +1 -1
  142. package/dist/index.js +39 -16
  143. package/dist/index.js.map +1 -1
  144. package/dist/ingest/body.d.ts +18 -0
  145. package/dist/ingest/body.d.ts.map +1 -0
  146. package/dist/ingest/body.js +47 -0
  147. package/dist/ingest/body.js.map +1 -0
  148. package/dist/ingest/filter.d.ts +11 -0
  149. package/dist/ingest/filter.d.ts.map +1 -0
  150. package/dist/ingest/filter.js +23 -0
  151. package/dist/ingest/filter.js.map +1 -0
  152. package/dist/ingest/headers.d.ts +3 -0
  153. package/dist/ingest/headers.d.ts.map +1 -0
  154. package/dist/ingest/headers.js +11 -0
  155. package/dist/ingest/headers.js.map +1 -0
  156. package/dist/ingest/types.d.ts +50 -0
  157. package/dist/ingest/types.d.ts.map +1 -0
  158. package/dist/ingest/types.js +9 -0
  159. package/dist/ingest/types.js.map +1 -0
  160. package/dist/persistence/in-memory-adapter.d.ts +4 -8
  161. package/dist/persistence/in-memory-adapter.d.ts.map +1 -1
  162. package/dist/persistence/in-memory-adapter.js +8 -25
  163. package/dist/persistence/in-memory-adapter.js.map +1 -1
  164. package/dist/persistence/types.d.ts +5 -19
  165. package/dist/persistence/types.d.ts.map +1 -1
  166. package/dist/router/types.d.ts +31 -3
  167. package/dist/router/types.d.ts.map +1 -1
  168. package/dist/router/workflow-router.d.ts +4 -3
  169. package/dist/router/workflow-router.d.ts.map +1 -1
  170. package/dist/router/workflow-router.js +258 -63
  171. package/dist/router/workflow-router.js.map +1 -1
  172. package/dist/runtime/types.d.ts +36 -56
  173. package/dist/runtime/types.d.ts.map +1 -1
  174. package/dist/runtime/workflow-runtime.d.ts +70 -46
  175. package/dist/runtime/workflow-runtime.d.ts.map +1 -1
  176. package/dist/runtime/workflow-runtime.js +217 -141
  177. package/dist/runtime/workflow-runtime.js.map +1 -1
  178. package/dist/steps/base-step.d.ts +23 -0
  179. package/dist/steps/base-step.d.ts.map +1 -1
  180. package/dist/steps/base-step.js +22 -0
  181. package/dist/steps/base-step.js.map +1 -1
  182. package/dist/steps/step-descriptor.d.ts +52 -0
  183. package/dist/steps/step-descriptor.d.ts.map +1 -0
  184. package/dist/steps/step-descriptor.js +32 -0
  185. package/dist/steps/step-descriptor.js.map +1 -0
  186. package/dist/testing/authz-conformance.d.ts.map +1 -1
  187. package/dist/testing/authz-conformance.js +6 -7
  188. package/dist/testing/authz-conformance.js.map +1 -1
  189. package/dist/testing/index.d.ts +1 -0
  190. package/dist/testing/index.d.ts.map +1 -1
  191. package/dist/testing/index.js +1 -0
  192. package/dist/testing/index.js.map +1 -1
  193. package/dist/testing/mock-step-context.d.ts +1 -0
  194. package/dist/testing/mock-step-context.d.ts.map +1 -1
  195. package/dist/testing/mock-step-context.js +1 -0
  196. package/dist/testing/mock-step-context.js.map +1 -1
  197. package/dist/testing/test-connector.d.ts +40 -0
  198. package/dist/testing/test-connector.d.ts.map +1 -0
  199. package/dist/testing/test-connector.js +39 -0
  200. package/dist/testing/test-connector.js.map +1 -0
  201. package/dist/triggers/api-trigger.d.ts +15 -0
  202. package/dist/triggers/api-trigger.d.ts.map +1 -0
  203. package/dist/triggers/api-trigger.js +14 -0
  204. package/dist/triggers/api-trigger.js.map +1 -0
  205. package/dist/triggers/base-trigger.d.ts +29 -0
  206. package/dist/triggers/base-trigger.d.ts.map +1 -1
  207. package/dist/triggers/base-trigger.js +16 -0
  208. package/dist/triggers/base-trigger.js.map +1 -1
  209. package/dist/triggers/trigger-descriptor.d.ts +42 -0
  210. package/dist/triggers/trigger-descriptor.d.ts.map +1 -0
  211. package/dist/triggers/trigger-descriptor.js +29 -0
  212. package/dist/triggers/trigger-descriptor.js.map +1 -0
  213. package/dist/triggers/webhook-trigger.d.ts +31 -38
  214. package/dist/triggers/webhook-trigger.d.ts.map +1 -1
  215. package/dist/triggers/webhook-trigger.js +19 -6
  216. package/dist/triggers/webhook-trigger.js.map +1 -1
  217. package/dist/types/known-types.d.ts +0 -1
  218. package/dist/types/known-types.d.ts.map +1 -1
  219. package/dist/types/known-types.js +0 -1
  220. package/dist/types/known-types.js.map +1 -1
  221. package/dist/types/validation.d.ts +3 -1
  222. package/dist/types/validation.d.ts.map +1 -1
  223. package/dist/types/validation.js +2 -0
  224. package/dist/types/validation.js.map +1 -1
  225. package/dist/types/workflow-config.d.ts +23 -0
  226. package/dist/types/workflow-config.d.ts.map +1 -1
  227. package/dist/types/workflow-config.js +8 -0
  228. package/dist/types/workflow-config.js.map +1 -1
  229. package/dist/util/credential-slot-schema.d.ts +20 -0
  230. package/dist/util/credential-slot-schema.d.ts.map +1 -0
  231. package/dist/util/credential-slot-schema.js +30 -0
  232. package/dist/util/credential-slot-schema.js.map +1 -0
  233. package/dist/util/event-channel.d.ts +25 -0
  234. package/dist/util/event-channel.d.ts.map +1 -0
  235. package/dist/util/event-channel.js +61 -0
  236. package/dist/util/event-channel.js.map +1 -0
  237. package/package.json +1 -1
  238. package/dist/common/expression-eval.d.ts +0 -35
  239. package/dist/common/expression-eval.d.ts.map +0 -1
  240. package/dist/common/expression-eval.js +0 -214
  241. package/dist/common/expression-eval.js.map +0 -1
  242. package/dist/common/principal.d.ts +0 -45
  243. package/dist/common/principal.d.ts.map +0 -1
  244. package/dist/common/principal.js +0 -9
  245. package/dist/common/principal.js.map +0 -1
  246. package/dist/steps/builtin/transform.step.d.ts +0 -247
  247. package/dist/steps/builtin/transform.step.d.ts.map +0 -1
  248. package/dist/steps/builtin/transform.step.js +0 -135
  249. package/dist/steps/builtin/transform.step.js.map +0 -1
  250. package/dist/types/attachment.d.ts +0 -23
  251. package/dist/types/attachment.d.ts.map +0 -1
  252. package/dist/types/attachment.js +0 -2
  253. package/dist/types/attachment.js.map +0 -1
  254. package/dist/types/resume-payload.d.ts +0 -34
  255. package/dist/types/resume-payload.d.ts.map +0 -1
  256. package/dist/types/resume-payload.js +0 -12
  257. package/dist/types/resume-payload.js.map +0 -1
  258. package/dist/util/executable-check.d.ts +0 -42
  259. package/dist/util/executable-check.d.ts.map +0 -1
  260. package/dist/util/executable-check.js +0 -115
  261. package/dist/util/executable-check.js.map +0 -1
@@ -11,16 +11,25 @@
11
11
  * - Resume paused executions
12
12
  * - Process queued jobs (the worker loop body)
13
13
  */
14
+ import { executionTopic, workflowTopic } from '../events/types.js';
14
15
  import { projectVariables } from '../types/workflow-variable.js';
15
16
  import { ConfigValidator } from '../engine/config-validator.js';
16
17
  import { validateTriggerPayload } from '../common/validate-payload.js';
17
18
  import { AccessDeniedError, ResourceNotFoundError } from '../authz/default.js';
18
19
  import { EventTrigger } from '../triggers/event-trigger.js';
19
20
  import { WebhookTrigger } from '../triggers/webhook-trigger.js';
21
+ import { ApiTrigger } from '../triggers/api-trigger.js';
22
+ import { CredentialResolver } from '../engine/credential-resolver.js';
23
+ import { headerValue } from '../ingest/headers.js';
24
+ import { parseInboundBody } from '../ingest/body.js';
25
+ import { matchesTriggerFilter } from '../ingest/filter.js';
20
26
  import { CronTrigger } from '../triggers/cron-trigger.js';
21
27
  import { PauseStep } from '../engine/pause-step.js';
22
28
  import { leafStepId } from '../engine/node-path.js';
23
29
  // ─── Helpers ───
30
+ function messageOf(err) {
31
+ return err instanceof Error ? err.message : String(err);
32
+ }
24
33
  const noopLogger = {
25
34
  info: () => { },
26
35
  warn: () => { },
@@ -31,6 +40,13 @@ function generateSecret() {
31
40
  crypto.getRandomValues(bytes);
32
41
  return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('');
33
42
  }
43
+ /** The trigger's author-supplied input schema, when it has one. */
44
+ function inputSchemaOf(config) {
45
+ const inputSchema = (config.trigger.config ?? {})['inputSchema'];
46
+ return inputSchema && typeof inputSchema === 'object' && !Array.isArray(inputSchema)
47
+ ? inputSchema
48
+ : undefined;
49
+ }
34
50
  function parseWorkflowConfig(raw) {
35
51
  return JSON.parse(raw ?? '{}');
36
52
  }
@@ -68,12 +84,12 @@ export class WorkflowRuntime {
68
84
  queue;
69
85
  scheduler;
70
86
  services;
71
- steps;
72
- triggers;
87
+ connectors;
73
88
  executor;
89
+ credentialResolver;
74
90
  log;
75
91
  baseUrl;
76
- /** Event bus for real-time SSE streaming. Exposed for the router. */
92
+ /** Pub/sub transport for live execution events. Exposed for the router's SSE stream. */
77
93
  eventBus;
78
94
  /** Storage adapter for file attachments. Exposed for the router. */
79
95
  storage;
@@ -94,9 +110,9 @@ export class WorkflowRuntime {
94
110
  this.queue = opts.queue;
95
111
  this.scheduler = opts.scheduler;
96
112
  this.services = opts.services;
97
- this.steps = opts.steps;
98
- this.triggers = opts.triggers;
113
+ this.connectors = opts.connectors;
99
114
  this.executor = opts.executor;
115
+ this.credentialResolver = new CredentialResolver(opts.persistence);
100
116
  this.log = opts.logger ?? noopLogger;
101
117
  this.eventBus = opts.eventBus;
102
118
  this.storage = opts.storage;
@@ -107,6 +123,43 @@ export class WorkflowRuntime {
107
123
  this.defaultCronTimezone = config.defaultCronTimezone ?? 'Asia/Kolkata';
108
124
  this.aiBuilder = config.aiBuilder;
109
125
  }
126
+ /**
127
+ * Publish to the bus. Fire-and-forget for the same reason the executor's
128
+ * `emitEvent` is: the bus may cross a network, and a mutation must not fail
129
+ * because the thing it was announcing itself to is unreachable. A lost
130
+ * notification costs a stale view until the next read; a failed write costs
131
+ * the user their work.
132
+ */
133
+ publish(topic, event) {
134
+ const bus = this.eventBus;
135
+ if (!bus)
136
+ return;
137
+ void bus.publish(topic, event).catch((err) => {
138
+ this.log.warn(`event bus publish failed (${event.event} on ${topic}): ${String(err)}`);
139
+ });
140
+ }
141
+ publishEvent(executionId, event) {
142
+ this.publish(executionTopic(executionId), event);
143
+ }
144
+ /**
145
+ * Announce that a workflow definition changed.
146
+ *
147
+ * Emitted from the runtime rather than the router on purpose: every writer goes
148
+ * through here — the builder, an agent's tool call, a host calling the runtime
149
+ * directly — so no caller can change a workflow without saying so. Put this in
150
+ * the HTTP layer instead and the first non-HTTP writer silently stops notifying.
151
+ */
152
+ publishWorkflowChange(workflowId, change, origin) {
153
+ this.publish(workflowTopic(workflowId), {
154
+ event: 'workflow_changed',
155
+ data: {
156
+ workflowId,
157
+ change,
158
+ ...(origin === undefined ? {} : { origin }),
159
+ at: new Date().toISOString(),
160
+ },
161
+ });
162
+ }
110
163
  // ─── Authorization primitives ───
111
164
  //
112
165
  // Every caller-facing operation goes through exactly one of these. There is no
@@ -209,21 +262,27 @@ export class WorkflowRuntime {
209
262
  * can render inline in the workflow builder.
210
263
  */
211
264
  validateConfig(config) {
212
- const validator = new ConfigValidator(this.triggers, this.steps);
265
+ const validator = new ConfigValidator(this.connectors);
213
266
  return validator.validate(config);
214
267
  }
215
268
  // ─── Workflow Lifecycle ───
216
269
  /**
217
270
  * Activate a workflow's trigger.
218
271
  *
219
- * - WebhookTrigger: generates a unique path, stores it, calls onActivate()
272
+ * - Every trigger: its credential slots must resolve to accepted credentials
220
273
  * - CronTrigger: registers with the scheduler
221
- * - EventTrigger / ManualTrigger: no activation needed (status change only)
274
+ *
275
+ * Only then is the workflow marked ACTIVE. Webhook ingest and event dispatch
276
+ * both key off that status, so it is what actually turns a workflow on.
222
277
  */
223
- async activateWorkflow(ctx, workflowId) {
278
+ async activateWorkflow(ctx, workflowId, opts) {
224
279
  // Guard up front, before any external side-effects (webhook/scheduler).
225
280
  const { record } = await this.authorizedWorkflow(ctx, workflowId, 'workflow:activate');
226
- return this.activateInternal(record);
281
+ await this.activateInternal(record);
282
+ await this.persistence.updateWorkflow(workflowId, { status: 'ACTIVE' });
283
+ // After the side-effects, never before: an activation that failed is not a
284
+ // change worth telling anyone about.
285
+ this.publishWorkflowChange(workflowId, 'activated', opts?.origin);
227
286
  }
228
287
  /**
229
288
  * The activation work, already authorized. Used by the guarded
@@ -233,20 +292,16 @@ export class WorkflowRuntime {
233
292
  async activateInternal(workflow) {
234
293
  const workflowId = workflow.id;
235
294
  const config = parseWorkflowConfig(workflow.config);
236
- if (!this.triggers.has(config.trigger.type)) {
295
+ if (!this.connectors.hasTrigger(config.trigger.type)) {
237
296
  throw new Error(`Trigger type "${config.trigger.type}" is not registered`);
238
297
  }
239
- const trigger = this.triggers.get(config.trigger.type);
298
+ const trigger = this.connectors.getTrigger(config.trigger.type);
299
+ // A missing, revoked or wrong-type credential fails here, naming the slot,
300
+ // rather than on the first event.
301
+ if (trigger.credentialSlots?.length) {
302
+ await this.credentialResolver.resolve(workflow.attributes, trigger, config.trigger.config);
303
+ }
240
304
  if (trigger instanceof WebhookTrigger) {
241
- const secret = generateSecret();
242
- const path = `/webhooks/${workflowId}/${secret}`;
243
- await this.persistence.storeWebhookPath(workflowId, path, secret);
244
- if (trigger.onActivate) {
245
- await trigger.onActivate(config.trigger.config, {
246
- webhookUrl: this.baseUrl + path,
247
- credentials: {},
248
- });
249
- }
250
305
  this.log.info(`activated webhook trigger for workflow ${workflowId}`);
251
306
  }
252
307
  else if (trigger instanceof CronTrigger) {
@@ -259,30 +314,27 @@ export class WorkflowRuntime {
259
314
  /**
260
315
  * Deactivate a workflow's trigger.
261
316
  */
262
- async deactivateWorkflow(ctx, workflowId) {
317
+ async deactivateWorkflow(ctx, workflowId, opts) {
263
318
  const workflow = await this.persistence.getWorkflow(workflowId);
264
319
  if (!workflow) {
265
320
  throw new Error(`Workflow ${workflowId} not found`);
266
321
  }
267
322
  await this.require(ctx, 'workflow:activate', this.workflowRef(workflow));
268
323
  const config = parseWorkflowConfig(workflow.config);
269
- if (!this.triggers.has(config.trigger.type))
270
- return;
271
- const trigger = this.triggers.get(config.trigger.type);
272
- if (trigger instanceof WebhookTrigger) {
273
- if (trigger.onDeactivate) {
274
- await trigger.onDeactivate(config.trigger.config, {
275
- webhookUrl: '',
276
- credentials: {},
277
- });
278
- }
279
- await this.persistence.removeWebhookPath(workflowId);
280
- this.log.info(`deactivated webhook trigger for workflow ${workflowId}`);
281
- }
282
- else if (trigger instanceof CronTrigger) {
324
+ // Falls through rather than returning early on an unregistered trigger type:
325
+ // the call still succeeded, and every successful call must leave by the same
326
+ // door so that none of them can quietly skip the notification below.
327
+ const trigger = this.connectors.hasTrigger(config.trigger.type)
328
+ ? this.connectors.getTrigger(config.trigger.type)
329
+ : null;
330
+ if (trigger instanceof CronTrigger) {
283
331
  await this.scheduler.unschedule(workflowId);
284
332
  this.log.info(`deactivated cron trigger for workflow ${workflowId}`);
285
333
  }
334
+ // Webhook, event and manual triggers need no teardown: webhook ingest and
335
+ // event dispatch only reach ACTIVE workflows, so the status below stops them.
336
+ await this.persistence.updateWorkflow(workflowId, { status: 'DISABLED' });
337
+ this.publishWorkflowChange(workflowId, 'deactivated', opts?.origin);
286
338
  }
287
339
  // ─── Workflow callback secret ───
288
340
  /**
@@ -333,9 +385,9 @@ export class WorkflowRuntime {
333
385
  const executionIds = [];
334
386
  for (const workflow of workflows) {
335
387
  const config = parseWorkflowConfig(workflow.config);
336
- if (!this.triggers.has(config.trigger.type))
388
+ if (!this.connectors.hasTrigger(config.trigger.type))
337
389
  continue;
338
- const trigger = this.triggers.get(config.trigger.type);
390
+ const trigger = this.connectors.getTrigger(config.trigger.type);
339
391
  if (!(trigger instanceof EventTrigger))
340
392
  continue;
341
393
  // Hydrate lightweight payload → full context
@@ -353,6 +405,10 @@ export class WorkflowRuntime {
353
405
  if (!trigger.matchFilters(config.trigger.config, payload)) {
354
406
  continue;
355
407
  }
408
+ // And the author's filter, before a run exists.
409
+ if (!matchesTriggerFilter(config.trigger.filter, payload, config.variables)) {
410
+ continue;
411
+ }
356
412
  const execId = await this.createAndEnqueue(workflow, {
357
413
  type: event.type,
358
414
  ...payload,
@@ -362,43 +418,80 @@ export class WorkflowRuntime {
362
418
  return executionIds;
363
419
  }
364
420
  /**
365
- * Handle an incoming webhook request.
421
+ * Ingest a request to a workflow's webhook URL, `<baseUrl>/webhooks/<workflowId>`.
422
+ *
423
+ * The order is fixed, and the trigger's own code runs only after the sender is
424
+ * authenticated:
425
+ *
426
+ * 1. The workflow exists, is ACTIVE and uses a webhook trigger — else 404, the
427
+ * same answer for each so workflow ids cannot be probed.
428
+ * 2. The trigger's credentials resolve — else 401, before any connector code runs.
429
+ * 3. The trigger's own `verify` accepts the request — else 401.
430
+ * 4. The body is parsed from the raw bytes — else 400.
431
+ * 5. The trigger turns the request into events and a reply.
432
+ * 6. Each event must pass the workflow's trigger filter, or it is dropped.
433
+ * 7. Survivors are enqueued before the reply is returned.
366
434
  *
367
- * Looks up the workflow by webhook path, delegates to the trigger's
368
- * `handleRequest()`, and enqueues an execution.
435
+ * Reasons for a rejection go to the log; responses stay generic.
369
436
  */
370
- async handleWebhook(path, request) {
371
- const webhookRecord = await this.persistence.getWebhookByPath(path);
372
- if (!webhookRecord) {
373
- return {
374
- executionId: '',
375
- httpResponse: { status: 404, body: { error: 'Webhook not found' } },
376
- };
437
+ async handleWebhook(workflowId, request) {
438
+ const notFound = {
439
+ reply: { status: 404, body: { error: 'Webhook not found' } },
440
+ executionIds: [],
441
+ };
442
+ const unauthorized = {
443
+ reply: { status: 401, body: { error: 'Unauthorized' } },
444
+ executionIds: [],
445
+ };
446
+ const workflow = await this.persistence.getWorkflow(workflowId);
447
+ if (!workflow || workflow.status !== 'ACTIVE')
448
+ return notFound;
449
+ const config = parseWorkflowConfig(workflow.config);
450
+ const trigger = this.connectors.findTrigger(config.trigger.type)?.trigger;
451
+ if (!(trigger instanceof WebhookTrigger))
452
+ return notFound;
453
+ const attributes = workflow.attributes;
454
+ const triggerConfig = config.trigger.config;
455
+ let ctx;
456
+ try {
457
+ const credentials = await this.credentialResolver.resolve(attributes, trigger, triggerConfig);
458
+ ctx = { credentials, attributes, baseUrl: this.baseUrl };
377
459
  }
378
- const workflow = await this.persistence.getWorkflow(webhookRecord.workflowId);
379
- if (!workflow || workflow.status !== 'ACTIVE') {
380
- return {
381
- executionId: '',
382
- httpResponse: { status: 404, body: { error: 'Workflow not active' } },
383
- };
460
+ catch (err) {
461
+ // Fail closed: verification never runs without the credentials it needs.
462
+ this.log.warn(`webhook ${workflowId}: rejected, ${messageOf(err)}`);
463
+ return unauthorized;
384
464
  }
385
- const config = parseWorkflowConfig(workflow.config);
386
- const trigger = this.triggers.get(config.trigger.type);
387
- if (!(trigger instanceof WebhookTrigger)) {
388
- return {
389
- executionId: '',
390
- httpResponse: {
391
- status: 400,
392
- body: { error: 'Workflow does not use a webhook trigger' },
393
- },
394
- };
465
+ let verified = false;
466
+ try {
467
+ verified = await trigger.verify(request, triggerConfig, ctx);
395
468
  }
396
- const { triggerData, httpResponse } = await trigger.handleRequest(request, config.trigger.config);
397
- const execId = await this.createAndEnqueue(workflow, {
398
- type: config.trigger.type,
399
- ...triggerData,
400
- }, { publicTrigger: true });
401
- return { executionId: execId, httpResponse };
469
+ catch (err) {
470
+ this.log.warn(`webhook ${workflowId}: verification threw: ${messageOf(err)}`);
471
+ }
472
+ if (!verified) {
473
+ this.log.warn(`webhook ${workflowId}: rejected by ${trigger.type} verification`);
474
+ return unauthorized;
475
+ }
476
+ let body;
477
+ try {
478
+ body = parseInboundBody(request.rawBody, headerValue(request.headers, 'content-type'));
479
+ }
480
+ catch (err) {
481
+ this.log.warn(`webhook ${workflowId}: rejected, ${messageOf(err)}`);
482
+ return { reply: { status: 400, body: { error: 'Malformed request body' } }, executionIds: [] };
483
+ }
484
+ const translation = await trigger.handleRequest({ ...request, body }, triggerConfig, ctx);
485
+ const executionIds = [];
486
+ for (const event of translation.events) {
487
+ if (!matchesTriggerFilter(config.trigger.filter, event.payload, config.variables))
488
+ continue;
489
+ executionIds.push(await this.createAndEnqueue(workflow, { type: event.type, ...event.payload }, { publicTrigger: true }));
490
+ }
491
+ return {
492
+ reply: translation.reply ?? { status: 200, body: { ok: true } },
493
+ executionIds,
494
+ };
402
495
  }
403
496
  /**
404
497
  * Handle an incoming wait-callback at `POST /wait/callback/<secret>/<executionId>`.
@@ -478,91 +571,67 @@ export class WorkflowRuntime {
478
571
  return this.createAndEnqueue(workflow, triggerPayload);
479
572
  }
480
573
  /**
481
- * Fire a workflow through the unauthenticated Webhook V2 API.
574
+ * Start a workflow from the API trigger route.
575
+ *
576
+ * No caller reaches the SDK here: the route has no session, and the SDK does
577
+ * not authorize whoever calls it. A host that exposes the route guards it
578
+ * itself, for example with an API key. The workflow must use an API trigger,
579
+ * and the payload must satisfy its input schema.
482
580
  *
483
- * This intentionally bypasses caller authorization because the host exposes
484
- * the route publicly. The workflow itself must opt in by using WEBHOOK_V2,
485
- * and the execution is marked as public so privileged steps can enforce their
486
- * own public-trigger restrictions.
581
+ * Runs are marked as public triggers, so privileged steps refuse them.
487
582
  */
488
- async triggerWebhookV2Public(workflowId, payload) {
489
- const workflow = await this.persistence.getWorkflow(workflowId);
490
- if (!workflow) {
491
- throw new Error(`Workflow ${workflowId} not found`);
492
- }
493
- const config = parseWorkflowConfig(workflow.config);
494
- if (config.trigger.type !== 'WEBHOOK_V2') {
495
- throw new Error(`Workflow ${workflowId} does not use a WEBHOOK_V2 trigger`);
496
- }
497
- const triggerCfg = (config.trigger.config ?? {});
498
- const inputSchema = triggerCfg['inputSchema'];
499
- const webhookV2InputSchema = inputSchema && Array.isArray(inputSchema['required'])
583
+ async triggerViaApi(workflowId, payload) {
584
+ const { workflow, config } = await this.apiTriggerWorkflow(workflowId);
585
+ const inputSchema = inputSchemaOf(config);
586
+ // Uploaded documents are optional on this route even when the schema lists
587
+ // them as required.
588
+ const apiInputSchema = inputSchema && Array.isArray(inputSchema['required'])
500
589
  ? {
501
590
  ...inputSchema,
502
591
  required: inputSchema['required'].filter((field) => field !== 'documents'),
503
592
  }
504
593
  : inputSchema;
505
- const errors = validateTriggerPayload(webhookV2InputSchema, payload);
594
+ const errors = validateTriggerPayload(apiInputSchema, payload);
506
595
  if (errors.length > 0) {
507
596
  throw new Error(`Invalid trigger payload: ${errors.map((e) => e.message).join('; ')}`);
508
597
  }
509
- return this.createAndEnqueue(workflow, {
510
- type: config.trigger.type,
511
- ...payload,
512
- }, { publicTrigger: true });
598
+ return this.createAndEnqueue(workflow, { type: config.trigger.type, ...payload }, { publicTrigger: true });
513
599
  }
514
600
  /**
515
- * Return the configured trigger input schema for routes that need to translate
516
- * transport-level payloads before calling `triggerManual()`.
601
+ * What the API trigger route needs before it builds a payload: the trigger's
602
+ * input schema, and the workflow's scope for storing uploaded files. Checked
603
+ * the same way as {@link triggerViaApi}.
517
604
  */
518
- async getWorkflowTriggerInputSchema(ctx, workflowId) {
519
- const workflow = await this.persistence.getWorkflow(workflowId);
520
- if (!workflow) {
521
- throw new Error(`Workflow ${workflowId} not found`);
522
- }
523
- await this.require(ctx, 'workflow:run', this.workflowRef(workflow));
524
- const config = parseWorkflowConfig(workflow.config);
525
- const triggerCfg = (config.trigger.config ?? {});
526
- const inputSchema = triggerCfg['inputSchema'];
527
- return inputSchema && typeof inputSchema === 'object' && !Array.isArray(inputSchema)
528
- ? inputSchema
529
- : undefined;
605
+ async getApiTriggerTarget(workflowId) {
606
+ const { workflow, config } = await this.apiTriggerWorkflow(workflowId);
607
+ return {
608
+ inputSchema: inputSchemaOf(config),
609
+ storageScope: workflow.attributes,
610
+ };
530
611
  }
531
- /**
532
- * Return trigger input schema for public transport routes.
533
- *
534
- * Unlike getWorkflowTriggerInputSchema(), this does not authorize a caller;
535
- * it only verifies the workflow exists and opted into the expected public
536
- * trigger type.
537
- */
538
- async getWorkflowTriggerInputSchemaPublic(workflowId, expectedTriggerType) {
612
+ /** The workflow exists and uses an API trigger. */
613
+ async apiTriggerWorkflow(workflowId) {
539
614
  const workflow = await this.persistence.getWorkflow(workflowId);
540
- if (!workflow) {
541
- throw new Error(`Workflow ${workflowId} not found`);
542
- }
615
+ if (!workflow)
616
+ throw new ResourceNotFoundError('Workflow', workflowId);
543
617
  const config = parseWorkflowConfig(workflow.config);
544
- if (config.trigger.type !== expectedTriggerType) {
545
- throw new Error(`Workflow ${workflowId} does not use a ${expectedTriggerType} trigger`);
618
+ const trigger = this.connectors.findTrigger(config.trigger.type)?.trigger;
619
+ if (!(trigger instanceof ApiTrigger)) {
620
+ throw new Error(`Workflow ${workflowId} does not use an API trigger`);
546
621
  }
547
- const triggerCfg = (config.trigger.config ?? {});
548
- const inputSchema = triggerCfg['inputSchema'];
549
- return inputSchema && typeof inputSchema === 'object' && !Array.isArray(inputSchema)
550
- ? inputSchema
551
- : undefined;
622
+ return { workflow, config };
552
623
  }
553
624
  /**
554
- * Return the workflow-owned storage scope for public transport routes.
625
+ * Return the configured trigger input schema for routes that need to translate
626
+ * transport-level payloads before calling `triggerManual()`.
555
627
  */
556
- async getWorkflowStorageScopePublic(workflowId, expectedTriggerType) {
628
+ async getWorkflowTriggerInputSchema(ctx, workflowId) {
557
629
  const workflow = await this.persistence.getWorkflow(workflowId);
558
630
  if (!workflow) {
559
631
  throw new Error(`Workflow ${workflowId} not found`);
560
632
  }
561
- const config = parseWorkflowConfig(workflow.config);
562
- if (config.trigger.type !== expectedTriggerType) {
563
- throw new Error(`Workflow ${workflowId} does not use a ${expectedTriggerType} trigger`);
564
- }
565
- return workflow.attributes;
633
+ await this.require(ctx, 'workflow:run', this.workflowRef(workflow));
634
+ return inputSchemaOf(parseWorkflowConfig(workflow.config));
566
635
  }
567
636
  /**
568
637
  * Resume a paused execution. Guards `execution:approve`, then does the work.
@@ -591,7 +660,7 @@ export class WorkflowRuntime {
591
660
  const isAbort = input?.abort === true || input?.data?.action === 'abort';
592
661
  if (isAbort) {
593
662
  await this.persistence.updateExecutionStatus(executionId, 'CANCELLED');
594
- this.eventBus?.emit(executionId, { event: 'execution_cancelled', data: {} });
663
+ this.publishEvent(executionId, { event: 'execution_cancelled', data: {} });
595
664
  this.log.info(`execution ${executionId} aborted by user`);
596
665
  return;
597
666
  }
@@ -696,7 +765,7 @@ export class WorkflowRuntime {
696
765
  * Validates the config, persists it, and optionally activates the trigger.
697
766
  * Returns the created WorkflowRecord.
698
767
  */
699
- async createWorkflow(ctx, input) {
768
+ async createWorkflow(ctx, input, opts) {
700
769
  // Create is an action on the *container*: the folder is the thing that
701
770
  // exists, so this same decision is what the UI projects onto its "New
702
771
  // workflow" button. Also proves the folder exists — no separate lookup.
@@ -720,11 +789,17 @@ export class WorkflowRuntime {
720
789
  // Internal post-create activation — already authorized by the create above.
721
790
  await this.activateInternal(record);
722
791
  }
723
- catch {
724
- // Non-fatal — workflow is created but trigger may need manual activation
792
+ catch (err) {
793
+ // Non-fatal — the workflow is created, and its trigger can be activated
794
+ // once whatever failed is fixed.
795
+ this.log.warn(`created workflow ${id} but could not activate its trigger: ${messageOf(err)}`);
725
796
  }
726
797
  }
727
798
  this.log.info(`created workflow ${id} (${input.name})`);
799
+ // One event, not also an `activated` one from the self-activation above: a
800
+ // subscriber re-reads and sees the settled state either way, and two frames
801
+ // for one call would just cost it a second read.
802
+ this.publishWorkflowChange(id, 'created', opts?.origin);
728
803
  return record;
729
804
  }
730
805
  /**
@@ -733,7 +808,7 @@ export class WorkflowRuntime {
733
808
  * Accepts partial updates: config, metadata, or both.
734
809
  * If config changes, validates it and updates the trigger type.
735
810
  */
736
- async updateWorkflow(ctx, workflowId, input) {
811
+ async updateWorkflow(ctx, workflowId, input, opts) {
737
812
  const { record: existing } = await this.authorizedWorkflow(ctx, workflowId, 'workflow:update');
738
813
  const update = {};
739
814
  if (input.summary !== undefined)
@@ -767,6 +842,7 @@ export class WorkflowRuntime {
767
842
  if (!updated)
768
843
  throw new Error('Failed to read updated workflow');
769
844
  this.log.info(`updated workflow ${workflowId}`);
845
+ this.publishWorkflowChange(workflowId, 'updated', opts?.origin);
770
846
  return updated;
771
847
  }
772
848
  // ─── Folders ───
@@ -814,13 +890,13 @@ export class WorkflowRuntime {
814
890
  return created;
815
891
  }
816
892
  /** Reorganize folders and workflows in one batch (drag-and-drop). */
817
- async moveBatch(ctx, input) {
893
+ async moveBatch(ctx, input, opts) {
818
894
  // Each item is authorized individually by the op it delegates to, which
819
895
  // guards both ends of the move: `*:update` on the thing moved and
820
896
  // `*:create` on the destination container.
821
897
  await Promise.all([
822
898
  ...input.folders.map((f) => this.updateFolder(ctx, f.id, { parentId: f.parentId })),
823
- ...input.workflows.map((w) => this.updateWorkflow(ctx, w.id, { folderId: w.folderId })),
899
+ ...input.workflows.map((w) => this.updateWorkflow(ctx, w.id, { folderId: w.folderId }, opts)),
824
900
  ]);
825
901
  }
826
902
  async updateFolder(ctx, id, input) {
@@ -1036,7 +1112,7 @@ export class WorkflowRuntime {
1036
1112
  throw new Error(`Execution ${executionId} is already terminal (status: ${exec.status})`);
1037
1113
  }
1038
1114
  await this.persistence.updateExecutionStatus(executionId, 'CANCELLED');
1039
- this.eventBus?.emit(executionId, { event: 'execution_cancelled', data: {} });
1115
+ this.publishEvent(executionId, { event: 'execution_cancelled', data: {} });
1040
1116
  this.log.info(`execution ${executionId} cancelled`);
1041
1117
  }
1042
1118
  /** List the credentials visible to `ctx` as safe, paginated summaries. */
@@ -1157,9 +1233,9 @@ export class WorkflowRuntime {
1157
1233
  if (!workflow || workflow.status !== 'ACTIVE')
1158
1234
  return null;
1159
1235
  const config = parseWorkflowConfig(workflow.config);
1160
- if (!this.triggers.has(config.trigger.type))
1236
+ if (!this.connectors.hasTrigger(config.trigger.type))
1161
1237
  return null;
1162
- const trigger = this.triggers.get(config.trigger.type);
1238
+ const trigger = this.connectors.getTrigger(config.trigger.type);
1163
1239
  if (!(trigger instanceof CronTrigger))
1164
1240
  return null;
1165
1241
  const metadata = JSON.parse(workflow.metadata ?? '{}');