workflow 5.0.0-beta.1 → 5.0.0-beta.2

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 (68) hide show
  1. package/README.md +4 -4
  2. package/dist/api-workflow.js +1 -1
  3. package/dist/api.js +1 -1
  4. package/dist/astro.js +1 -1
  5. package/dist/index.js +1 -1
  6. package/dist/internal/builtins.js +1 -1
  7. package/dist/internal/class-serialization.js +1 -1
  8. package/dist/internal/errors.js +1 -1
  9. package/dist/nest.js +1 -1
  10. package/dist/next.cjs +1 -1
  11. package/dist/nitro.js +1 -1
  12. package/dist/nuxt.js +1 -1
  13. package/dist/observability.js +1 -1
  14. package/dist/runtime.js +1 -1
  15. package/dist/stdlib.js +1 -1
  16. package/dist/sveltekit.js +1 -1
  17. package/dist/typescript-plugin.cjs +1 -1
  18. package/dist/vite.js +1 -1
  19. package/dist/workflow.js +1 -1
  20. package/docs/ai/resumable-streams.mdx +1 -1
  21. package/docs/api-reference/workflow/create-webhook.mdx +37 -18
  22. package/docs/api-reference/workflow/get-workflow-metadata.mdx +34 -0
  23. package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
  24. package/docs/api-reference/workflow-ai/index.mdx +0 -5
  25. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
  26. package/docs/cookbook/advanced/custom-serialization.mdx +168 -0
  27. package/docs/cookbook/advanced/durable-objects.mdx +148 -0
  28. package/docs/cookbook/advanced/isomorphic-packages.mdx +145 -0
  29. package/docs/cookbook/advanced/meta.json +10 -0
  30. package/docs/cookbook/advanced/publishing-libraries.mdx +279 -0
  31. package/docs/cookbook/advanced/serializable-steps.mdx +135 -0
  32. package/docs/cookbook/agent-patterns/durable-agent.mdx +191 -0
  33. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +278 -0
  34. package/docs/cookbook/agent-patterns/meta.json +10 -0
  35. package/docs/cookbook/agent-patterns/stop-workflow.mdx +216 -0
  36. package/docs/cookbook/agent-patterns/tool-orchestration.mdx +255 -0
  37. package/docs/cookbook/agent-patterns/tool-streaming.mdx +181 -0
  38. package/docs/cookbook/common-patterns/batching.mdx +179 -0
  39. package/docs/cookbook/common-patterns/child-workflows.mdx +372 -0
  40. package/docs/cookbook/common-patterns/content-router.mdx +207 -0
  41. package/docs/cookbook/common-patterns/fan-out.mdx +208 -0
  42. package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
  43. package/docs/cookbook/common-patterns/meta.json +15 -0
  44. package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
  45. package/docs/cookbook/common-patterns/saga.mdx +152 -0
  46. package/docs/cookbook/common-patterns/scheduling.mdx +249 -0
  47. package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
  48. package/docs/cookbook/index.mdx +41 -0
  49. package/docs/cookbook/integrations/ai-sdk.mdx +204 -0
  50. package/docs/cookbook/integrations/chat-sdk.mdx +203 -0
  51. package/docs/cookbook/integrations/meta.json +4 -0
  52. package/docs/cookbook/integrations/sandbox.mdx +128 -0
  53. package/docs/cookbook/meta.json +5 -0
  54. package/docs/deploying/world/local-world.mdx +1 -1
  55. package/docs/deploying/world/postgres-world.mdx +1 -1
  56. package/docs/deploying/world/vercel-world.mdx +1 -1
  57. package/docs/errors/start-invalid-workflow-function.mdx +1 -1
  58. package/docs/getting-started/index.mdx +8 -1
  59. package/docs/getting-started/meta.json +2 -1
  60. package/docs/getting-started/python.mdx +165 -0
  61. package/docs/meta.json +1 -0
  62. package/docs/migration-guides/index.mdx +34 -0
  63. package/docs/migration-guides/meta.json +9 -0
  64. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +311 -0
  65. package/docs/migration-guides/migrating-from-inngest.mdx +282 -0
  66. package/docs/migration-guides/migrating-from-temporal.mdx +284 -0
  67. package/docs/migration-guides/migrating-from-trigger-dev.mdx +296 -0
  68. package/package.json +13 -13
