workflow 5.0.0-beta.2 → 5.0.0-beta.21

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 (170) hide show
  1. package/dist/api-workflow.d.ts +1 -1
  2. package/dist/api-workflow.d.ts.map +1 -1
  3. package/dist/api-workflow.js +2 -2
  4. package/dist/api.d.ts +5 -1
  5. package/dist/api.d.ts.map +1 -1
  6. package/dist/api.js +14 -2
  7. package/dist/internal/builtins.d.ts +17 -0
  8. package/dist/internal/builtins.d.ts.map +1 -1
  9. package/dist/internal/builtins.js +65 -1
  10. package/dist/observability.d.ts +1 -1
  11. package/dist/observability.js +2 -2
  12. package/dist/runtime.d.ts +1 -1
  13. package/dist/runtime.d.ts.map +1 -1
  14. package/dist/runtime.js +2 -2
  15. package/docs/ai/index.mdx +27 -23
  16. package/docs/api-reference/index.mdx +24 -0
  17. package/docs/api-reference/meta.json +8 -0
  18. package/docs/api-reference/vitest/index.mdx +28 -7
  19. package/docs/api-reference/workflow/create-hook.mdx +38 -0
  20. package/docs/api-reference/workflow/create-webhook.mdx +1 -0
  21. package/docs/api-reference/workflow/experimental-set-attributes.mdx +65 -0
  22. package/docs/api-reference/workflow/fetch.mdx +5 -0
  23. package/docs/api-reference/workflow/index.mdx +3 -0
  24. package/docs/api-reference/workflow-ai/durable-agent.mdx +7 -45
  25. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +7 -0
  26. package/docs/api-reference/workflow-api/get-run.mdx +6 -0
  27. package/docs/api-reference/workflow-api/index.mdx +6 -8
  28. package/docs/api-reference/workflow-api/resume-hook.mdx +57 -0
  29. package/docs/api-reference/workflow-api/start.mdx +13 -5
  30. package/docs/api-reference/workflow-astro/index.mdx +18 -0
  31. package/docs/api-reference/workflow-astro/meta.json +4 -0
  32. package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
  33. package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
  34. package/docs/api-reference/workflow-errors/index.mdx +85 -0
  35. package/docs/api-reference/workflow-errors/meta.json +5 -0
  36. package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
  37. package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
  38. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
  39. package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
  40. package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
  41. package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
  42. package/docs/api-reference/workflow-nest/index.mdx +31 -0
  43. package/docs/api-reference/workflow-nest/meta.json +9 -0
  44. package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
  45. package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
  46. package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
  47. package/docs/api-reference/workflow-next/with-workflow.mdx +56 -2
  48. package/docs/api-reference/workflow-nitro/index.mdx +59 -0
  49. package/docs/api-reference/workflow-nuxt/index.mdx +47 -0
  50. package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
  51. package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
  52. package/docs/api-reference/workflow-observability/index.mdx +64 -0
  53. package/docs/api-reference/workflow-observability/meta.json +11 -0
  54. package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
  55. package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
  56. package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
  57. package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
  58. package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
  59. package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
  60. package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +7 -10
  61. package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
  62. package/docs/api-reference/workflow-runtime/index.mdx +43 -0
  63. package/docs/api-reference/workflow-runtime/meta.json +12 -0
  64. package/docs/api-reference/workflow-runtime/set-world.mdx +49 -0
  65. package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +42 -0
  66. package/docs/api-reference/{workflow-api → workflow-runtime}/world/index.mdx +5 -8
  67. package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
  68. package/docs/api-reference/{workflow-api → workflow-runtime}/world/queue.mdx +2 -2
  69. package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +11 -4
  70. package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +2 -2
  71. package/docs/api-reference/workflow-serde/index.mdx +0 -1
  72. package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +1 -2
  73. package/docs/api-reference/workflow-serde/workflow-serialize.mdx +1 -2
  74. package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
  75. package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
  76. package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
  77. package/docs/api-reference/workflow-vite/index.mdx +18 -0
  78. package/docs/api-reference/workflow-vite/meta.json +4 -0
  79. package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
  80. package/docs/changelog/attributes-mvp.mdx +380 -0
  81. package/docs/changelog/eager-processing.mdx +269 -0
  82. package/docs/changelog/index.mdx +2 -1
  83. package/docs/changelog/lazy-event-creation.md +127 -0
  84. package/docs/changelog/meta.json +7 -1
  85. package/docs/changelog/resilient-start.mdx +31 -283
  86. package/docs/changelog/turbo-mode.md +87 -0
  87. package/docs/cookbook/advanced/child-workflows.mdx +315 -0
  88. package/docs/cookbook/advanced/meta.json +2 -3
  89. package/docs/cookbook/advanced/publishing-libraries.mdx +87 -29
  90. package/docs/cookbook/advanced/serializable-steps.mdx +17 -5
  91. package/docs/cookbook/advanced/upgrading-workflows.mdx +195 -0
  92. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +156 -0
  93. package/docs/cookbook/agent-patterns/durable-agent.mdx +11 -184
  94. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +150 -173
  95. package/docs/cookbook/agent-patterns/meta.json +1 -7
  96. package/docs/cookbook/common-patterns/batching.mdx +44 -118
  97. package/docs/cookbook/common-patterns/idempotency.mdx +36 -52
  98. package/docs/cookbook/common-patterns/meta.json +4 -4
  99. package/docs/cookbook/common-patterns/rate-limiting.mdx +1 -1
  100. package/docs/cookbook/common-patterns/saga.mdx +128 -33
  101. package/docs/cookbook/common-patterns/scheduling.mdx +77 -193
  102. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +155 -0
  103. package/docs/cookbook/common-patterns/timeouts.mdx +100 -0
  104. package/docs/cookbook/common-patterns/workflow-composition.mdx +117 -0
  105. package/docs/cookbook/index.mdx +14 -17
  106. package/docs/cookbook/integrations/ai-sdk.mdx +330 -142
  107. package/docs/cookbook/integrations/chat-sdk.mdx +264 -151
  108. package/docs/cookbook/integrations/sandbox.mdx +482 -81
  109. package/docs/cookbook/meta.json +1 -1
  110. package/docs/deploying/building-a-world.mdx +1 -1
  111. package/docs/deploying/world/postgres-world.mdx +5 -3
  112. package/docs/deploying/world/vercel-world.mdx +2 -0
  113. package/docs/errors/abort-signal-timeout-in-workflow.mdx +80 -0
  114. package/docs/errors/corrupted-event-log.mdx +5 -5
  115. package/docs/errors/hook-conflict.mdx +56 -4
  116. package/docs/errors/index.mdx +9 -0
  117. package/docs/errors/replay-divergence.mdx +27 -0
  118. package/docs/errors/runtime-decryption-failed.mdx +77 -0
  119. package/docs/errors/step-executed-multiple-times.mdx +23 -0
  120. package/docs/errors/step-not-registered.mdx +1 -1
  121. package/docs/foundations/cancellation.mdx +459 -0
  122. package/docs/foundations/errors-and-retries.mdx +7 -3
  123. package/docs/foundations/hooks.mdx +29 -0
  124. package/docs/foundations/idempotency.mdx +236 -11
  125. package/docs/foundations/index.mdx +3 -3
  126. package/docs/foundations/meta.json +3 -2
  127. package/docs/foundations/serialization.mdx +78 -42
  128. package/docs/foundations/starting-workflows.mdx +6 -2
  129. package/docs/foundations/streaming.mdx +14 -23
  130. package/docs/foundations/versioning.mdx +263 -0
  131. package/docs/getting-started/astro.mdx +6 -0
  132. package/docs/getting-started/index.mdx +6 -7
  133. package/docs/getting-started/meta.json +1 -0
  134. package/docs/getting-started/nestjs.mdx +9 -0
  135. package/docs/getting-started/next.mdx +5 -3
  136. package/docs/getting-started/nitro.mdx +22 -0
  137. package/docs/getting-started/sveltekit.mdx +6 -0
  138. package/docs/getting-started/tanstack-start.mdx +241 -0
  139. package/docs/how-it-works/cancellation.mdx +287 -0
  140. package/docs/how-it-works/code-transform.mdx +2 -2
  141. package/docs/how-it-works/encryption.mdx +2 -2
  142. package/docs/how-it-works/event-sourcing.mdx +2 -2
  143. package/docs/how-it-works/meta.json +2 -1
  144. package/docs/internal/index.mdx +21 -0
  145. package/docs/internal/meta.json +10 -0
  146. package/docs/internal/nitro-native-build.mdx +38 -0
  147. package/docs/internal/nitro-web-ui.mdx +24 -0
  148. package/docs/internal/serializable-abort-controller.mdx +148 -0
  149. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +63 -16
  150. package/docs/migration-guides/migrating-from-inngest.mdx +44 -22
  151. package/docs/migration-guides/migrating-from-temporal.mdx +43 -14
  152. package/docs/migration-guides/migrating-from-trigger-dev.mdx +59 -27
  153. package/docs/observability/attributes.mdx +87 -0
  154. package/docs/observability/index.mdx +25 -1
  155. package/docs/observability/meta.json +1 -1
  156. package/docs/observability/tracing.mdx +106 -0
  157. package/docs/testing/index.mdx +2 -2
  158. package/package.json +14 -13
  159. package/docs/api-reference/workflow-api/world/meta.json +0 -4
  160. package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
  161. package/docs/cookbook/advanced/custom-serialization.mdx +0 -168
  162. package/docs/cookbook/advanced/durable-objects.mdx +0 -148
  163. package/docs/cookbook/advanced/isomorphic-packages.mdx +0 -145
  164. package/docs/cookbook/agent-patterns/stop-workflow.mdx +0 -216
  165. package/docs/cookbook/agent-patterns/tool-orchestration.mdx +0 -255
  166. package/docs/cookbook/agent-patterns/tool-streaming.mdx +0 -181
  167. package/docs/cookbook/common-patterns/child-workflows.mdx +0 -372
  168. package/docs/cookbook/common-patterns/content-router.mdx +0 -207
  169. package/docs/cookbook/common-patterns/fan-out.mdx +0 -208
  170. package/docs/foundations/common-patterns.mdx +0 -265
