workflow 5.0.0-beta.4 → 5.0.0-beta.41

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 (227) hide show
  1. package/README.md +63 -20
  2. package/dist/api-workflow.d.ts +1 -1
  3. package/dist/api-workflow.d.ts.map +1 -1
  4. package/dist/api-workflow.js +1 -1
  5. package/dist/api.d.ts +5 -1
  6. package/dist/api.d.ts.map +1 -1
  7. package/dist/api.js +12 -2
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +6 -1
  11. package/dist/internal/builtins.d.ts +17 -0
  12. package/dist/internal/builtins.d.ts.map +1 -1
  13. package/dist/internal/builtins.js +65 -1
  14. package/dist/internal/errors.d.ts +1 -1
  15. package/dist/internal/errors.d.ts.map +1 -1
  16. package/dist/internal/errors.js +2 -2
  17. package/dist/nest-builder.d.ts +2 -0
  18. package/dist/nest-builder.d.ts.map +1 -0
  19. package/dist/nest-builder.js +2 -0
  20. package/dist/nest-vercel-builder.d.ts +2 -0
  21. package/dist/nest-vercel-builder.d.ts.map +1 -0
  22. package/dist/nest-vercel-builder.js +2 -0
  23. package/dist/observability.d.ts +1 -1
  24. package/dist/observability.js +2 -2
  25. package/dist/runtime.d.ts +2 -1
  26. package/dist/runtime.d.ts.map +1 -1
  27. package/dist/runtime.js +5 -2
  28. package/docs/ai/chat-session-modeling.mdx +7 -3
  29. package/docs/ai/defining-tools.mdx +2 -2
  30. package/docs/ai/index.mdx +30 -25
  31. package/docs/ai/message-queueing.mdx +10 -10
  32. package/docs/ai/resumable-streams.mdx +9 -1
  33. package/docs/api-reference/index.mdx +24 -0
  34. package/docs/api-reference/meta.json +8 -0
  35. package/docs/api-reference/vitest/index.mdx +28 -7
  36. package/docs/api-reference/workflow/create-hook.mdx +79 -0
  37. package/docs/api-reference/workflow/create-webhook.mdx +1 -0
  38. package/docs/api-reference/workflow/define-hook.mdx +26 -24
  39. package/docs/api-reference/workflow/fatal-error.mdx +29 -7
  40. package/docs/api-reference/workflow/fetch.mdx +8 -4
  41. package/docs/api-reference/workflow/index.mdx +3 -0
  42. package/docs/api-reference/workflow/set-attributes.mdx +61 -0
  43. package/docs/api-reference/workflow/sleep.mdx +1 -1
  44. package/docs/api-reference/workflow-ai/durable-agent.mdx +7 -45
  45. package/docs/api-reference/workflow-ai/index.mdx +2 -2
  46. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +44 -1
  47. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +9 -0
  48. package/docs/api-reference/workflow-api/get-run.mdx +25 -0
  49. package/docs/api-reference/workflow-api/index.mdx +6 -8
  50. package/docs/api-reference/workflow-api/resume-hook.mdx +60 -1
  51. package/docs/api-reference/workflow-api/resume-webhook.mdx +6 -4
  52. package/docs/api-reference/workflow-api/start.mdx +39 -6
  53. package/docs/api-reference/workflow-astro/index.mdx +18 -0
  54. package/docs/api-reference/workflow-astro/meta.json +4 -0
  55. package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
  56. package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
  57. package/docs/api-reference/workflow-errors/index.mdx +88 -0
  58. package/docs/api-reference/workflow-errors/meta.json +6 -0
  59. package/docs/api-reference/workflow-errors/precondition-failed-error.mdx +68 -0
  60. package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
  61. package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
  62. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
  63. package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
  64. package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
  65. package/docs/api-reference/workflow-globals.mdx +4 -1
  66. package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
  67. package/docs/api-reference/workflow-nest/index.mdx +31 -0
  68. package/docs/api-reference/workflow-nest/meta.json +9 -0
  69. package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
  70. package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
  71. package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
  72. package/docs/api-reference/workflow-next/with-workflow.mdx +56 -2
  73. package/docs/api-reference/workflow-nitro/index.mdx +60 -0
  74. package/docs/api-reference/workflow-nuxt/index.mdx +48 -0
  75. package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
  76. package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
  77. package/docs/api-reference/workflow-observability/index.mdx +64 -0
  78. package/docs/api-reference/workflow-observability/meta.json +11 -0
  79. package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
  80. package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
  81. package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
  82. package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
  83. package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
  84. package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
  85. package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +7 -10
  86. package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
  87. package/docs/api-reference/workflow-runtime/index.mdx +43 -0
  88. package/docs/api-reference/workflow-runtime/meta.json +12 -0
  89. package/docs/api-reference/workflow-runtime/set-world.mdx +49 -0
  90. package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +42 -0
  91. package/docs/api-reference/workflow-runtime/world/analytics.mdx +138 -0
  92. package/docs/api-reference/{workflow-api → workflow-runtime}/world/index.mdx +10 -10
  93. package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
  94. package/docs/api-reference/{workflow-api → workflow-runtime}/world/queue.mdx +8 -6
  95. package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +24 -7
  96. package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +2 -2
  97. package/docs/api-reference/workflow-serde/index.mdx +0 -1
  98. package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +1 -2
  99. package/docs/api-reference/workflow-serde/workflow-serialize.mdx +2 -2
  100. package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
  101. package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
  102. package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
  103. package/docs/api-reference/workflow-vite/index.mdx +18 -0
  104. package/docs/api-reference/workflow-vite/meta.json +4 -0
  105. package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
  106. package/docs/changelog/attributes-mvp.mdx +380 -0
  107. package/docs/changelog/eager-processing.mdx +269 -0
  108. package/docs/changelog/index.mdx +3 -1
  109. package/docs/changelog/lazy-event-creation.md +127 -0
  110. package/docs/changelog/meta.json +9 -1
  111. package/docs/changelog/resilient-resume.mdx +22 -0
  112. package/docs/changelog/resilient-start.mdx +31 -283
  113. package/docs/changelog/step-message-ownership.mdx +360 -0
  114. package/docs/changelog/turbo-mode.md +87 -0
  115. package/docs/comparisons/index.mdx +66 -0
  116. package/docs/comparisons/meta.json +11 -0
  117. package/docs/comparisons/workflow-sdk-vs-aws-agentcore.mdx +55 -0
  118. package/docs/comparisons/workflow-sdk-vs-aws-step-functions.mdx +111 -0
  119. package/docs/comparisons/workflow-sdk-vs-cloudflare-workflows.mdx +71 -0
  120. package/docs/comparisons/workflow-sdk-vs-inngest.mdx +102 -0
  121. package/docs/comparisons/workflow-sdk-vs-temporal.mdx +123 -0
  122. package/docs/comparisons/workflow-sdk-vs-trigger-dev.mdx +103 -0
  123. package/docs/configuration/build-and-diagnostics.mdx +70 -0
  124. package/docs/configuration/cli-and-web-ui.mdx +182 -0
  125. package/docs/configuration/framework-options.mdx +165 -0
  126. package/docs/configuration/index.mdx +32 -0
  127. package/docs/configuration/meta.json +12 -0
  128. package/docs/configuration/runtime-tuning.mdx +281 -0
  129. package/docs/configuration/worlds.mdx +283 -0
  130. package/docs/cookbook/advanced/child-workflows.mdx +203 -256
  131. package/docs/cookbook/advanced/meta.json +1 -1
  132. package/docs/cookbook/advanced/publishing-libraries.mdx +33 -24
  133. package/docs/cookbook/advanced/serializable-steps.mdx +12 -4
  134. package/docs/cookbook/advanced/upgrading-workflows.mdx +199 -0
  135. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +34 -75
  136. package/docs/cookbook/agent-patterns/durable-agent.mdx +14 -142
  137. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +11 -3
  138. package/docs/cookbook/common-patterns/batching.mdx +4 -0
  139. package/docs/cookbook/common-patterns/idempotency.mdx +40 -52
  140. package/docs/cookbook/common-patterns/rate-limiting.mdx +5 -1
  141. package/docs/cookbook/common-patterns/saga.mdx +6 -2
  142. package/docs/cookbook/common-patterns/scheduling.mdx +12 -0
  143. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +4 -0
  144. package/docs/cookbook/common-patterns/timeouts.mdx +6 -1
  145. package/docs/cookbook/common-patterns/webhooks.mdx +4 -0
  146. package/docs/cookbook/common-patterns/workflow-composition.mdx +19 -16
  147. package/docs/cookbook/index.mdx +3 -3
  148. package/docs/cookbook/integrations/ai-sdk.mdx +64 -28
  149. package/docs/cookbook/integrations/chat-sdk.mdx +17 -0
  150. package/docs/cookbook/integrations/sandbox.mdx +17 -0
  151. package/docs/{deploying/index.mdx → deploying.mdx} +7 -7
  152. package/docs/errors/abort-signal-timeout-in-workflow.mdx +84 -0
  153. package/docs/errors/corrupted-event-log.mdx +11 -8
  154. package/docs/errors/deployment-mismatch.mdx +71 -0
  155. package/docs/errors/fetch-in-workflow.mdx +4 -0
  156. package/docs/errors/hook-conflict.mdx +60 -4
  157. package/docs/errors/index.mdx +1 -35
  158. package/docs/errors/node-js-module-in-workflow.mdx +4 -0
  159. package/docs/errors/replay-divergence.mdx +27 -0
  160. package/docs/errors/runtime-decryption-failed.mdx +77 -0
  161. package/docs/errors/serialization-failed.mdx +4 -0
  162. package/docs/errors/start-invalid-workflow-function.mdx +4 -0
  163. package/docs/errors/step-executed-multiple-times.mdx +23 -0
  164. package/docs/errors/step-not-registered.mdx +1 -1
  165. package/docs/errors/timeout-in-workflow.mdx +4 -0
  166. package/docs/errors/webhook-response-not-sent.mdx +4 -0
  167. package/docs/foundations/cancellation.mdx +459 -0
  168. package/docs/foundations/errors-and-retries.mdx +7 -3
  169. package/docs/foundations/hooks.mdx +29 -0
  170. package/docs/foundations/idempotency.mdx +243 -11
  171. package/docs/foundations/index.mdx +1 -23
  172. package/docs/foundations/meta.json +3 -1
  173. package/docs/foundations/serialization.mdx +77 -41
  174. package/docs/foundations/starting-workflows.mdx +79 -2
  175. package/docs/foundations/streaming.mdx +14 -23
  176. package/docs/foundations/versioning.mdx +263 -0
  177. package/docs/getting-started/astro.mdx +12 -2
  178. package/docs/getting-started/express.mdx +6 -2
  179. package/docs/getting-started/fastify.mdx +6 -2
  180. package/docs/getting-started/hono.mdx +6 -2
  181. package/docs/getting-started/index.mdx +16 -10
  182. package/docs/getting-started/meta.json +4 -1
  183. package/docs/getting-started/nestjs.mdx +77 -3
  184. package/docs/getting-started/next.mdx +12 -6
  185. package/docs/getting-started/nitro.mdx +28 -2
  186. package/docs/getting-started/nuxt.mdx +6 -2
  187. package/docs/getting-started/python.mdx +24 -18
  188. package/docs/getting-started/react-router/index.mdx +33 -0
  189. package/docs/getting-started/react-router/meta.json +5 -0
  190. package/docs/getting-started/react-router/v7.mdx +237 -0
  191. package/docs/getting-started/react-router/v8.mdx +232 -0
  192. package/docs/getting-started/sveltekit.mdx +12 -2
  193. package/docs/getting-started/tanstack-start.mdx +245 -0
  194. package/docs/getting-started/vite.mdx +6 -2
  195. package/docs/how-it-works/cancellation.mdx +287 -0
  196. package/docs/how-it-works/code-transform.mdx +21 -17
  197. package/docs/how-it-works/encryption.mdx +5 -5
  198. package/docs/how-it-works/event-sourcing.mdx +17 -9
  199. package/docs/how-it-works/framework-integrations.mdx +96 -337
  200. package/docs/how-it-works/meta.json +2 -1
  201. package/docs/internal/index.mdx +21 -0
  202. package/docs/internal/meta.json +10 -0
  203. package/docs/internal/nitro-native-build.mdx +38 -0
  204. package/docs/internal/nitro-web-ui.mdx +24 -0
  205. package/docs/internal/serializable-abort-controller.mdx +148 -0
  206. package/docs/meta.json +2 -2
  207. package/docs/observability/attributes.mdx +112 -0
  208. package/docs/observability/index.mdx +20 -1
  209. package/docs/observability/meta.json +1 -1
  210. package/docs/observability/tracing.mdx +124 -0
  211. package/docs/testing/index.mdx +2 -2
  212. package/package.json +17 -13
  213. package/docs/api-reference/workflow-api/world/meta.json +0 -4
  214. package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
  215. package/docs/cookbook/advanced/distributed-abort-controller.mdx +0 -318
  216. package/docs/deploying/building-a-world.mdx +0 -251
  217. package/docs/deploying/meta.json +0 -4
  218. package/docs/deploying/world/local-world.mdx +0 -84
  219. package/docs/deploying/world/meta.json +0 -4
  220. package/docs/deploying/world/postgres-world.mdx +0 -222
  221. package/docs/deploying/world/vercel-world.mdx +0 -179
  222. package/docs/migration-guides/index.mdx +0 -34
  223. package/docs/migration-guides/meta.json +0 -9
  224. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +0 -363
  225. package/docs/migration-guides/migrating-from-inngest.mdx +0 -314
  226. package/docs/migration-guides/migrating-from-temporal.mdx +0 -318
  227. package/docs/migration-guides/migrating-from-trigger-dev.mdx +0 -337