@@ -0,0 +1,179 @@
1
+ ---
2
+ title: Batching & Parallel Processing
3
+ description: Process large collections in parallel batches with failure isolation between groups.
4
+ type: guide
5
+ summary: Split items into fixed-size batches, process each batch concurrently with Promise.allSettled, and pace batches with sleep to avoid overloading downstream services.
6
+ ---
7
+
8
+ Use batching when you need to process a large list of items in parallel while controlling concurrency. Items are split into fixed-size batches, each batch runs concurrently, and failures in one batch don't affect others.
9
+
10
+ ## When to use this
11
+
12
+ - Processing hundreds or thousands of items (orders, images, records)
13
+ - Calling rate-limited APIs where you need to control concurrency
14
+ - Any fan-out where you want failure isolation between groups
15
+
16
+ ## Pattern
17
+
18
+ The workflow splits items into chunks and processes each chunk with `Promise.allSettled()`. A `sleep()` between chunks prevents overloading downstream services.
19
+
20
+ ```typescript
21
+ import { sleep } from "workflow";
22
+
23
+ declare function processItem(item: string): Promise<{ item: string; ok: boolean }>; // @setup
24
+
25
+ export async function processBatch(items: string[], batchSize: number = 5) {
26
+ "use workflow";
27
+
28
+ const results = [];
29
+
30
+ for (let i = 0; i < items.length; i += batchSize) {
31
+ const batch = items.slice(i, i + batchSize);
32
+
33
+ // Run batch in parallel -- failures are isolated
34
+ const outcomes = await Promise.allSettled( // [!code highlight]
35
+ batch.map((item) => processItem(item))
36
+ );
37
+
38
+ for (let j = 0; j < outcomes.length; j++) {
39
+ const outcome = outcomes[j];
40
+ results.push(
41
+ outcome.status === "fulfilled"
42
+ ? outcome.value
43
+ : { item: batch[j], ok: false, error: String(outcome.reason) }
44
+ );
45
+ }
46
+
47
+ // Pace between batches to avoid overload
48
+ if (i + batchSize < items.length) {
49
+ await sleep("1s"); // [!code highlight]
50
+ }
51
+ }
52
+
53
+ const succeeded = results.filter((r) => r.ok).length;
54
+ return { total: results.length, succeeded, failed: results.length - succeeded };
55
+ }
56
+ ```
57
+
58
+ ### Step function
59
+
60
+ Each item is processed in its own step, giving it full Node.js access and automatic retries.
61
+
62
+ ```typescript
63
+ async function processItem(item: string): Promise<{ item: string; ok: boolean }> {
64
+ "use step";
65
+ const res = await fetch(`https://api.example.com/process`, {
66
+ method: "POST",
67
+ body: JSON.stringify({ item }),
68
+ });
69
+ if (!res.ok) throw new Error(`Failed to process ${item}`);
70
+ return { item, ok: true };
71
+ }
72
+ ```
73
+
74
+ ## Variations
75
+
76
+ ### Scatter-gather
77
+
78
+ When you need results from multiple independent sources before continuing, fan out in parallel and collect all results:
79
+
80
+ ```typescript
81
+ export async function scatterGather(query: string) {
82
+ "use workflow";
83
+
84
+ const [web, database, cache] = await Promise.allSettled([ // [!code highlight]
85
+ searchWeb(query),
86
+ searchDatabase(query),
87
+ searchCache(query),
88
+ ]);
89
+
90
+ return {
91
+ web: web.status === "fulfilled" ? web.value : null,
92
+ database: database.status === "fulfilled" ? database.value : null,
93
+ cache: cache.status === "fulfilled" ? cache.value : null,
94
+ };
95
+ }
96
+
97
+ async function searchWeb(query: string): Promise<string[]> {
98
+ "use step";
99
+ // Full Node.js access -- call external APIs
100
+ const res = await fetch(`https://search.example.com?q=${query}`);
101
+ return res.json();
102
+ }
103
+
104
+ async function searchDatabase(query: string): Promise<string[]> {
105
+ "use step";
106
+ // Query your database
107
+ return [`db-result-for-${query}`];
108
+ }
109
+
110
+ async function searchCache(query: string): Promise<string[]> {
111
+ "use step";
112
+ return [`cached-result-for-${query}`];
113
+ }
114
+ ```
115
+
116
+ ## In-step concurrency control
117
+
118
+ When you need to process many items against a rate-limited API but want the entire operation to be a single atomic step, batch the work inside the step itself. This keeps the event log clean (one step instead of hundreds) while still controlling concurrency.
119
+
120
+ ```typescript
121
+ async function processConcurrently<T>(
122
+ items: string[],
123
+ processor: (item: string) => Promise<T>,
124
+ maxConcurrent: number = 5,
125
+ ): Promise<T[]> {
126
+ "use step";
127
+ const results: T[] = [];
128
+
129
+ for (let i = 0; i < items.length; i += maxConcurrent) {
130
+ const batch = items.slice(i, i + maxConcurrent);
131
+ const batchResults = await Promise.all(batch.map(processor)); // [!code highlight]
132
+ results.push(...batchResults);
133
+ }
134
+
135
+ return results;
136
+ }
137
+ ```
138
+
139
+ Usage in a workflow:
140
+
141
+ ```typescript
142
+ declare function processConcurrently<T>(items: string[], processor: (item: string) => Promise<T>, maxConcurrent?: number): Promise<T[]>; // @setup
143
+
144
+ export async function moderateImages(imageUrls: string[]) {
145
+ "use workflow";
146
+
147
+ const results = await processConcurrently(
148
+ imageUrls,
149
+ async (url) => {
150
+ const res = await fetch("https://api.example.com/moderate", {
151
+ method: "POST",
152
+ body: JSON.stringify({ url }),
153
+ });
154
+ return res.json();
155
+ },
156
+ 3, // max 3 concurrent API calls
157
+ );
158
+
159
+ return { total: results.length, results };
160
+ }
161
+ ```
162
+
163
+ **When to use in-step batching vs workflow-level batching:**
164
+ - **Workflow-level** (the pattern above): Each item is its own step with independent retries and failure isolation. Use when items are independent and individual failures should be retried.
165
+ - **In-step**: All items are processed in one step. Use when the items are tightly coupled (e.g., moderating all thumbnails for a single video) or when you want to minimize step overhead for large item counts.
166
+
167
+ ## Tips
168
+
169
+ - **Use `Promise.allSettled` over `Promise.all`** when you want to continue even if some items fail. `Promise.all` rejects on the first failure; `allSettled` waits for everything and tells you what failed.
170
+ - **Tune batch size to your downstream API limits.** If the API allows 10 concurrent requests, use `batchSize: 10`.
171
+ - **Add pacing with `sleep()`** between batches to respect rate limits. The sleep is durable -- it survives cold starts.
172
+ - **Each `processItem` call is an independent step.** If one fails, it retries up to 3 times without affecting other items in the batch.
173
+
174
+ ## Key APIs
175
+
176
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
177
+ - [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions that run with full Node.js access
178
+ - [`sleep()`](/docs/api-reference/workflow/sleep) -- pacing delay between batches
179
+ - [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) -- runs items in parallel, isolating failures
@@ -0,0 +1,372 @@
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