@@ -1,372 +0,0 @@
1
- ---
2
- title: Child Workflows
3
- description: Spawn child workflows from a parent and poll their progress for batch processing, report generation, and other multi-workflow orchestration scenarios.
4
- type: guide
5
- summary: Orchestrate independent child workflows from a parent workflow using start(), sleep(), and getRun() to fan out work with isolated failure boundaries.
6
- ---
7
-
8
- Use child workflows when a single workflow needs to orchestrate many independent units of work. Each child runs as its own workflow with a separate event log, retry boundary, and failure scope -- if one child fails, it doesn't take down the parent or siblings.
9
-
10
- ## When to use child workflows
11
-
12
- Child workflows are the right choice when:
13
-
14
- - **Work units are independent.** Each child can run without knowing about the others (e.g., processing individual documents, generating separate reports).
15
- - **You need isolated failure boundaries.** A failing child should not abort unrelated work. The parent decides how to handle failures.
16
- - **You want massive fan-out.** Spawning 50 or 500 children is practical because each runs on its own infrastructure.
17
- - **You need per-item observability.** Each child workflow has its own run ID, status, and event log for monitoring.
18
-
19
- For simpler cases where steps share a single event log, use [direct await composition](/docs/foundations/common-patterns#direct-await-flattening) instead.
20
-
21
- ## Basic pattern: spawn and poll
22
-
23
- The core pattern has three parts:
24
-
25
- 1. A **step** that calls `start()` to spawn a child workflow and returns the run ID
26
- 2. A **polling loop** in the parent workflow that checks child status with `getRun()`
27
- 3. A **step** that retrieves the child's return value once it completes
28
-
29
- ```typescript
30
- import { sleep } from "workflow";
31
- import { getRun, start } from "workflow/api";
32
-
33
- declare function pollUntilComplete(runIds: string[]): Promise<void>; // @setup
34
- declare function collectResults(runIds: string[]): Promise<Array<{ documentId: string; summary: string }>>; // @setup
35
-
36
- // Child workflow -- processes a single document
37
- export async function processDocument(documentId: string) {
38
- "use workflow";
39
-
40
- const content = await fetchDocument(documentId);
41
- const analysis = await analyzeContent(content);
42
- const summary = await generateSummary(analysis);
43
-
44
- return { documentId, summary };
45
- }
46
-
47
- async function fetchDocument(documentId: string): Promise<string> {
48
- "use step";
49
- const res = await fetch(`https://docs.example.com/api/${documentId}`);
50
- return res.text();
51
- }
52
-
53
- async function analyzeContent(content: string): Promise<string> {
54
- "use step";
55
- // Call analysis API
56
- return `analysis of ${content.length} chars`;
57
- }
58
-
59
- async function generateSummary(analysis: string): Promise<string> {
60
- "use step";
61
- // Generate summary from analysis
62
- return `Summary: ${analysis}`;
63
- }
64
-
65
- // Parent workflow -- orchestrates document processing
66
- export async function processDocumentBatch(documentIds: string[]) {
67
- "use workflow";
68
-
69
- // Spawn a child workflow for each document
70
- const runIds = await spawnChildren(documentIds);
71
-
72
- // Poll until all children complete
73
- await pollUntilComplete(runIds);
74
-
75
- // Collect results
76
- const results = await collectResults(runIds);
77
-
78
- return { processed: results.length, results };
79
- }
80
-
81
- async function spawnChildren(
82
- documentIds: string[]
83
- ): Promise<string[]> {
84
- "use step"; // [!code highlight]
85
-
86
- const runIds: string[] = [];
87
- for (const docId of documentIds) {
88
- const run = await start(processDocument, [docId]); // [!code highlight]
89
- runIds.push(run.runId);
90
- }
91
- return runIds;
92
- }
93
- ```
94
-
95
- ### Polling loop
96
-
97
- The parent workflow polls child statuses in a loop, sleeping between checks. This is durable -- if the parent replays, the sleep and status checks replay from the event log.
98
-
99
- ```typescript
100
- import { sleep } from "workflow";
101
- import { getRun } from "workflow/api";
102
-
103
- const POLL_INTERVAL = "30s";
104
- const MAX_POLL_ITERATIONS = 120; // 60 minutes at 30s intervals
105
-
106
- async function pollUntilComplete(runIds: string[]): Promise<void> {
107
- let iteration = 0;
108
-
109
- while (iteration < MAX_POLL_ITERATIONS) {
110
- const status = await checkStatuses(runIds); // [!code highlight]
111
-
112
- if (status.running === 0) {
113
- if (status.failed > 0) {
114
- throw new Error(
115
- `${status.failed} of ${runIds.length} children failed`
116
- );
117
- }
118
- return; // All completed successfully
119
- }
120
-
121
- iteration += 1;
122
- await sleep(POLL_INTERVAL); // [!code highlight]
123
- }
124
-
125
- throw new Error("Timed out waiting for children to complete");
126
- }
127
-
128
- async function checkStatuses(
129
- runIds: string[]
130
- ): Promise<{ running: number; completed: number; failed: number }> {
131
- "use step"; // [!code highlight]
132
-
133
- let running = 0;
134
- let completed = 0;
135
- let failed = 0;
136
-
137
- for (const runId of runIds) {
138
- const run = getRun(runId); // [!code highlight]
139
- const status = await run.status; // [!code highlight]
140
-
141
- if (status === "completed") completed += 1;
142
- else if (status === "failed" || status === "cancelled") failed += 1;
143
- else running += 1; // pending, running
144
- }
145
-
146
- return { running, completed, failed };
147
- }
148
-
149
- async function collectResults(
150
- runIds: string[]
151
- ): Promise<Array<{ documentId: string; summary: string }>> {
152
- "use step";
153
-
154
- const results = [];
155
- for (const runId of runIds) {
156
- const run = getRun(runId);
157
- const value = await run.returnValue;
158
- results.push(value as { documentId: string; summary: string });
159
- }
160
- return results;
161
- }
162
- ```
163
-
164
- ## Fan-out pattern: chunked spawning
165
-
166
- When spawning hundreds of children, batch the `start()` calls to avoid overwhelming the system. Use multiple spawn steps, each launching a chunk of children.
167
-
168
- ```typescript
169
- import { start } from "workflow/api";
170
-
171
- declare function pollUntilComplete(runIds: string[]): Promise<void>; // @setup
172
-
173
- const CHUNK_SIZE = 10;
174
-
175
- export async function largeReportBatch(reportConfigs: Array<{ id: string; query: string }>) {
176
- "use workflow";
177
-
178
- // Spawn children in chunks
179
- const allRunIds: string[] = [];
180
- for (let i = 0; i < reportConfigs.length; i += CHUNK_SIZE) {
181
- const chunk = reportConfigs.slice(i, i + CHUNK_SIZE);
182
- const runIds = await spawnReportChunk(chunk); // [!code highlight]
183
- allRunIds.push(...runIds);
184
- }
185
-
186
- // Poll until all complete
187
- await pollUntilComplete(allRunIds);
188
-
189
- const results = await collectReportResults(allRunIds);
190
- return { total: results.length, results };
191
- }
192
-
193
- async function spawnReportChunk(
194
- configs: Array<{ id: string; query: string }>
195
- ): Promise<string[]> {
196
- "use step";
197
-
198
- const runIds: string[] = [];
199
- for (const config of configs) {
200
- const run = await start(generateReport, [config.id, config.query]);
201
- runIds.push(run.runId);
202
- }
203
- return runIds;
204
- }
205
-
206
- async function generateReport(reportId: string, query: string) {
207
- "use workflow";
208
-
209
- const data = await queryDatabase(reportId, query);
210
- const formatted = await formatReport(reportId, data);
211
- return { reportId, formatted };
212
- }
213
-
214
- declare function queryDatabase(reportId: string, query: string): Promise<string>; // @setup
215
- declare function formatReport(reportId: string, data: string): Promise<string>; // @setup
216
-
217
- declare function collectReportResults(
218
- runIds: string[]
219
- ): Promise<Array<{ reportId: string; formatted: string }>>; // @setup
220
- ```
221
-
222
- ## Error handling
223
-
224
- ### Tolerating partial failures
225
-
226
- Not every batch requires 100% success. Use `allowFailures` logic to let the parent continue when some children fail, while still surfacing the failures.
227
-
228
- ```typescript
229
- import { sleep } from "workflow";
230
- import { getRun } from "workflow/api";
231
-
232
- const POLL_INTERVAL = "30s";
233
- const MAX_POLL_ITERATIONS = 120;
234
-
235
- async function pollWithPartialFailures(
236
- runIds: string[],
237
- maxFailureRate: number
238
- ): Promise<{ completed: string[]; failed: string[] }> {
239
- let iteration = 0;
240
- const completedIds: string[] = [];
241
- const failedIds: string[] = [];
242
-
243
- while (iteration < MAX_POLL_ITERATIONS) {
244
- const status = await checkDetailedStatuses(runIds);
245
-
246
- completedIds.length = 0;
247
- failedIds.length = 0;
248
-
249
- for (const entry of status) {
250
- if (entry.status === "completed") completedIds.push(entry.runId);
251
- else if (entry.status === "failed" || entry.status === "cancelled")
252
- failedIds.push(entry.runId);
253
- }
254
-
255
- const active = runIds.length - completedIds.length - failedIds.length;
256
-
257
- // Check if failure rate exceeds threshold
258
- const failureRate = failedIds.length / Math.max(1, runIds.length); // [!code highlight]
259
- if (failureRate > maxFailureRate) { // [!code highlight]
260
- throw new Error( // [!code highlight]
261
- `Failure rate ${(failureRate * 100).toFixed(1)}% exceeds ` + // [!code highlight]
262
- `threshold of ${(maxFailureRate * 100).toFixed(1)}%` // [!code highlight]
263
- ); // [!code highlight]
264
- } // [!code highlight]
265
-
266
- if (active === 0) {
267
- return { completed: completedIds, failed: failedIds };
268
- }
269
-
270
- iteration += 1;
271
- await sleep(POLL_INTERVAL);
272
- }
273
-
274
- throw new Error("Timed out waiting for children");
275
- }
276
-
277
- async function checkDetailedStatuses(
278
- runIds: string[]
279
- ): Promise<Array<{ runId: string; status: string }>> {
280
- "use step";
281
-
282
- const statuses = [];
283
- for (const runId of runIds) {
284
- const run = getRun(runId);
285
- const status = await run.status;
286
- statuses.push({ runId, status });
287
- }
288
- return statuses;
289
- }
290
- ```
291
-
292
- ### Retrying failed children
293
-
294
- When a child fails, the parent can spawn a replacement and continue polling. Track restart counts to prevent infinite retry loops.
295
-
296
- ```typescript
297
- import { sleep } from "workflow";
298
-
299
- declare function checkDetailedStatuses(runIds: string[]): Promise<Array<{ runId: string; status: string }>>; // @setup
300
-
301
- const POLL_INTERVAL = "30s";
302
- const MAX_POLL_ITERATIONS = 120;
303
-
304
- async function pollWithRetries(
305
- initialRunIds: string[],
306
- maxRestartsPerChild: number,
307
- spawnReplacement: (index: number) => Promise<string>
308
- ): Promise<void> {
309
- const activeRuns = new Map<number, string>();
310
- const restartCounts = new Map<number, number>();
311
-
312
- initialRunIds.forEach((runId, index) => activeRuns.set(index, runId));
313
-
314
- let iteration = 0;
315
-
316
- while (iteration < MAX_POLL_ITERATIONS) {
317
- const statuses = await checkDetailedStatuses(
318
- Array.from(activeRuns.values())
319
- );
320
- const statusByRunId = new Map(
321
- statuses.map((s) => [s.runId, s.status])
322
- );
323
-
324
- for (const [index, runId] of activeRuns.entries()) {
325
- const status = statusByRunId.get(runId) ?? "running";
326
-
327
- if (status === "completed") {
328
- activeRuns.delete(index);
329
- continue;
330
- }
331
-
332
- if (status === "failed" || status === "cancelled") {
333
- const restarts = (restartCounts.get(index) ?? 0) + 1; // [!code highlight]
334
- restartCounts.set(index, restarts); // [!code highlight]
335
-
336
- if (restarts > maxRestartsPerChild) { // [!code highlight]
337
- throw new Error( // [!code highlight]
338
- `Child ${index} exceeded restart limit (${maxRestartsPerChild})` // [!code highlight]
339
- ); // [!code highlight]
340
- } // [!code highlight]
341
-
342
- const newRunId = await spawnReplacement(index); // [!code highlight]
343
- activeRuns.set(index, newRunId); // [!code highlight]
344
- }
345
- }
346
-
347
- if (activeRuns.size === 0) return;
348
-
349
- iteration += 1;
350
- await sleep(POLL_INTERVAL);
351
- }
352
-
353
- throw new Error("Timed out waiting for children");
354
- }
355
- ```
356
-
357
- ## Tips
358
-
359
- - **`start()` must be called from a step**, not directly from a workflow function. Wrap it in a `"use step"` function.
360
- - **`getRun()` must also be called from a step.** The polling loop lives in the workflow, but the actual status check is a step.
361
- - **Set a max iteration count on polling loops** to prevent runaway workflows. Calculate the count from your expected max duration and poll interval.
362
- - **Use chunked spawning for large batches.** Spawning 500 children in a single step can time out. Break it into chunks of 10-50.
363
- - **Each child has its own retry semantics.** Steps inside child workflows retry independently. The parent only sees the child's final status.
364
- - **Use `deploymentId: "latest"`** if children should run on the most recent deployment. See the [`start()` API reference](/docs/api-reference/workflow-api/start#using-deploymentid-latest) for compatibility considerations.
365
-
366
- ## Key APIs
367
-
368
- - [`start()`](/docs/api-reference/workflow-api/start) -- spawn a new workflow run and get its run ID
369
- - [`getRun()`](/docs/api-reference/workflow-api/get-run) -- retrieve a workflow run's status and return value
370
- - [`sleep()`](/docs/api-reference/workflow/sleep) -- durably pause between polling iterations
371
- - [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
372
- - [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions with full Node.js access
@@ -1,207 +0,0 @@
1
- ---
2
- title: Conditional Routing
3
- description: Inspect a payload and route it to different step handlers based on its content.
4
- type: guide
5
- summary: Classify incoming messages and branch to specialized handlers using standard if/else logic in the workflow function.
6
- ---
7
-
8
- Use conditional routing when incoming messages need different processing paths depending on their content. A support ticket about billing goes to the billing handler; a bug report goes to engineering. The workflow inspects the payload and branches with standard JavaScript control flow.
9
-
10
- ## When to use this
11
-
12
- - Support ticket routing by category
13
- - Order processing with different flows per product type
14
- - Event handling where different event types need different logic
15
- - Any message-driven system where the handler depends on the content
16
-
17
- ## Pattern: Content-based router
18
-
19
- The workflow classifies the input, then branches with `if`/`else` to call the appropriate step:
20
-
21
- ```typescript
22
- declare function classifyTicket(ticketId: string, subject: string): Promise<{ ticketType: string }>; // @setup
23
- declare function handleBilling(ticketId: string): Promise<void>; // @setup
24
- declare function handleTechnical(ticketId: string): Promise<void>; // @setup
25
- declare function handleAccount(ticketId: string): Promise<void>; // @setup
26
- declare function handleFeedback(ticketId: string): Promise<void>; // @setup
27
-
28
- export async function routeTicket(ticketId: string, subject: string) {
29
- "use workflow";
30
-
31
- const { ticketType } = await classifyTicket(ticketId, subject); // [!code highlight]
32
-
33
- if (ticketType === "billing") { // [!code highlight]
34
- await handleBilling(ticketId);
35
- } else if (ticketType === "technical") {
36
- await handleTechnical(ticketId);
37
- } else if (ticketType === "account") {
38
- await handleAccount(ticketId);
39
- } else {
40
- await handleFeedback(ticketId);
41
- }
42
-
43
- return { ticketId, routedTo: ticketType };
44
- }
45
- ```
46
-
47
- ### Step functions
48
-
49
- Each handler is a separate `"use step"` function. The classification step can use an LLM, keyword matching, or any logic you need:
50
-
51
- ```typescript
52
- async function classifyTicket(
53
- ticketId: string,
54
- subject: string
55
- ): Promise<{ ticketType: string }> {
56
- "use step";
57
-
58
- // Example: simple keyword classification
59
- // In production, this could call an LLM or ML model
60
- const lower = subject.toLowerCase();
61
- if (lower.includes("invoice") || lower.includes("charge") || lower.includes("refund")) {
62
- return { ticketType: "billing" };
63
- }
64
- if (lower.includes("error") || lower.includes("bug") || lower.includes("crash")) {
65
- return { ticketType: "technical" };
66
- }
67
- if (lower.includes("password") || lower.includes("login") || lower.includes("access")) {
68
- return { ticketType: "account" };
69
- }
70
- return { ticketType: "feedback" };
71
- }
72
-
73
- async function handleBilling(ticketId: string): Promise<void> {
74
- "use step";
75
- // Look up billing records, process refund, etc.
76
- }
77
-
78
- async function handleTechnical(ticketId: string): Promise<void> {
79
- "use step";
80
- // Create bug report, notify engineering, etc.
81
- }
82
-
83
- async function handleAccount(ticketId: string): Promise<void> {
84
- "use step";
85
- // Reset password, update permissions, etc.
86
- }
87
-
88
- async function handleFeedback(ticketId: string): Promise<void> {
89
- "use step";
90
- // Log feedback, notify product team, etc.
91
- }
92
- ```
93
-
94
- ## Pattern: Enrichment before routing
95
-
96
- When downstream handlers need more context than the raw input provides, enrich the message in parallel before routing:
97
-
98
- ```typescript
99
- export async function enrichAndRoute(email: string) {
100
- "use workflow";
101
-
102
- // Step 1: Look up base data
103
- const contact = await lookupContact(email);
104
-
105
- // Step 2: Enrich from multiple sources in parallel
106
- const [crm, social] = await Promise.allSettled([ // [!code highlight]
107
- fetchCrmData(contact),
108
- fetchSocialData(contact),
109
- ]);
110
-
111
- const enriched = {
112
- ...contact,
113
- crm: crm.status === "fulfilled" ? crm.value : null,
114
- social: social.status === "fulfilled" ? social.value : null,
115
- };
116
-
117
- // Step 3: Route based on enriched data
118
- if (enriched.crm?.segment === "enterprise") { // [!code highlight]
119
- await routeToEnterpriseSales(enriched);
120
- } else {
121
- await routeToSelfServe(enriched);
122
- }
123
-
124
- return { email, segment: enriched.crm?.segment ?? "self-serve" };
125
- }
126
-
127
- async function lookupContact(email: string): Promise<{ email: string; domain: string }> {
128
- "use step";
129
- return { email, domain: email.split("@")[1] ?? "unknown" };
130
- }
131
-
132
- async function fetchCrmData(contact: { email: string }): Promise<{ segment: string }> {
133
- "use step";
134
- const res = await fetch(`https://crm.example.com/lookup?email=${contact.email}`);
135
- return res.json();
136
- }
137
-
138
- async function fetchSocialData(contact: { email: string }): Promise<{ followers: number }> {
139
- "use step";
140
- const res = await fetch(`https://social.example.com/lookup?email=${contact.email}`);
141
- return res.json();
142
- }
143
-
144
- async function routeToEnterpriseSales(enriched: unknown): Promise<void> {
145
- "use step";
146
- // Assign to enterprise sales team
147
- }
148
-
149
- async function routeToSelfServe(enriched: unknown): Promise<void> {
150
- "use step";
151
- // Add to self-serve onboarding flow
152
- }
153
- ```
154
-
155
- ## Pattern: Multiple event sources
156
-
157
- When a workflow must wait for signals from different systems before proceeding, create one hook per source and use `Promise.all` with a deadline:
158
-
159
- ```typescript
160
- import { defineHook, sleep } from "workflow";
161
-
162
- export const orderSignal = defineHook<{ ok: true }>();
163
-
164
- const SIGNALS = ["payment", "inventory", "fraud"] as const;
165
-
166
- export async function waitForAllSignals(orderId: string) {
167
- "use workflow";
168
-
169
- const hooks = SIGNALS.map((kind) =>
170
- orderSignal.create({ token: `${kind}:${orderId}` }) // [!code highlight]
171
- );
172
-
173
- const outcome = await Promise.race([ // [!code highlight]
174
- Promise.all(hooks).then(() => ({ type: "ready" as const })), // [!code highlight]
175
- sleep("5m").then(() => ({ type: "timeout" as const })), // [!code highlight]
176
- ]);
177
-
178
- if (outcome.type === "timeout") {
179
- return { orderId, status: "timeout" };
180
- }
181
-
182
- await shipOrder(orderId);
183
- return { orderId, status: "shipped" };
184
- }
185
-
186
- async function shipOrder(orderId: string): Promise<void> {
187
- "use step";
188
- await fetch(`https://shipping.example.com/ship`, {
189
- method: "POST",
190
- body: JSON.stringify({ orderId }),
191
- });
192
- }
193
- ```
194
-
195
- ## Tips
196
-
197
- - **Workflow functions use standard JavaScript.** `if`/`else`, `switch`, ternaries -- any branching logic works. No special routing DSL needed.
198
- - **Each handler is an independent step.** This means each gets its own retries, its own error handling, and its own entry in the event log.
199
- - **Combine with enrichment** when downstream handlers need data from multiple sources. Fan out enrichment with `Promise.allSettled`, then route on the merged result.
200
- - **Use `defineHook` for event gateways** when the routing decision depends on external signals arriving asynchronously.
201
-
202
- ## Key APIs
203
-
204
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) -- marks the orchestrator function
205
- - [`"use step"`](/docs/api-reference/workflow/use-step) -- marks each handler as a durable step
206
- - [`defineHook()`](/docs/api-reference/workflow/define-hook) -- creates hooks for event gateway patterns
207
- - [`sleep()`](/docs/api-reference/workflow/sleep) -- durable deadline for event gateways