@@ -1,10 +1,16 @@
1
1
  ---
2
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.
3
+ description: Spawn child workflows from a parent and wait for completion via hook resume.
4
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.
5
+ summary: Orchestrate independent child workflows from a parent using start(), defineHook(), and startAndWait() the child resumes the parent's hook when done instead of polling getRun().status.
6
+ related:
7
+ - /docs/api-reference/workflow-api/start
6
8
  ---
7
9
 
10
+ <CopyPrompt
11
+ text="Refactor this workflow to use child workflows. Keep the parent as an exported `&quot;use workflow&quot;` function. Move independent units of durable work into separate exported child workflow functions. From the parent, call `start(childWorkflow, [args])` from `workflow/api` or the documented `startAndWait`/hook pattern where completion must resume the parent. Pass only serializable state to children. For fan-out, start children in parallel with `Promise.all` or bounded batches, collect run IDs, handle partial failures with `Promise.allSettled`, and use `getRun(runId)` when status, cancellation, streams, or return values are needed. Verify child start, completion, failure, and parent resume behavior."
12
+ />
13
+
8
14
  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
15
 
10
16
  ## When to use child workflows
@@ -18,20 +24,68 @@ Child workflows are the right choice when:
18
24
 
19
25
  For simpler cases where steps share a single event log, use [direct await composition](/cookbook/common-patterns/workflow-composition#direct-await-flattening) instead.
20
26
 
21
- ## Basic pattern: spawn and poll
27
+ ## Basic pattern: spawn and wait via hook
22
28
 
23
- The core pattern has three parts:
29
+ The recommended pattern has four parts:
24
30
 
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
31
+ 1. A **completion hook** the parent creates and awaits zero compute while waiting
32
+ 2. A **wrapped child export** that runs the real child in try/catch/finally and resumes the parent's hook from a step in `finally`
33
+ 3. A **`start()` call** that spawns the wrapped child with the hook token (directly from the workflow in v5)
34
+ 4. A **`startAndWait()` helper** that ties the hook, spawn, and typed result together
28
35
 
29
36
  ```typescript
30
- import { sleep } from "workflow";
31
- import { getRun, start } from "workflow/api";
37
+ import { defineHook, getWorkflowMetadata } from "workflow";
38
+ import { start } from "workflow/api";
39
+ import { z } from "zod";
40
+
41
+ declare function fetchDocument(documentId: string): Promise<string>; // @setup
42
+ declare function analyzeContent(content: string): Promise<string>; // @setup
43
+ declare function generateSummary(analysis: string): Promise<string>; // @setup
32
44
 
33
- declare function pollUntilComplete(runIds: string[]): Promise<void>; // @setup
34
- declare function collectResults(runIds: string[]): Promise<Array<{ documentId: string; summary: string }>>; // @setup
45
+ const childCompletionHook = defineHook({
46
+ schema: z.discriminatedUnion("status", [
47
+ z.object({ status: z.literal("completed"), value: z.unknown() }),
48
+ z.object({ status: z.literal("failed"), error: z.string() }),
49
+ ]),
50
+ });
51
+
52
+ function completionToken(parentRunId: string, key: string) {
53
+ return `child-completion:${parentRunId}:${key}`;
54
+ }
55
+
56
+ async function resumeParentCompletion(
57
+ token: string,
58
+ result:
59
+ | { status: "completed"; value: unknown }
60
+ | { status: "failed"; error: string }
61
+ ) {
62
+ "use step";
63
+ await childCompletionHook.resume(token, result);
64
+ }
65
+
66
+ async function withChildCompletionHook<TResult>(
67
+ runChild: () => Promise<TResult>,
68
+ completionTokenArg: string
69
+ ) {
70
+ let result:
71
+ | { status: "completed"; value: TResult }
72
+ | { status: "failed"; error: string }
73
+ | undefined;
74
+
75
+ try {
76
+ const value = await runChild();
77
+ result = { status: "completed", value };
78
+ } catch (error) {
79
+ result = {
80
+ status: "failed",
81
+ error: error instanceof Error ? error.message : String(error),
82
+ };
83
+ } finally {
84
+ if (result) {
85
+ await resumeParentCompletion(completionTokenArg, result);
86
+ }
87
+ }
88
+ }
35
89
 
36
90
  // Child workflow -- processes a single document
37
91
  export async function processDocument(documentId: string) {
@@ -44,163 +98,113 @@ export async function processDocument(documentId: string) {
44
98
  return { documentId, summary };
45
99
  }
46
100
 
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
- }
101
+ // Spawnable wrapper -- explicit export so `start()` can register it
102
+ export async function processDocumentWithCompletion(
103
+ documentId: string,
104
+ completionTokenArg: string
105
+ ) {
106
+ "use workflow";
52
107
 
53
- async function analyzeContent(content: string): Promise<string> {
54
- "use step";
55
- // Call analysis API
56
- return `analysis of ${content.length} chars`;
108
+ await withChildCompletionHook(
109
+ () => processDocument(documentId),
110
+ completionTokenArg
111
+ );
57
112
  }
58
113
 
59
- async function generateSummary(analysis: string): Promise<string> {
60
- "use step";
61
- // Generate summary from analysis
62
- return `Summary: ${analysis}`;
114
+ async function startAndWait<TResult>(
115
+ key: string,
116
+ startChild: (completionTokenArg: string) => Promise<void>
117
+ ): Promise<TResult> {
118
+ const { workflowRunId } = getWorkflowMetadata();
119
+ const token = completionToken(workflowRunId, key);
120
+ const hook = childCompletionHook.create({ token }); // [!code highlight]
121
+
122
+ await startChild(token);
123
+
124
+ const completion = await hook; // [!code highlight]
125
+ if (completion.status === "failed") {
126
+ throw new Error(completion.error);
127
+ }
128
+ return completion.value as TResult;
63
129
  }
64
130
 
65
131
  // Parent workflow -- orchestrates document processing
66
132
  export async function processDocumentBatch(documentIds: string[]) {
67
133
  "use workflow";
68
134
 
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);
135
+ const results = await Promise.all(
136
+ documentIds.map((documentId) =>
137
+ startAndWait<{ documentId: string; summary: string }>(documentId, (token) =>
138
+ start(processDocumentWithCompletion, [documentId, token]).then(() => undefined) // [!code highlight]
139
+ )
140
+ )
141
+ );
77
142
 
78
143
  return { processed: results.length, results };
79
144
  }
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
145
  ```
94
146
 
95
- ### Polling loop
147
+ ### Why hooks instead of polling?
96
148
 
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.
149
+ Polling with `getRun().status` in a `sleep()` loop works, but hook resume is preferable because:
98
150
 
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
- }
151
+ - **Zero compute while waiting** — the parent suspends on the hook instead of waking every poll interval
152
+ - **Immediate wake-up** the parent resumes as soon as the child finishes, not on the next poll tick
153
+ - **Typed payloads** — the child sends `{ status, value | error }` directly; no separate `returnValue` fetch step
154
+ - **No worker-pool pressure** — `Run#returnValue` polling inside steps can hold worker slots while waiting for children (see [Eager Processing](/docs/changelog/eager-processing))
120
155
 
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
- ```
156
+ When a parent calls a child workflow inline with `await` (flattened into the same run), the same wrapper and hook handshake still works — pass the token and `await processDocumentWithCompletion(...)` inside `startAndWait()` instead of calling `start()`.
163
157
 
164
158
  ## Fan-out pattern: chunked spawning
165
159
 
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.
160
+ When spawning hundreds of children, batch the `start()` calls to avoid overwhelming the system. Each child still gets its own completion hook keyed by a stable identifier (document ID, report ID, index).
167
161
 
168
162
  ```typescript
