ai-workflows 2.1.3 → 2.3.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 (188) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/CHANGELOG.md +8 -1
  3. package/README.md +2 -0
  4. package/dist/barrier.d.ts +6 -0
  5. package/dist/barrier.d.ts.map +1 -1
  6. package/dist/barrier.js +45 -7
  7. package/dist/barrier.js.map +1 -1
  8. package/dist/cascade-context.d.ts.map +1 -1
  9. package/dist/cascade-context.js +25 -25
  10. package/dist/cascade-context.js.map +1 -1
  11. package/dist/cascade-executor.d.ts.map +1 -1
  12. package/dist/cascade-executor.js +1 -1
  13. package/dist/cascade-executor.js.map +1 -1
  14. package/dist/context.d.ts.map +1 -1
  15. package/dist/context.js +23 -7
  16. package/dist/context.js.map +1 -1
  17. package/dist/cron-parser.d.ts +65 -0
  18. package/dist/cron-parser.d.ts.map +1 -0
  19. package/dist/cron-parser.js +294 -0
  20. package/dist/cron-parser.js.map +1 -0
  21. package/dist/cron-scheduler.d.ts +117 -0
  22. package/dist/cron-scheduler.d.ts.map +1 -0
  23. package/dist/cron-scheduler.js +176 -0
  24. package/dist/cron-scheduler.js.map +1 -0
  25. package/dist/database-context.d.ts +184 -0
  26. package/dist/database-context.d.ts.map +1 -0
  27. package/dist/database-context.js +428 -0
  28. package/dist/database-context.js.map +1 -0
  29. package/dist/digital-objects-adapter.d.ts +159 -0
  30. package/dist/digital-objects-adapter.d.ts.map +1 -0
  31. package/dist/digital-objects-adapter.js +229 -0
  32. package/dist/digital-objects-adapter.js.map +1 -0
  33. package/dist/durable-execution-cloudflare.d.ts +427 -0
  34. package/dist/durable-execution-cloudflare.d.ts.map +1 -0
  35. package/dist/durable-execution-cloudflare.js +510 -0
  36. package/dist/durable-execution-cloudflare.js.map +1 -0
  37. package/dist/durable-execution.d.ts +482 -0
  38. package/dist/durable-execution.d.ts.map +1 -0
  39. package/dist/durable-execution.js +594 -0
  40. package/dist/durable-execution.js.map +1 -0
  41. package/dist/durable-workflow.d.ts +176 -0
  42. package/dist/durable-workflow.d.ts.map +1 -0
  43. package/dist/durable-workflow.js +552 -0
  44. package/dist/durable-workflow.js.map +1 -0
  45. package/dist/graph/topological-sort.d.ts.map +1 -1
  46. package/dist/graph/topological-sort.js +5 -5
  47. package/dist/graph/topological-sort.js.map +1 -1
  48. package/dist/index.d.ts +4 -0
  49. package/dist/index.d.ts.map +1 -1
  50. package/dist/index.js +15 -0
  51. package/dist/index.js.map +1 -1
  52. package/dist/logger.d.ts +101 -0
  53. package/dist/logger.d.ts.map +1 -0
  54. package/dist/logger.js +115 -0
  55. package/dist/logger.js.map +1 -0
  56. package/dist/on.d.ts.map +1 -1
  57. package/dist/on.js +3 -3
  58. package/dist/on.js.map +1 -1
  59. package/dist/runtime.d.ts +169 -0
  60. package/dist/runtime.d.ts.map +1 -0
  61. package/dist/runtime.js +275 -0
  62. package/dist/runtime.js.map +1 -0
  63. package/dist/send.d.ts.map +1 -1
  64. package/dist/send.js +4 -3
  65. package/dist/send.js.map +1 -1
  66. package/dist/telemetry.d.ts +150 -0
  67. package/dist/telemetry.d.ts.map +1 -0
  68. package/dist/telemetry.js +388 -0
  69. package/dist/telemetry.js.map +1 -0
  70. package/dist/timer-registry.d.ts +25 -0
  71. package/dist/timer-registry.d.ts.map +1 -1
  72. package/dist/timer-registry.js +42 -8
  73. package/dist/timer-registry.js.map +1 -1
  74. package/dist/types.d.ts +17 -6
  75. package/dist/types.d.ts.map +1 -1
  76. package/dist/types.js +1 -1
  77. package/dist/types.js.map +1 -1
  78. package/dist/worker/durable-step.d.ts +481 -0
  79. package/dist/worker/durable-step.d.ts.map +1 -0
  80. package/dist/worker/durable-step.js +606 -0
  81. package/dist/worker/durable-step.js.map +1 -0
  82. package/dist/worker/index.d.ts +106 -0
  83. package/dist/worker/index.d.ts.map +1 -0
  84. package/dist/worker/index.js +124 -0
  85. package/dist/worker/index.js.map +1 -0
  86. package/dist/worker/state-adapter.d.ts +230 -0
  87. package/dist/worker/state-adapter.d.ts.map +1 -0
  88. package/dist/worker/state-adapter.js +409 -0
  89. package/dist/worker/state-adapter.js.map +1 -0
  90. package/dist/worker/topological-executor.d.ts +282 -0
  91. package/dist/worker/topological-executor.d.ts.map +1 -0
  92. package/dist/worker/topological-executor.js +396 -0
  93. package/dist/worker/topological-executor.js.map +1 -0
  94. package/dist/worker/workflow-builder.d.ts +286 -0
  95. package/dist/worker/workflow-builder.d.ts.map +1 -0
  96. package/dist/worker/workflow-builder.js +565 -0
  97. package/dist/worker/workflow-builder.js.map +1 -0
  98. package/dist/worker.d.ts +800 -0
  99. package/dist/worker.d.ts.map +1 -0
  100. package/dist/worker.js +2428 -0
  101. package/dist/worker.js.map +1 -0
  102. package/dist/workflow-builder.d.ts +287 -0
  103. package/dist/workflow-builder.d.ts.map +1 -0
  104. package/dist/workflow-builder.js +762 -0
  105. package/dist/workflow-builder.js.map +1 -0
  106. package/dist/workflow.d.ts +14 -30
  107. package/dist/workflow.d.ts.map +1 -1
  108. package/dist/workflow.js +132 -292
  109. package/dist/workflow.js.map +1 -1
  110. package/examples/01-ecommerce-order-pipeline.ts +358 -0
  111. package/examples/02-content-moderation-cascade.ts +454 -0
  112. package/examples/03-scheduled-reporting-dependencies.ts +479 -0
  113. package/examples/04-database-persistence.ts +518 -0
  114. package/examples/README.md +173 -0
  115. package/package.json +30 -13
  116. package/src/__tests__/digital-objects-adapter.test.ts +274 -0
  117. package/src/__tests__/durable-workflow.test.ts +297 -0
  118. package/src/barrier.ts +48 -7
  119. package/src/cascade-context.ts +36 -29
  120. package/src/cascade-executor.ts +3 -2
  121. package/src/context.ts +41 -12
  122. package/src/cron-parser.ts +347 -0
  123. package/src/cron-scheduler.ts +239 -0
  124. package/src/database-context.ts +658 -0
  125. package/src/digital-objects-adapter.ts +351 -0
  126. package/src/durable-execution-cloudflare.ts +855 -0
  127. package/src/durable-execution.ts +1042 -0
  128. package/src/durable-workflow.ts +717 -0
  129. package/src/graph/topological-sort.ts +6 -8
  130. package/src/index.ts +69 -0
  131. package/src/logger.ts +148 -0
  132. package/src/on.ts +8 -9
  133. package/src/runtime.ts +436 -0
  134. package/src/send.ts +4 -5
  135. package/src/telemetry.ts +577 -0
  136. package/src/timer-registry.ts +44 -10
  137. package/src/types.ts +32 -17
  138. package/src/worker/durable-step.ts +976 -0
  139. package/src/worker/index.ts +216 -0
  140. package/src/worker/state-adapter.ts +589 -0
  141. package/src/worker/topological-executor.ts +625 -0
  142. package/src/worker/workflow-builder.ts +871 -0
  143. package/src/worker.ts +2906 -0
  144. package/src/workflow-builder.ts +1068 -0
  145. package/src/workflow.ts +188 -351
  146. package/test/barrier-join.test.ts +32 -24
  147. package/test/cascade-executor.test.ts +9 -16
  148. package/test/cron-parser.test.ts +314 -0
  149. package/test/cron-scheduler.test.ts +291 -0
  150. package/test/database-context.test.ts +770 -0
  151. package/test/db-provider-adapter.test.ts +862 -0
  152. package/test/durable-execution-cloudflare.test.ts +606 -0
  153. package/test/durable-execution-in-process.test.ts +286 -0
  154. package/test/durable-execution.test.ts +247 -0
  155. package/test/e2e/workflow-scenarios.e2e.test.ts +1039 -0
  156. package/test/integration.test.ts +442 -0
  157. package/test/rpc-surface.test.ts +946 -0
  158. package/test/runtime.test.ts +262 -0
  159. package/test/schedule-timer-cleanup.test.ts +30 -21
  160. package/test/send-race-conditions.test.ts +30 -40
  161. package/test/worker/durable-cascade.test.ts +1117 -0
  162. package/test/worker/durable-step.test.ts +723 -0
  163. package/test/worker/topological-executor.test.ts +1240 -0
  164. package/test/worker/workflow-builder.test.ts +1067 -0
  165. package/test/worker.test.ts +608 -0
  166. package/test/workflow-builder.test.ts +1670 -0
  167. package/test/workflow-cron.test.ts +256 -0
  168. package/test/workflow-state-adapter.test.ts +923 -0
  169. package/test/workflow.test.ts +25 -22
  170. package/tsconfig.json +3 -1
  171. package/vitest.config.ts +38 -1
  172. package/vitest.workers.config.ts +44 -0
  173. package/wrangler.jsonc +22 -0
  174. package/.turbo/turbo-test.log +0 -169
  175. package/LICENSE +0 -21
  176. package/src/context.js +0 -83
  177. package/src/every.js +0 -267
  178. package/src/index.js +0 -71
  179. package/src/on.js +0 -79
  180. package/src/send.js +0 -111
  181. package/src/types.js +0 -4
  182. package/src/workflow.js +0 -455
  183. package/test/context.test.js +0 -116
  184. package/test/every.test.js +0 -282
  185. package/test/on.test.js +0 -80
  186. package/test/send.test.js +0 -89
  187. package/test/workflow.test.js +0 -224
  188. package/vitest.config.js +0 -7