169
163
  import { start } from "workflow/api";
170
164
 
171
- declare function pollUntilComplete(runIds: string[]): Promise<void>; // @setup
165
+ declare function startAndWait<TResult>(
166
+ key: string,
167
+ startChild: (completionTokenArg: string) => Promise<void>
168
+ ): Promise<TResult>; // @setup
172
169
 
173
170
  const CHUNK_SIZE = 10;
174
171
 
175
- export async function largeReportBatch(reportConfigs: Array<{ id: string; query: string }>) {
172
+ export async function largeReportBatch(
173
+ reportConfigs: Array<{ id: string; query: string }>
174
+ ) {
176
175
  "use workflow";
177
176
 
178
- // Spawn children in chunks
179
- const allRunIds: string[] = [];
177
+ const results = [];
180
178
  for (let i = 0; i < reportConfigs.length; i += CHUNK_SIZE) {
181
179
  const chunk = reportConfigs.slice(i, i + CHUNK_SIZE);
182
- const runIds = await spawnReportChunk(chunk); // [!code highlight]
183
- allRunIds.push(...runIds);
180
+ const chunkResults = await Promise.all(
181
+ chunk.map((config) =>
182
+ startAndWait<{ reportId: string; formatted: string }>(config.id, (token) =>
183
+ start(generateReportWithCompletion, [
184
+ config.id,
185
+ config.query,
186
+ token,
187
+ ]).then(() => undefined)
188
+ )
189
+ )
190
+ );
191
+ results.push(...chunkResults);
184
192
  }
185
193
 
186
- // Poll until all complete
187
- await pollUntilComplete(allRunIds);
188
-
189
- const results = await collectReportResults(allRunIds);
190
194
  return { total: results.length, results };
191
195
  }
192
196
 
193
- async function spawnReportChunk(
194
- configs: Array<{ id: string; query: string }>
195
- ): Promise<string[]> {
196
- "use step";
197
+ async function generateReportWithCompletion(
198
+ reportId: string,
199
+ query: string,
200
+ completionTokenArg: string
201
+ ) {
202
+ "use workflow";
197
203
 
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
+ await withChildCompletionHook(
205
+ () => generateReport(reportId, query),
206
+ completionTokenArg
207
+ );
204
208
  }
205
209
 
206
210
  async function generateReport(reportId: string, query: string) {
@@ -213,160 +217,103 @@ async function generateReport(reportId: string, query: string) {
213
217
 
214
218
  declare function queryDatabase(reportId: string, query: string): Promise<string>; // @setup
215
219
  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
+ declare function withChildCompletionHook<TResult>(
221
+ runChild: () => Promise<TResult>,
222
+ completionTokenArg: string
223
+ ): Promise<void>; // @setup
220
224
  ```
221
225
 
222
226
  ## Error handling
223
227
 
224
228
  ### Tolerating partial failures
225
229
 
226
- Not every batch requires 100% success. Use `allowFailures` logic to let the parent continue when some children fail, while still surfacing the failures.
230
+ Use `Promise.allSettled` with `startAndWait()` so one failing child doesn't abort siblings. The hook payload already carries `{ status: "failed", error }` no status polling required.
227
231
 
228
232
  ```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
- }
233
+ import { start } from "workflow/api";
273
234
 
274
- throw new Error("Timed out waiting for children");
275
- }
235
+ declare function startAndWait<TResult>(
236
+ key: string,
237
+ startChild: (completionTokenArg: string) => Promise<void>
238
+ ): Promise<TResult>; // @setup
239
+ declare function processDocumentWithCompletion(
240
+ documentId: string,
241
+ completionTokenArg: string
242
+ ): Promise<void>; // @setup
276
243
 
277
- async function checkDetailedStatuses(
278
- runIds: string[]
279
- ): Promise<Array<{ runId: string; status: string }>> {
280
- "use step";
244
+ export async function processDocumentBatchTolerant(documentIds: string[]) {
245
+ "use workflow";
281
246
 
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;
247
+ const settled = await Promise.allSettled(
248
+ documentIds.map((documentId) =>
249
+ startAndWait<{ documentId: string; summary: string }>(documentId, (token) =>
250
+ start(processDocumentWithCompletion, [documentId, token]).then(
251
+ () => undefined
252
+ )
253
+ )
254
+ )
255
+ );
256
+
257
+ const results = settled
258
+ .filter(
259
+ (entry): entry is PromiseFulfilledResult<{ documentId: string; summary: string }> =>
260
+ entry.status === "fulfilled"
261
+ )
262
+ .map((entry) => entry.value);
263
+
264
+ const failed = settled.filter((entry) => entry.status === "rejected").length;
265
+
266
+ return { processed: results.length, failed, results };
289
267
  }
290
268
  ```
291
269
 
292
270
  ### Retrying failed children
293
271
 
294
- When a child fails, the parent can spawn a replacement and continue polling. Track restart counts to prevent infinite retry loops.
272
+ When a child fails, spawn a replacement with a fresh hook token. Track restart counts to prevent infinite retry loops.
295
273
 
296
274
  ```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
- }
275
+ declare function startAndWait<TResult>(
276
+ key: string,
277
+ startChild: (completionTokenArg: string) => Promise<void>
278
+ ): Promise<TResult>; // @setup
279
+ declare function spawnProcessDocument(
280
+ documentId: string,
281
+ completionTokenArg: string
282
+ ): Promise<void>; // @setup
283
+
284
+ async function startAndWaitWithRetries(
285
+ documentId: string,
286
+ maxRestarts: number
287
+ ): Promise<{ documentId: string; summary: string }> {
288
+ for (let attempt = 0; attempt <= maxRestarts; attempt++) {
289
+ try {
290
+ return await startAndWait<{ documentId: string; summary: string }>(
291
+ `${documentId}:${attempt}`,
292
+ (token) => spawnProcessDocument(documentId, token)
293
+ );
294
+ } catch (error) {
295
+ if (attempt === maxRestarts) throw error;
345
296
  }
346
-
347
- if (activeRuns.size === 0) return;
348
-
349
- iteration += 1;
350
- await sleep(POLL_INTERVAL);
351
297
  }
352
298
 
353
- throw new Error("Timed out waiting for children");
299
+ throw new Error("unreachable");
354
300
  }
355
301
  ```
356
302
 
357
303
  ## Tips
358
304
 
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.
305
+ - **`defineHook().resume()` must be called from a step.** The wrapped child's `finally` block calls a step that resumes the parent hook.
306
+ - **Export wrapped children at module scope.** The SDK registers `"use workflow"` functions statically a runtime higher-order function returned from `withChildCompletionHook()` cannot be passed to `start()`.
307
+ - **Use stable hook keys** document ID, job ID, or index so parallel children inside one parent run don't collide on tokens.
308
+ - **Use chunked spawning for large batches.** Starting 500 children at once can create a large burst of work. Break it into chunks of 10-50.
309
+ - **Each child has its own retry semantics.** Steps inside child workflows retry independently. The parent sees the final `{ status, value | error }` payload from the hook.
310
+ - **Use `deploymentId: "latest"`** if children should run on the most recent deployment. See [Versioning](/docs/foundations/versioning) for the full model and the [`start()` API reference](/docs/api-reference/workflow-api/start#using-deploymentid-latest) for compatibility considerations.
365
311
 
366
312
  ## Key APIs
367
313
 
368
314
  - [`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
315
+ - [`defineHook()`](/docs/api-reference/workflow/define-hook) -- typed hook for parent/child completion handshakes
316
+ - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) -- resume a waiting parent from a step (called by the child wrapper)
317
+ - [`getWorkflowMetadata()`](/docs/api-reference/workflow/get-workflow-metadata) -- read the parent run ID for deterministic hook tokens
371
318
  - [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
372
319
  - [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions with full Node.js access
@@ -2,7 +2,7 @@
2
2
  "title": "Advanced",
3
3
  "pages": [
4
4
  "child-workflows",
5
- "distributed-abort-controller",
5
+ "upgrading-workflows",
6
6
  "serializable-steps",
7
7
  "publishing-libraries"
8
8
  ]