@@ -0,0 +1,871 @@
1
+ /**
2
+ * WorkflowBuilder DSL - Declarative workflow definition with fluent API
3
+ *
4
+ * Provides a builder pattern for creating durable workflows with:
5
+ * - Step definitions with dependencies
6
+ * - Event triggers
7
+ * - Scheduled execution
8
+ * - Type-safe execution context
9
+ *
10
+ * ## Features
11
+ *
12
+ * - **Fluent API**: Chain methods for intuitive workflow definition
13
+ * - **Step Dependencies**: Declare execution order with `.dependsOn()`
14
+ * - **Event Triggers**: React to domain events with `.on('Noun.event')`
15
+ * - **Schedule Triggers**: Run on intervals with `.every()`
16
+ * - **Error Handling**: Per-step error handlers with `.onError()`
17
+ * - **Parallel Execution**: Steps without dependencies run concurrently
18
+ * - **Cycle Detection**: Automatically detects circular dependencies
19
+ *
20
+ * ## Basic Usage
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * import { WorkflowBuilder } from 'ai-workflows/worker'
25
+ *
26
+ * const workflow = WorkflowBuilder.create('order-process')
27
+ * .step('validate', async (input) => {
28
+ * return { valid: true }
29
+ * })
30
+ * .step('charge', async (input, ctx) => {
31
+ * const validation = ctx.getStepResult<{ valid: boolean }>('validate')
32
+ * if (!validation.valid) throw new Error('Invalid order')
33
+ * return { charged: true }
34
+ * }).dependsOn('validate')
35
+ * .step('fulfill', fulfillOrder).dependsOn('charge')
36
+ * .on('Order.placed').do('validate')
37
+ * .build()
38
+ *
39
+ * // Execute the workflow
40
+ * const results = await workflow.execute({ orderId: 'order-123' })
41
+ * ```
42
+ *
43
+ * ## With DurableStep
44
+ *
45
+ * @example
46
+ * ```typescript
47
+ * import { WorkflowBuilder, DurableStep } from 'ai-workflows/worker'
48
+ *
49
+ * const fetchData = new DurableStep('fetch-data', async ({ url }) => {
50
+ * return fetch(url).then(r => r.json())
51
+ * })
52
+ *
53
+ * const workflow = WorkflowBuilder.create('data-pipeline')
54
+ * .step(fetchData)
55
+ * .step('process', processData).dependsOn('fetch-data')
56
+ * .build()
57
+ * ```
58
+ *
59
+ * ## Scheduled Workflows
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * const workflow = WorkflowBuilder.create('daily-report')
64
+ * .step('generate', generateReport)
65
+ * .step('send', sendReport).dependsOn('generate')
66
+ * .every('day').at('9am').do('generate')
67
+ * .build()
68
+ * ```
69
+ *
70
+ * @packageDocumentation
71
+ */
72
+
73
+ import { DurableStep, type StepConfig, type RetryConfig, StepContext } from './durable-step.js'
74
+
75
+ // ============================================================================
76
+ // Type Definitions
77
+ // ============================================================================
78
+
79
+ /**
80
+ * Configuration for WorkflowBuilder
81
+ */
82
+ export interface WorkflowBuilderConfig {
83
+ /** Human-readable description of the workflow */
84
+ description?: string
85
+ /** Version string (semver recommended) */
86
+ version?: string
87
+ /** Default timeout for all steps */
88
+ timeout?: string | number
89
+ /** Default retry configuration for all steps */
90
+ retries?: RetryConfig
91
+ }
92
+
93
+ /**
94
+ * Step definition stored in the builder
95
+ */
96
+ export interface StepDefinition<TInput = unknown, TOutput = unknown> {
97
+ /** Unique step name */
98
+ name: string
99
+ /** The step function */
100
+ fn: (input: TInput, ctx: ExecutionContext) => Promise<TOutput>
101
+ /** Step configuration (retries, timeout) */
102
+ config?: StepConfig
103
+ /** Dependencies (step names this step depends on) */
104
+ dependencies: Array<{ name: string; options?: DependencyOptions }>
105
+ /** Error handler */
106
+ errorHandler?: (error: Error, ctx: ExecutionContext) => unknown | Promise<unknown>
107
+ /** DurableStep wrapper */
108
+ durableStep?: DurableStep<TInput, TOutput>
109
+ }
110
+
111
+ /**
112
+ * Options for dependency declarations
113
+ */
114
+ export interface DependencyOptions {
115
+ /** Dependency type: 'hard' (default) or 'soft' (can proceed on failure) */
116
+ type?: 'hard' | 'soft'
117
+ /** Timeout for waiting on dependency */
118
+ timeout?: string | number
119
+ }
120
+
121
+ /**
122
+ * Event trigger configuration
123
+ */
124
+ export interface TriggerConfig {
125
+ /** Event name (Noun.event format) */
126
+ event: string
127
+ /** Step name to trigger (or inline function was converted to implicit step) */
128
+ stepName: string
129
+ /** Filter function */
130
+ filter?: (event: unknown) => boolean
131
+ }
132
+
133
+ /**
134
+ * Schedule trigger configuration
135
+ */
136
+ export interface ScheduleConfig {
137
+ /** Schedule expression (interval, cron, or natural language) */
138
+ schedule: string
139
+ /** Numeric value for interval (e.g., 5 for every(5).minutes()) */
140
+ value?: number
141
+ /** Time of day (e.g., '9am') */
142
+ time?: string
143
+ /** Timezone (e.g., 'America/New_York') */
144
+ timezone?: string
145
+ /** Step name to execute */
146
+ stepName: string
147
+ }
148
+
149
+ /**
150
+ * Built workflow definition (immutable)
151
+ */
152
+ export interface BuiltWorkflow<TInput = unknown, TResults = Record<string, unknown>> {
153
+ /** Workflow name */
154
+ readonly name: string
155
+ /** All registered steps */
156
+ readonly steps: ReadonlyArray<StepDefinition>
157
+ /** Workflow triggers */
158
+ readonly triggers: {
159
+ readonly events: ReadonlyArray<TriggerConfig>
160
+ readonly schedules: ReadonlyArray<ScheduleConfig>
161
+ }
162
+ /** Dependency graph (step -> dependencies) */
163
+ readonly dependencyGraph: Map<string, string[]>
164
+ /** Topologically sorted execution order */
165
+ readonly executionOrder: string[]
166
+ /** Workflow metadata */
167
+ readonly metadata?: WorkflowBuilderConfig
168
+ /** Whether compatible with Cloudflare Workflows */
169
+ readonly isCloudflareCompatible: boolean
170
+ /** Execute the workflow */
171
+ execute: (input?: TInput) => Promise<TResults>
172
+ }
173
+
174
+ /**
175
+ * Execution context passed to step functions
176
+ */
177
+ export interface ExecutionContext {
178
+ /** Get result of a previously executed step */
179
+ getStepResult: <T = unknown>(stepName: string) => T
180
+ /** Current workflow input */
181
+ readonly input: unknown
182
+ /** All step results so far */
183
+ readonly results: Record<string, unknown>
184
+ }
185
+
186
+ /**
187
+ * Step chain for configuring a step after adding it
188
+ */
189
+ export interface StepChain {
190
+ /** Declare dependencies for this step */
191
+ dependsOn(step: string, options?: DependencyOptions): StepChain
192
+ dependsOn(steps: string[]): StepChain
193
+ /** Set timeout for this step */
194
+ timeout(duration: string | number): StepChain
195
+ /** Set retry configuration */
196
+ retries(config: RetryConfig): StepChain
197
+ /** Set error handler */
198
+ onError(handler: (error: Error, ctx: ExecutionContext) => unknown | Promise<unknown>): StepChain
199
+ /** Add another step (returns to builder) */
200
+ step<TI = unknown, TO = unknown>(
201
+ name: string,
202
+ fn: (input: TI, ctx: ExecutionContext) => Promise<TO>
203
+ ): StepChain
204
+ step<TI = unknown, TO = unknown>(
205
+ name: string,
206
+ config: StepConfig,
207
+ fn: (input: TI, ctx: ExecutionContext) => Promise<TO>
208
+ ): StepChain
209
+ step<TI = unknown, TO = unknown>(durableStep: DurableStep<TI, TO>): StepChain
210
+ /** Register event trigger */
211
+ on<T = unknown>(event: string): EventTriggerChain<T>
212
+ /** Register schedule trigger */
213
+ every(schedule: string): ScheduleTriggerChain
214
+ every(value: number): NumericScheduleChain
215
+ /** Build the workflow */
216
+ build(): BuiltWorkflow
217
+ }
218
+
219
+ /**
220
+ * Event trigger chain for configuring event-triggered steps
221
+ */
222
+ export interface EventTriggerChain<T = unknown> {
223
+ /** Add a filter function */
224
+ filter(predicate: (event: T) => boolean): EventTriggerChain<T>
225
+ /** Execute a step when event fires */
226
+ do(stepName: string): StepChain
227
+ do(fn: (event: T) => Promise<unknown>): StepChain
228
+ }
229
+
230
+ /**
231
+ * Schedule trigger chain for configuring scheduled steps
232
+ */
233
+ export interface ScheduleTriggerChain {
234
+ /** Set time of day */
235
+ at(time: string): ScheduleTriggerChain
236
+ /** Set timezone */
237
+ timezone(tz: string): ScheduleTriggerChain
238
+ /** Execute a step on schedule */
239
+ do(stepName: string): StepChain
240
+ do(fn: () => Promise<unknown>): StepChain
241
+ }
242
+
243
+ /**
244
+ * Numeric schedule chain (for every(5).minutes() pattern)
245
+ */
246
+ export interface NumericScheduleChain {
247
+ minutes(): ScheduleTriggerChain
248
+ hours(): ScheduleTriggerChain
249
+ days(): ScheduleTriggerChain
250
+ seconds(): ScheduleTriggerChain
251
+ }
252
+
253
+ // ============================================================================
254
+ // Implementation
255
+ // ============================================================================
256
+
257
+ /**
258
+ * Internal class implementing StepChain, EventTriggerChain, ScheduleTriggerChain
259
+ */
260
+ class WorkflowBuilderImpl
261
+ implements StepChain, EventTriggerChain, ScheduleTriggerChain, NumericScheduleChain
262
+ {
263
+ private _name: string
264
+ private _config?: WorkflowBuilderConfig
265
+ private _steps: Map<string, StepDefinition> = new Map()
266
+ private _eventTriggers: TriggerConfig[] = []
267
+ private _scheduleTriggers: ScheduleConfig[] = []
268
+ private _currentStep: string | null = null
269
+ private _implicitStepCounter = 0
270
+
271
+ // For event trigger chain
272
+ private _currentEvent: string | null = null
273
+ private _currentFilter: ((event: unknown) => boolean) | null = null
274
+
275
+ // For schedule trigger chain
276
+ private _currentSchedule: string | null = null
277
+ private _currentScheduleValue: number | null = null
278
+ private _currentTime: string | null = null
279
+ private _currentTimezone: string | null = null
280
+
281
+ constructor(name: string, config?: WorkflowBuilderConfig) {
282
+ if (!name || name.trim() === '') {
283
+ throw new Error('Workflow name is required')
284
+ }
285
+ this._name = name
286
+ if (config !== undefined) {
287
+ this._config = config
288
+ }
289
+ }
290
+
291
+ get name(): string {
292
+ return this._name
293
+ }
294
+
295
+ get config(): WorkflowBuilderConfig | undefined {
296
+ return this._config
297
+ }
298
+
299
+ // ==================== Step Methods ====================
300
+
301
+ step<TI = unknown, TO = unknown>(
302
+ nameOrStep: string | DurableStep<TI, TO>,
303
+ configOrFn?: StepConfig | ((input: TI, ctx: ExecutionContext) => Promise<TO>),
304
+ maybeFn?: (input: TI, ctx: ExecutionContext) => Promise<TO>
305
+ ): StepChain {
306
+ let name: string
307
+ let fn: (input: TI, ctx: ExecutionContext) => Promise<TO>
308
+ let stepConfig: StepConfig | undefined
309
+ let durableStep: DurableStep<TI, TO> | undefined
310
+
311
+ if (nameOrStep instanceof DurableStep) {
312
+ name = nameOrStep.name
313
+ fn = nameOrStep.fn as unknown as (input: TI, ctx: ExecutionContext) => Promise<TO>
314
+ stepConfig = nameOrStep.config
315
+ durableStep = nameOrStep
316
+ } else {
317
+ name = nameOrStep
318
+ if (typeof configOrFn === 'function') {
319
+ fn = configOrFn
320
+ } else {
321
+ stepConfig = configOrFn
322
+ fn = maybeFn!
323
+ }
324
+ }
325
+
326
+ // Check for duplicate step names
327
+ if (this._steps.has(name)) {
328
+ throw new Error(`Step "${name}" already exists`)
329
+ }
330
+
331
+ const stepDef: StepDefinition<TI, TO> = {
332
+ name,
333
+ fn,
334
+ ...(stepConfig !== undefined && { config: stepConfig }),
335
+ dependencies: [],
336
+ durableStep:
337
+ durableStep ??
338
+ new DurableStep(
339
+ name,
340
+ stepConfig ?? {},
341
+ fn as unknown as (input: TI, ctx?: StepContext) => Promise<TO>
342
+ ),
343
+ }
344
+
345
+ this._steps.set(name, stepDef as StepDefinition)
346
+ this._currentStep = name
347
+
348
+ return this
349
+ }
350
+
351
+ dependsOn(stepOrSteps: string | string[], options?: DependencyOptions): StepChain
352
+ dependsOn(...steps: (string | string[] | DependencyOptions | undefined)[]): StepChain {
353
+ if (!this._currentStep) {
354
+ throw new Error('No current step to add dependencies to')
355
+ }
356
+
357
+ const currentStepDef = this._steps.get(this._currentStep)
358
+ if (!currentStepDef) {
359
+ throw new Error(`Step "${this._currentStep}" not found`)
360
+ }
361
+
362
+ // Parse arguments
363
+ let dependencyNames: string[] = []
364
+ let dependencyOptions: DependencyOptions | undefined
365
+
366
+ for (const arg of steps) {
367
+ if (arg === undefined) continue
368
+ if (typeof arg === 'string') {
369
+ dependencyNames.push(arg)
370
+ } else if (Array.isArray(arg)) {
371
+ dependencyNames.push(...arg)
372
+ } else if (typeof arg === 'object' && ('type' in arg || 'timeout' in arg)) {
373
+ dependencyOptions = arg as DependencyOptions
374
+ }
375
+ }
376
+
377
+ // Add dependencies
378
+ for (const depName of dependencyNames) {
379
+ currentStepDef.dependencies.push({
380
+ name: depName,
381
+ ...(dependencyOptions !== undefined && { options: dependencyOptions }),
382
+ })
383
+ }
384
+
385
+ return this
386
+ }
387
+
388
+ timeout(duration: string | number): StepChain {
389
+ if (!this._currentStep) {
390
+ throw new Error('No current step to set timeout for')
391
+ }
392
+
393
+ const stepDef = this._steps.get(this._currentStep)
394
+ if (stepDef) {
395
+ stepDef.config = { ...stepDef.config, timeout: duration }
396
+ }
397
+
398
+ return this
399
+ }
400
+
401
+ retries(config: RetryConfig): StepChain {
402
+ if (!this._currentStep) {
403
+ throw new Error('No current step to set retries for')
404
+ }
405
+
406
+ const stepDef = this._steps.get(this._currentStep)
407
+ if (stepDef) {
408
+ stepDef.config = { ...stepDef.config, retries: config }
409
+ }
410
+
411
+ return this
412
+ }
413
+
414
+ onError(handler: (error: Error, ctx: ExecutionContext) => unknown | Promise<unknown>): StepChain {
415
+ if (!this._currentStep) {
416
+ throw new Error('No current step to set error handler for')
417
+ }
418
+
419
+ const stepDef = this._steps.get(this._currentStep)
420
+ if (stepDef) {
421
+ stepDef.errorHandler = handler
422
+ }
423
+
424
+ return this
425
+ }
426
+
427
+ // ==================== Event Trigger Methods ====================
428
+
429
+ on<T = unknown>(event: string): EventTriggerChain<T> {
430
+ // Validate event format (Noun.event)
431
+ const eventRegex = /^[A-Z][a-zA-Z]*\.[a-z][a-zA-Z]*$/
432
+ if (!eventRegex.test(event)) {
433
+ // Store invalid event - will throw when .do() is called
434
+ this._currentEvent = event
435
+ this._currentFilter = null
436
+ return this as unknown as EventTriggerChain<T>
437
+ }
438
+
439
+ this._currentEvent = event
440
+ this._currentFilter = null
441
+ return this as unknown as EventTriggerChain<T>
442
+ }
443
+
444
+ filter<T = unknown>(predicate: (event: T) => boolean): EventTriggerChain<T> {
445
+ this._currentFilter = predicate as (event: unknown) => boolean
446
+ return this as unknown as EventTriggerChain<T>
447
+ }
448
+
449
+ // ==================== Schedule Trigger Methods ====================
450
+
451
+ every(scheduleOrValue: string | number): ScheduleTriggerChain & NumericScheduleChain {
452
+ if (typeof scheduleOrValue === 'number') {
453
+ this._currentScheduleValue = scheduleOrValue
454
+ this._currentSchedule = null
455
+ } else {
456
+ this._currentSchedule = scheduleOrValue
457
+ this._currentScheduleValue = null
458
+ }
459
+ this._currentTime = null
460
+ this._currentTimezone = null
461
+ return this as unknown as ScheduleTriggerChain & NumericScheduleChain
462
+ }
463
+
464
+ minutes(): ScheduleTriggerChain {
465
+ if (this._currentScheduleValue !== null) {
466
+ this._currentSchedule = `${this._currentScheduleValue} minutes`
467
+ }
468
+ return this
469
+ }
470
+
471
+ hours(): ScheduleTriggerChain {
472
+ if (this._currentScheduleValue !== null) {
473
+ this._currentSchedule = `${this._currentScheduleValue} hours`
474
+ }
475
+ return this
476
+ }
477
+
478
+ days(): ScheduleTriggerChain {
479
+ if (this._currentScheduleValue !== null) {
480
+ this._currentSchedule = `${this._currentScheduleValue} days`
481
+ }
482
+ return this
483
+ }
484
+
485
+ seconds(): ScheduleTriggerChain {
486
+ if (this._currentScheduleValue !== null) {
487
+ this._currentSchedule = `${this._currentScheduleValue} seconds`
488
+ }
489
+ return this
490
+ }
491
+
492
+ at(time: string): ScheduleTriggerChain {
493
+ this._currentTime = time
494
+ return this
495
+ }
496
+
497
+ timezone(tz: string): ScheduleTriggerChain {
498
+ this._currentTimezone = tz
499
+ return this
500
+ }
501
+
502
+ /**
503
+ * Execute a step when event fires or on schedule
504
+ */
505
+ do(stepNameOrFn: string | ((event?: unknown) => Promise<unknown>)): StepChain {
506
+ // Handle event trigger
507
+ if (this._currentEvent !== null) {
508
+ const eventRegex = /^[A-Z][a-zA-Z]*\.[a-z][a-zA-Z]*$/
509
+ if (!eventRegex.test(this._currentEvent)) {
510
+ throw new Error(
511
+ `Invalid event name format: "${this._currentEvent}". Expected Noun.event format`
512
+ )
513
+ }
514
+
515
+ let stepName: string
516
+ if (typeof stepNameOrFn === 'function') {
517
+ // Create implicit step
518
+ stepName = `__implicit_${this._currentEvent.replace('.', '_')}_${++this
519
+ ._implicitStepCounter}`
520
+ this._steps.set(stepName, {
521
+ name: stepName,
522
+ fn: stepNameOrFn as (input: unknown, ctx: ExecutionContext) => Promise<unknown>,
523
+ dependencies: [],
524
+ })
525
+ } else {
526
+ stepName = stepNameOrFn
527
+ }
528
+
529
+ this._eventTriggers.push({
530
+ event: this._currentEvent,
531
+ stepName,
532
+ ...(this._currentFilter !== null && { filter: this._currentFilter }),
533
+ })
534
+
535
+ this._currentEvent = null
536
+ this._currentFilter = null
537
+ return this
538
+ }
539
+
540
+ // Handle schedule trigger
541
+ if (this._currentSchedule !== null) {
542
+ let stepName: string
543
+ if (typeof stepNameOrFn === 'function') {
544
+ // Create implicit step
545
+ stepName = `__implicit_schedule_${++this._implicitStepCounter}`
546
+ this._steps.set(stepName, {
547
+ name: stepName,
548
+ fn: stepNameOrFn as (input: unknown, ctx: ExecutionContext) => Promise<unknown>,
549
+ dependencies: [],
550
+ })
551
+ } else {
552
+ stepName = stepNameOrFn
553
+ }
554
+
555
+ this._scheduleTriggers.push({
556
+ schedule: this._currentSchedule,
557
+ ...(this._currentScheduleValue !== null && { value: this._currentScheduleValue }),
558
+ ...(this._currentTime !== null && { time: this._currentTime }),
559
+ ...(this._currentTimezone !== null && { timezone: this._currentTimezone }),
560
+ stepName,
561
+ })
562
+
563
+ this._currentSchedule = null
564
+ this._currentScheduleValue = null
565
+ this._currentTime = null
566
+ this._currentTimezone = null
567
+ return this
568
+ }
569
+
570
+ throw new Error('No event or schedule to trigger')
571
+ }
572
+
573
+ // ==================== Build Method ====================
574
+
575
+ build(): BuiltWorkflow {
576
+ // Validate event triggers reference existing steps
577
+ for (const trigger of this._eventTriggers) {
578
+ if (!this._steps.has(trigger.stepName)) {
579
+ throw new Error(`Step "${trigger.stepName}" not found for event trigger`)
580
+ }
581
+ }
582
+
583
+ // Validate schedule triggers reference existing steps
584
+ for (const trigger of this._scheduleTriggers) {
585
+ if (!this._steps.has(trigger.stepName)) {
586
+ throw new Error(`Step "${trigger.stepName}" not found for schedule trigger`)
587
+ }
588
+ }
589
+
590
+ // Build dependency graph
591
+ const dependencyGraph = new Map<string, string[]>()
592
+ for (const [name, stepDef] of this._steps) {
593
+ const deps = stepDef.dependencies.map((d) => d.name)
594
+ dependencyGraph.set(name, deps)
595
+
596
+ // Validate dependencies exist
597
+ for (const dep of deps) {
598
+ if (!this._steps.has(dep)) {
599
+ throw new Error(`Dependency "${dep}" not found for step "${name}"`)
600
+ }
601
+ }
602
+ }
603
+
604
+ // Check for circular dependencies
605
+ this.detectCircularDependencies(dependencyGraph)
606
+
607
+ // Compute topological sort
608
+ const executionOrder = this.topologicalSort(dependencyGraph)
609
+
610
+ // Create immutable copies
611
+ const steps = Array.from(this._steps.values()).map((s) => ({ ...s }))
612
+ const eventTriggers = [...this._eventTriggers]
613
+ const scheduleTriggers = [...this._scheduleTriggers]
614
+
615
+ const workflow: BuiltWorkflow = {
616
+ name: this._name,
617
+ steps: Object.freeze(steps),
618
+ triggers: Object.freeze({
619
+ events: Object.freeze(eventTriggers),
620
+ schedules: Object.freeze(scheduleTriggers),
621
+ }),
622
+ dependencyGraph: new Map(dependencyGraph),
623
+ executionOrder,
624
+ ...(this._config !== undefined && { metadata: this._config }),
625
+ isCloudflareCompatible: true,
626
+ execute: async (input?: unknown) => {
627
+ return this.executeWorkflow(steps, dependencyGraph, executionOrder, input)
628
+ },
629
+ }
630
+
631
+ return workflow
632
+ }
633
+
634
+ // ==================== Private Helpers ====================
635
+
636
+ private detectCircularDependencies(graph: Map<string, string[]>): void {
637
+ const visited = new Set<string>()
638
+ const recursionStack = new Set<string>()
639
+
640
+ const dfs = (node: string, path: string[]): void => {
641
+ visited.add(node)
642
+ recursionStack.add(node)
643
+
644
+ const deps = graph.get(node) || []
645
+ for (const dep of deps) {
646
+ if (!visited.has(dep)) {
647
+ dfs(dep, [...path, node])
648
+ } else if (recursionStack.has(dep)) {
649
+ // Check if it's self-referential
650
+ if (dep === node) {
651
+ throw new Error(
652
+ `Circular dependency detected: step "${node}" depends on itself (self-referential)`
653
+ )
654
+ }
655
+ throw new Error(`Circular dependency detected: ${[...path, node, dep].join(' -> ')}`)
656
+ }
657
+ }
658
+
659
+ recursionStack.delete(node)
660
+ }
661
+
662
+ for (const node of graph.keys()) {
663
+ if (!visited.has(node)) {
664
+ dfs(node, [])
665
+ }
666
+ }
667
+ }
668
+
669
+ private topologicalSort(graph: Map<string, string[]>): string[] {
670
+ const result: string[] = []
671
+ const visited = new Set<string>()
672
+ const temp = new Set<string>()
673
+
674
+ const visit = (node: string): void => {
675
+ if (temp.has(node)) {
676
+ throw new Error('Circular dependency detected')
677
+ }
678
+ if (visited.has(node)) return
679
+
680
+ temp.add(node)
681
+
682
+ const deps = graph.get(node) || []
683
+ for (const dep of deps) {
684
+ visit(dep)
685
+ }
686
+
687
+ temp.delete(node)
688
+ visited.add(node)
689
+ result.push(node)
690
+ }
691
+
692
+ for (const node of graph.keys()) {
693
+ visit(node)
694
+ }
695
+
696
+ return result
697
+ }
698
+
699
+ private async executeWorkflow(
700
+ steps: StepDefinition[],
701
+ dependencyGraph: Map<string, string[]>,
702
+ executionOrder: string[],
703
+ input?: unknown
704
+ ): Promise<Record<string, unknown>> {
705
+ const results: Record<string, unknown> = {}
706
+ const stepMap = new Map(steps.map((s) => [s.name, s]))
707
+
708
+ const ctx: ExecutionContext = {
709
+ input,
710
+ results,
711
+ getStepResult: <T = unknown>(stepName: string): T => {
712
+ if (!(stepName in results)) {
713
+ throw new Error(`Step "${stepName}" has not been executed yet`)
714
+ }
715
+ return results[stepName] as T
716
+ },
717
+ }
718
+
719
+ // Track which steps are completed
720
+ const completed = new Set<string>()
721
+ const inProgress = new Set<string>()
722
+ const pending = new Set(executionOrder)
723
+
724
+ // Execute steps respecting dependencies
725
+ while (pending.size > 0) {
726
+ // Find steps that can be executed (all dependencies satisfied)
727
+ const ready: string[] = []
728
+ for (const stepName of pending) {
729
+ const deps = dependencyGraph.get(stepName) || []
730
+ if (deps.every((d) => completed.has(d)) && !inProgress.has(stepName)) {
731
+ ready.push(stepName)
732
+ }
733
+ }
734
+
735
+ if (ready.length === 0 && pending.size > 0) {
736
+ throw new Error('Deadlock detected: no steps can be executed')
737
+ }
738
+
739
+ // Execute ready steps in parallel
740
+ const execPromises = ready.map(async (stepName) => {
741
+ inProgress.add(stepName)
742
+ const step = stepMap.get(stepName)!
743
+
744
+ try {
745
+ const result = await step.fn(input, ctx)
746
+ results[stepName] = result
747
+ } catch (error) {
748
+ if (step.errorHandler) {
749
+ results[stepName] = await step.errorHandler(error as Error, ctx)
750
+ } else {
751
+ throw error
752
+ }
753
+ } finally {
754
+ inProgress.delete(stepName)
755
+ completed.add(stepName)
756
+ pending.delete(stepName)
757
+ }
758
+ })
759
+
760
+ await Promise.all(execPromises)
761
+ }
762
+
763
+ return results
764
+ }
765
+
766
+ /**
767
+ * Register workflow with a service
768
+ */
769
+ registerWorkflow(workflow: BuiltWorkflow): { id: string } {
770
+ // This is handled by WorkflowServiceCore
771
+ return { id: workflow.name }
772
+ }
773
+ }
774
+
775
+ /**
776
+ * WorkflowBuilder - Static factory for creating workflow definitions
777
+ *
778
+ * @example
779
+ * ```ts
780
+ * const workflow = WorkflowBuilder.create('order-process')
781
+ * .step('validate', validateOrder)
782
+ * .step('charge', chargePayment).dependsOn('validate')
783
+ * .on('Order.placed').do('validate')
784
+ * .build()
785
+ * ```
786
+ */
787
+ export class WorkflowBuilder {
788
+ /** Workflow name */
789
+ readonly name: string
790
+ /** Workflow configuration */
791
+ readonly config?: WorkflowBuilderConfig
792
+
793
+ private impl: WorkflowBuilderImpl
794
+
795
+ private constructor(name: string, config?: WorkflowBuilderConfig) {
796
+ this.name = name
797
+ if (config !== undefined) {
798
+ this.config = config
799
+ }
800
+ this.impl = new WorkflowBuilderImpl(name, config)
801
+ }
802
+
803
+ /**
804
+ * Create a new workflow builder
805
+ *
806
+ * @param name - Unique workflow name
807
+ * @param config - Optional workflow configuration
808
+ * @returns WorkflowBuilder instance
809
+ */
810
+ static create(name: string, config?: WorkflowBuilderConfig): WorkflowBuilder {
811
+ return new WorkflowBuilder(name, config)
812
+ }
813
+
814
+ /**
815
+ * Add a step to the workflow
816
+ */
817
+ step<TI = unknown, TO = unknown>(
818
+ name: string,
819
+ fn: (input: TI, ctx: ExecutionContext) => Promise<TO>
820
+ ): StepChain
821
+ step<TI = unknown, TO = unknown>(
822
+ name: string,
823
+ config: StepConfig,
824
+ fn: (input: TI, ctx: ExecutionContext) => Promise<TO>
825
+ ): StepChain
826
+ step<TI = unknown, TO = unknown>(durableStep: DurableStep<TI, TO>): StepChain
827
+ step<TI = unknown, TO = unknown>(
828
+ nameOrStep: string | DurableStep<TI, TO>,
829
+ configOrFn?: StepConfig | ((input: TI, ctx: ExecutionContext) => Promise<TO>),
830
+ maybeFn?: (input: TI, ctx: ExecutionContext) => Promise<TO>
831
+ ): StepChain {
832
+ return this.impl.step(nameOrStep, configOrFn, maybeFn)
833
+ }
834
+
835
+ /**
836
+ * Register an event trigger
837
+ */
838
+ on<T = unknown>(event: string): EventTriggerChain<T> {
839
+ return this.impl.on<T>(event)
840
+ }
841
+
842
+ /**
843
+ * Register a schedule trigger
844
+ */
845
+ every(schedule: string): ScheduleTriggerChain
846
+ every(value: number): NumericScheduleChain
847
+ every(scheduleOrValue: string | number): ScheduleTriggerChain & NumericScheduleChain {
848
+ return this.impl.every(scheduleOrValue)
849
+ }
850
+
851
+ /**
852
+ * Declare dependencies for the most recently added step
853
+ * This allows for: builder.step('a', fn).step('b', fn).dependsOn('a')
854
+ */
855
+ dependsOn(step: string, options?: DependencyOptions): WorkflowBuilder
856
+ dependsOn(steps: string[]): WorkflowBuilder
857
+ dependsOn(stepOrSteps: string | string[], options?: DependencyOptions): WorkflowBuilder {
858
+ this.impl.dependsOn(stepOrSteps, options)
859
+ return this
860
+ }
861
+
862
+ /**
863
+ * Build the workflow definition
864
+ */
865
+ build(): BuiltWorkflow {
866
+ return this.impl.build()
867
+ }
868
+ }
869
+
870
+ // Re-export for convenience
871
+ export { DurableStep, type StepConfig, type RetryConfig }