workflow 5.0.0-beta.3 → 5.0.0-beta.30

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 (176) 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 +1 -1
  4. package/dist/api.d.ts +5 -1
  5. package/dist/api.d.ts.map +1 -1
  6. package/dist/api.js +12 -2
  7. package/dist/index.d.ts +1 -0
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +6 -1
  10. package/dist/internal/builtins.d.ts +17 -0
  11. package/dist/internal/builtins.d.ts.map +1 -1
  12. package/dist/internal/builtins.js +65 -1
  13. package/dist/observability.d.ts +1 -1
  14. package/dist/observability.js +2 -2
  15. package/dist/runtime.d.ts +2 -1
  16. package/dist/runtime.d.ts.map +1 -1
  17. package/dist/runtime.js +5 -2
  18. package/docs/ai/chat-session-modeling.mdx +7 -3
  19. package/docs/ai/defining-tools.mdx +2 -2
  20. package/docs/ai/index.mdx +28 -23
  21. package/docs/ai/message-queueing.mdx +10 -10
  22. package/docs/ai/resumable-streams.mdx +9 -1
  23. package/docs/api-reference/index.mdx +24 -0
  24. package/docs/api-reference/meta.json +8 -0
  25. package/docs/api-reference/vitest/index.mdx +28 -7
  26. package/docs/api-reference/workflow/create-hook.mdx +38 -0
  27. package/docs/api-reference/workflow/create-webhook.mdx +1 -0
  28. package/docs/api-reference/workflow/experimental-set-attributes.mdx +65 -0
  29. package/docs/api-reference/workflow/fetch.mdx +5 -0
  30. package/docs/api-reference/workflow/index.mdx +3 -0
  31. package/docs/api-reference/workflow-ai/durable-agent.mdx +7 -45
  32. package/docs/api-reference/workflow-ai/index.mdx +2 -2
  33. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +44 -1
  34. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +7 -0
  35. package/docs/api-reference/workflow-api/get-run.mdx +25 -0
  36. package/docs/api-reference/workflow-api/index.mdx +6 -8
  37. package/docs/api-reference/workflow-api/resume-hook.mdx +57 -0
  38. package/docs/api-reference/workflow-api/start.mdx +13 -5
  39. package/docs/api-reference/workflow-astro/index.mdx +18 -0
  40. package/docs/api-reference/workflow-astro/meta.json +4 -0
  41. package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
  42. package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
  43. package/docs/api-reference/workflow-errors/index.mdx +85 -0
  44. package/docs/api-reference/workflow-errors/meta.json +5 -0
  45. package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
  46. package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
  47. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
  48. package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
  49. package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
  50. package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
  51. package/docs/api-reference/workflow-nest/index.mdx +31 -0
  52. package/docs/api-reference/workflow-nest/meta.json +9 -0
  53. package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
  54. package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
  55. package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
  56. package/docs/api-reference/workflow-next/with-workflow.mdx +56 -2
  57. package/docs/api-reference/workflow-nitro/index.mdx +60 -0
  58. package/docs/api-reference/workflow-nuxt/index.mdx +48 -0
  59. package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
  60. package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
  61. package/docs/api-reference/workflow-observability/index.mdx +64 -0
  62. package/docs/api-reference/workflow-observability/meta.json +11 -0
  63. package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
  64. package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
  65. package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
  66. package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
  67. package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
  68. package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
  69. package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +7 -10
  70. package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
  71. package/docs/api-reference/workflow-runtime/index.mdx +43 -0
  72. package/docs/api-reference/workflow-runtime/meta.json +12 -0
  73. package/docs/api-reference/workflow-runtime/set-world.mdx +49 -0
  74. package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +42 -0
  75. package/docs/api-reference/{workflow-api → workflow-runtime}/world/index.mdx +5 -8
  76. package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
  77. package/docs/api-reference/{workflow-api → workflow-runtime}/world/queue.mdx +4 -2
  78. package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +11 -4
  79. package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +2 -2
  80. package/docs/api-reference/workflow-serde/index.mdx +0 -1
  81. package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +1 -2
  82. package/docs/api-reference/workflow-serde/workflow-serialize.mdx +1 -2
  83. package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
  84. package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
  85. package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
  86. package/docs/api-reference/workflow-vite/index.mdx +18 -0
  87. package/docs/api-reference/workflow-vite/meta.json +4 -0
  88. package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
  89. package/docs/changelog/attributes-mvp.mdx +380 -0
  90. package/docs/changelog/eager-processing.mdx +269 -0
  91. package/docs/changelog/index.mdx +2 -1
  92. package/docs/changelog/lazy-event-creation.md +127 -0
  93. package/docs/changelog/meta.json +8 -1
  94. package/docs/changelog/resilient-start.mdx +31 -283
  95. package/docs/changelog/step-message-ownership.mdx +360 -0
  96. package/docs/changelog/turbo-mode.md +87 -0
  97. package/docs/configuration/build-and-diagnostics.mdx +51 -0
  98. package/docs/configuration/cli-and-web-ui.mdx +154 -0
  99. package/docs/configuration/framework-options.mdx +165 -0
  100. package/docs/configuration/index.mdx +32 -0
  101. package/docs/configuration/meta.json +12 -0
  102. package/docs/configuration/runtime-tuning.mdx +156 -0
  103. package/docs/configuration/worlds.mdx +228 -0
  104. package/docs/cookbook/advanced/child-workflows.mdx +199 -256
  105. package/docs/cookbook/advanced/meta.json +1 -1
  106. package/docs/cookbook/advanced/publishing-libraries.mdx +13 -12
  107. package/docs/cookbook/advanced/serializable-steps.mdx +8 -4
  108. package/docs/cookbook/advanced/upgrading-workflows.mdx +195 -0
  109. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +31 -76
  110. package/docs/cookbook/agent-patterns/durable-agent.mdx +11 -143
  111. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +7 -3
  112. package/docs/cookbook/common-patterns/idempotency.mdx +36 -52
  113. package/docs/cookbook/common-patterns/rate-limiting.mdx +1 -1
  114. package/docs/cookbook/common-patterns/saga.mdx +2 -2
  115. package/docs/cookbook/common-patterns/scheduling.mdx +8 -0
  116. package/docs/cookbook/common-patterns/timeouts.mdx +2 -1
  117. package/docs/cookbook/common-patterns/workflow-composition.mdx +15 -16
  118. package/docs/cookbook/index.mdx +3 -3
  119. package/docs/cookbook/integrations/ai-sdk.mdx +60 -28
  120. package/docs/cookbook/integrations/chat-sdk.mdx +13 -0
  121. package/docs/cookbook/integrations/sandbox.mdx +13 -0
  122. package/docs/deploying/building-a-world.mdx +1 -1
  123. package/docs/deploying/index.mdx +1 -0
  124. package/docs/deploying/world/local-world.mdx +23 -6
  125. package/docs/deploying/world/postgres-world.mdx +79 -13
  126. package/docs/deploying/world/vercel-world.mdx +47 -12
  127. package/docs/errors/abort-signal-timeout-in-workflow.mdx +80 -0
  128. package/docs/errors/corrupted-event-log.mdx +5 -5
  129. package/docs/errors/hook-conflict.mdx +56 -4
  130. package/docs/errors/index.mdx +1 -35
  131. package/docs/errors/replay-divergence.mdx +27 -0
  132. package/docs/errors/runtime-decryption-failed.mdx +77 -0
  133. package/docs/errors/step-executed-multiple-times.mdx +23 -0
  134. package/docs/errors/step-not-registered.mdx +1 -1
  135. package/docs/foundations/cancellation.mdx +459 -0
  136. package/docs/foundations/errors-and-retries.mdx +7 -3
  137. package/docs/foundations/hooks.mdx +29 -0
  138. package/docs/foundations/idempotency.mdx +236 -11
  139. package/docs/foundations/index.mdx +1 -23
  140. package/docs/foundations/meta.json +3 -1
  141. package/docs/foundations/serialization.mdx +77 -41
  142. package/docs/foundations/starting-workflows.mdx +5 -1
  143. package/docs/foundations/streaming.mdx +14 -23
  144. package/docs/foundations/versioning.mdx +263 -0
  145. package/docs/getting-started/astro.mdx +6 -0
  146. package/docs/getting-started/index.mdx +6 -7
  147. package/docs/getting-started/meta.json +1 -0
  148. package/docs/getting-started/nestjs.mdx +9 -0
  149. package/docs/getting-started/next.mdx +5 -3
  150. package/docs/getting-started/nitro.mdx +22 -0
  151. package/docs/getting-started/sveltekit.mdx +6 -0
  152. package/docs/getting-started/tanstack-start.mdx +241 -0
  153. package/docs/how-it-works/cancellation.mdx +287 -0
  154. package/docs/how-it-works/code-transform.mdx +2 -2
  155. package/docs/how-it-works/encryption.mdx +2 -2
  156. package/docs/how-it-works/event-sourcing.mdx +2 -2
  157. package/docs/how-it-works/meta.json +2 -1
  158. package/docs/internal/index.mdx +21 -0
  159. package/docs/internal/meta.json +10 -0
  160. package/docs/internal/nitro-native-build.mdx +38 -0
  161. package/docs/internal/nitro-web-ui.mdx +24 -0
  162. package/docs/internal/serializable-abort-controller.mdx +148 -0
  163. package/docs/meta.json +1 -1
  164. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +7 -12
  165. package/docs/migration-guides/migrating-from-inngest.mdx +7 -17
  166. package/docs/migration-guides/migrating-from-temporal.mdx +6 -11
  167. package/docs/migration-guides/migrating-from-trigger-dev.mdx +8 -17
  168. package/docs/observability/attributes.mdx +87 -0
  169. package/docs/observability/index.mdx +18 -1
  170. package/docs/observability/meta.json +1 -1
  171. package/docs/observability/tracing.mdx +119 -0
  172. package/docs/testing/index.mdx +2 -2
  173. package/package.json +14 -13
  174. package/docs/api-reference/workflow-api/world/meta.json +0 -4
  175. package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
  176. package/docs/cookbook/advanced/distributed-abort-controller.mdx +0 -318
@@ -1,8 +1,10 @@
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
 
8
10
  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.
@@ -18,20 +20,68 @@ Child workflows are the right choice when:
18
20
 
19
21
  For simpler cases where steps share a single event log, use [direct await composition](/cookbook/common-patterns/workflow-composition#direct-await-flattening) instead.
20
22
 
21
- ## Basic pattern: spawn and poll
23
+ ## Basic pattern: spawn and wait via hook
22
24
 
23
- The core pattern has three parts:
25
+ The recommended pattern has four parts:
24
26
 
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
27
+ 1. A **completion hook** the parent creates and awaits zero compute while waiting
28
+ 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`
29
+ 3. A **`start()` call** that spawns the wrapped child with the hook token (directly from the workflow in v5)
30
+ 4. A **`startAndWait()` helper** that ties the hook, spawn, and typed result together
28
31
 
29
32
  ```typescript
30
- import { sleep } from "workflow";
31
- import { getRun, start } from "workflow/api";
33
+ import { defineHook, getWorkflowMetadata } from "workflow";
34
+ import { start } from "workflow/api";
35
+ import { z } from "zod";
36
+
37
+ declare function fetchDocument(documentId: string): Promise<string>; // @setup
38
+ declare function analyzeContent(content: string): Promise<string>; // @setup
39
+ declare function generateSummary(analysis: string): Promise<string>; // @setup
40
+
41
+ const childCompletionHook = defineHook({
42
+ schema: z.discriminatedUnion("status", [
43
+ z.object({ status: z.literal("completed"), value: z.unknown() }),
44
+ z.object({ status: z.literal("failed"), error: z.string() }),
45
+ ]),
46
+ });
32
47
 
33
- declare function pollUntilComplete(runIds: string[]): Promise<void>; // @setup
34
- declare function collectResults(runIds: string[]): Promise<Array<{ documentId: string; summary: string }>>; // @setup
48
+ function completionToken(parentRunId: string, key: string) {
49
+ return `child-completion:${parentRunId}:${key}`;
50
+ }
51
+
52
+ async function resumeParentCompletion(
53
+ token: string,
54
+ result:
55
+ | { status: "completed"; value: unknown }
56
+ | { status: "failed"; error: string }
57
+ ) {
58
+ "use step";
59
+ await childCompletionHook.resume(token, result);
60
+ }
61
+
62
+ async function withChildCompletionHook<TResult>(
63
+ runChild: () => Promise<TResult>,
64
+ completionTokenArg: string
65
+ ) {
66
+ let result:
67
+ | { status: "completed"; value: TResult }
68
+ | { status: "failed"; error: string }
69
+ | undefined;
70
+
71
+ try {
72
+ const value = await runChild();
73
+ result = { status: "completed", value };
74
+ } catch (error) {
75
+ result = {
76
+ status: "failed",
77
+ error: error instanceof Error ? error.message : String(error),
78
+ };
79
+ } finally {
80
+ if (result) {
81
+ await resumeParentCompletion(completionTokenArg, result);
82
+ }
83
+ }
84
+ }
35
85
 
36
86
  // Child workflow -- processes a single document
37
87
  export async function processDocument(documentId: string) {
@@ -44,163 +94,113 @@ export async function processDocument(documentId: string) {
44
94
  return { documentId, summary };
45
95
  }
46
96
 
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
- }
97
+ // Spawnable wrapper -- explicit export so `start()` can register it
98
+ export async function processDocumentWithCompletion(
99
+ documentId: string,
100
+ completionTokenArg: string
101
+ ) {
102
+ "use workflow";
52
103
 
53
- async function analyzeContent(content: string): Promise<string> {
54
- "use step";
55
- // Call analysis API
56
- return `analysis of ${content.length} chars`;
104
+ await withChildCompletionHook(
105
+ () => processDocument(documentId),
106
+ completionTokenArg
107
+ );
57
108
  }
58
109
 
59
- async function generateSummary(analysis: string): Promise<string> {
60
- "use step";
61
- // Generate summary from analysis
62
- return `Summary: ${analysis}`;
110
+ async function startAndWait<TResult>(
111
+ key: string,
112
+ startChild: (completionTokenArg: string) => Promise<void>
113
+ ): Promise<TResult> {
114
+ const { workflowRunId } = getWorkflowMetadata();
115
+ const token = completionToken(workflowRunId, key);
116
+ const hook = childCompletionHook.create({ token }); // [!code highlight]
117
+
118
+ await startChild(token);
119
+
120
+ const completion = await hook; // [!code highlight]
121
+ if (completion.status === "failed") {
122
+ throw new Error(completion.error);
123
+ }
124
+ return completion.value as TResult;
63
125
  }
64
126
 
65
127
  // Parent workflow -- orchestrates document processing
66
128
  export async function processDocumentBatch(documentIds: string[]) {
67
129
  "use workflow";
68
130
 
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);
131
+ const results = await Promise.all(
132
+ documentIds.map((documentId) =>
133
+ startAndWait<{ documentId: string; summary: string }>(documentId, (token) =>
134
+ start(processDocumentWithCompletion, [documentId, token]).then(() => undefined) // [!code highlight]
135
+ )
136
+ )
137
+ );
77
138
 
78
139
  return { processed: results.length, results };
79
140
  }
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
141
  ```
94
142
 
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
- }
143
+ ### Why hooks instead of polling?
145
144
 
146
- return { running, completed, failed };
147
- }
145
+ Polling with `getRun().status` in a `sleep()` loop works, but hook resume is preferable because:
148
146
 
149
- async function collectResults(
150
- runIds: string[]
151
- ): Promise<Array<{ documentId: string; summary: string }>> {
152
- "use step";
147
+ - **Zero compute while waiting** — the parent suspends on the hook instead of waking every poll interval
148
+ - **Immediate wake-up** — the parent resumes as soon as the child finishes, not on the next poll tick
149
+ - **Typed payloads** — the child sends `{ status, value | error }` directly; no separate `returnValue` fetch step
150
+ - **No worker-pool pressure** — `Run#returnValue` polling inside steps can hold worker slots while waiting for children (see [Eager Processing](/docs/changelog/eager-processing))
153
151
 
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
- ```
152
+ 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
153
 
164
154
  ## Fan-out pattern: chunked spawning
165
155
 
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.
156
+ 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
157
 
168
158
  ```typescript
169
159
  import { start } from "workflow/api";
170
160
 
171
- declare function pollUntilComplete(runIds: string[]): Promise<void>; // @setup
161
+ declare function startAndWait<TResult>(
162
+ key: string,
163
+ startChild: (completionTokenArg: string) => Promise<void>
164
+ ): Promise<TResult>; // @setup
172
165
 
173
166
  const CHUNK_SIZE = 10;
174
167
 
175
- export async function largeReportBatch(reportConfigs: Array<{ id: string; query: string }>) {
168
+ export async function largeReportBatch(
169
+ reportConfigs: Array<{ id: string; query: string }>
170
+ ) {
176
171
  "use workflow";
177
172
 
178
- // Spawn children in chunks
179
- const allRunIds: string[] = [];
173
+ const results = [];
180
174
  for (let i = 0; i < reportConfigs.length; i += CHUNK_SIZE) {
181
175
  const chunk = reportConfigs.slice(i, i + CHUNK_SIZE);
182
- const runIds = await spawnReportChunk(chunk); // [!code highlight]
183
- allRunIds.push(...runIds);
176
+ const chunkResults = await Promise.all(
177
+ chunk.map((config) =>
178
+ startAndWait<{ reportId: string; formatted: string }>(config.id, (token) =>
179
+ start(generateReportWithCompletion, [
180
+ config.id,
181
+ config.query,
182
+ token,
183
+ ]).then(() => undefined)
184
+ )
185
+ )
186
+ );
187
+ results.push(...chunkResults);
184
188
  }
185
189
 
186
- // Poll until all complete
187
- await pollUntilComplete(allRunIds);
188
-
189
- const results = await collectReportResults(allRunIds);
190
190
  return { total: results.length, results };
191
191
  }
192
192
 
193
- async function spawnReportChunk(
194
- configs: Array<{ id: string; query: string }>
195
- ): Promise<string[]> {
196
- "use step";
193
+ async function generateReportWithCompletion(
194
+ reportId: string,
195
+ query: string,
196
+ completionTokenArg: string
197
+ ) {
198
+ "use workflow";
197
199
 
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;
200
+ await withChildCompletionHook(
201
+ () => generateReport(reportId, query),
202
+ completionTokenArg
203
+ );
204
204
  }
205
205
 
206
206
  async function generateReport(reportId: string, query: string) {
@@ -213,160 +213,103 @@ async function generateReport(reportId: string, query: string) {
213
213
 
214
214
  declare function queryDatabase(reportId: string, query: string): Promise<string>; // @setup
215
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
216
+ declare function withChildCompletionHook<TResult>(
217
+ runChild: () => Promise<TResult>,
218
+ completionTokenArg: string
219
+ ): Promise<void>; // @setup
220
220
  ```
221
221
 
222
222
  ## Error handling
223
223
 
224
224
  ### Tolerating partial failures
225
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.
226
+ 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
227
 
228
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
- }
229
+ import { start } from "workflow/api";
273
230
 
274
- throw new Error("Timed out waiting for children");
275
- }
231
+ declare function startAndWait<TResult>(
232
+ key: string,
233
+ startChild: (completionTokenArg: string) => Promise<void>
234
+ ): Promise<TResult>; // @setup
235
+ declare function processDocumentWithCompletion(
236
+ documentId: string,
237
+ completionTokenArg: string
238
+ ): Promise<void>; // @setup
276
239
 
277
- async function checkDetailedStatuses(
278
- runIds: string[]
279
- ): Promise<Array<{ runId: string; status: string }>> {
280
- "use step";
240
+ export async function processDocumentBatchTolerant(documentIds: string[]) {
241
+ "use workflow";
281
242
 
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;
243
+ const settled = await Promise.allSettled(
244
+ documentIds.map((documentId) =>
245
+ startAndWait<{ documentId: string; summary: string }>(documentId, (token) =>
246
+ start(processDocumentWithCompletion, [documentId, token]).then(
247
+ () => undefined
248
+ )
249
+ )
250
+ )
251
+ );
252
+
253
+ const results = settled
254
+ .filter(
255
+ (entry): entry is PromiseFulfilledResult<{ documentId: string; summary: string }> =>
256
+ entry.status === "fulfilled"
257
+ )
258
+ .map((entry) => entry.value);
259
+
260
+ const failed = settled.filter((entry) => entry.status === "rejected").length;
261
+
262
+ return { processed: results.length, failed, results };
289
263
  }
290
264
  ```
291
265
 
292
266
  ### Retrying failed children
293
267
 
294
- When a child fails, the parent can spawn a replacement and continue polling. Track restart counts to prevent infinite retry loops.
268
+ When a child fails, spawn a replacement with a fresh hook token. Track restart counts to prevent infinite retry loops.
295
269
 
296
270
  ```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
- }
271
+ declare function startAndWait<TResult>(
272
+ key: string,
273
+ startChild: (completionTokenArg: string) => Promise<void>
274
+ ): Promise<TResult>; // @setup
275
+ declare function spawnProcessDocument(
276
+ documentId: string,
277
+ completionTokenArg: string
278
+ ): Promise<void>; // @setup
279
+
280
+ async function startAndWaitWithRetries(
281
+ documentId: string,
282
+ maxRestarts: number
283
+ ): Promise<{ documentId: string; summary: string }> {
284
+ for (let attempt = 0; attempt <= maxRestarts; attempt++) {
285
+ try {
286
+ return await startAndWait<{ documentId: string; summary: string }>(
287
+ `${documentId}:${attempt}`,
288
+ (token) => spawnProcessDocument(documentId, token)
289
+ );
290
+ } catch (error) {
291
+ if (attempt === maxRestarts) throw error;
345
292
  }
346
-
347
- if (activeRuns.size === 0) return;
348
-
349
- iteration += 1;
350
- await sleep(POLL_INTERVAL);
351
293
  }
352
294
 
353
- throw new Error("Timed out waiting for children");
295
+ throw new Error("unreachable");
354
296
  }
355
297
  ```
356
298
 
357
299
  ## Tips
358
300
 
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.
301
+ - **`defineHook().resume()` must be called from a step.** The wrapped child's `finally` block calls a step that resumes the parent hook.
302
+ - **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()`.
303
+ - **Use stable hook keys** document ID, job ID, or index so parallel children inside one parent run don't collide on tokens.
304
+ - **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.
305
+ - **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.
306
+ - **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
307
 
366
308
  ## Key APIs
367
309
 
368
310
  - [`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
311
+ - [`defineHook()`](/docs/api-reference/workflow/define-hook) -- typed hook for parent/child completion handshakes
312
+ - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) -- resume a waiting parent from a step (called by the child wrapper)
313
+ - [`getWorkflowMetadata()`](/docs/api-reference/workflow/get-workflow-metadata) -- read the parent run ID for deterministic hook tokens
371
314
  - [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
372
315
  - [`"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
  ]
@@ -270,14 +270,14 @@ Declare `workflow` as an **optional** peer so consumers without the runtime aren
270
270
 
271
271
  ### Runtime detection
272
272
 
273
- Wrap a dynamic `import("workflow")` in try/catch. If either the module isn't installed *or* `getWorkflowMetadata()` throws (call site isn't inside a workflow run), fall through to the standalone path.
273
+ Wrap a dynamic `import("workflow")` in try/catch. If either the module isn't installed *or* `getStepMetadata()` throws (call site isn't inside a workflow step), fall through to the standalone path.
274
274
 
275
275
  ```typescript lineNumbers
276
- async function getWorkflowRunId(): Promise<string | null> { // [!code highlight]
276
+ async function getWorkflowStepId(): Promise<string | null> { // [!code highlight]
277
277
  try {
278
278
  const wf = await import("workflow");
279
- const { workflowRunId } = wf.getWorkflowMetadata();
280
- return workflowRunId;
279
+ const { stepId } = wf.getStepMetadata();
280
+ return stepId;
281
281
  } catch {
282
282
  return null;
283
283
  }
@@ -286,13 +286,14 @@ async function getWorkflowRunId(): Promise<string | null> { // [!code highlight]
286
286
 
287
287
  ### A concrete use case: replay-safe idempotency keys
288
288
 
289
- A payments utility that uses the workflow run ID as a Stripe idempotency key when available, and a fresh UUID otherwise:
289
+ A payments utility that uses the current workflow step ID as a Stripe idempotency key when available, and a fresh UUID otherwise:
290
290
 
291
- {/* @skip-typecheck - depends on getWorkflowRunId defined in the previous block */}
292
291
  ```typescript lineNumbers
292
+ declare function getWorkflowStepId(): Promise<string | null>; // @setup (defined in the previous block)
293
+
293
294
  export async function processPayment(amount: number, currency: string) {
294
- const runId = await getWorkflowRunId();
295
- const idempotencyKey = runId ?? crypto.randomUUID(); // [!code highlight]
295
+ const stepId = await getWorkflowStepId();
296
+ const idempotencyKey = stepId ? `payment:${stepId}` : crypto.randomUUID(); // [!code highlight]
296
297
 
297
298
  const res = await fetch("https://api.stripe.com/v1/charges", {
298
299
  method: "POST",
@@ -306,7 +307,7 @@ export async function processPayment(amount: number, currency: string) {
306
307
  }
307
308
  ```
308
309
 
309
- When called from inside a workflow run, the step wrapping this utility gets a stable idempotency key across replays — Stripe dedupes retries for free. When called from a plain Node.js process, it behaves like any other function and a fresh UUID is generated.
310
+ When called from inside a workflow step, the utility gets a stable idempotency key for that step across retries — Stripe dedupes retries for free. When called from a plain Node.js process, it behaves like any other function and a fresh UUID is generated. For more patterns, see [Idempotency](/docs/foundations/idempotency).
310
311
 
311
312
  ### In production
312
313
 
@@ -330,7 +331,7 @@ Before publishing a workflow library:
330
331
 
331
332
  ## Key APIs
332
333
 
333
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
334
- - [`"use step"`](/docs/api-reference/workflow/use-step) — marks functions for durable execution
335
- - [`start`](/docs/api-reference/workflow/start) — starts a workflow run
334
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions) — declares the orchestrator function
335
+ - [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) — marks functions for durable execution
336
+ - [`start`](/docs/api-reference/workflow-api/start) — starts a workflow run
336
337
  - [`getWorkflowMetadata`](/docs/api-reference/workflow/get-workflow-metadata) — runtime detection and run ID access
@@ -6,7 +6,7 @@ summary: Return a callback from a step to defer construction of a non-owned clas
6
6
  related:
7
7
  - /docs/foundations/serialization
8
8
  - /docs/foundations/serialization#custom-class-serialization
9
- - /docs/api-reference/workflow/use-step
9
+ - /docs/foundations/workflows-and-steps#step-functions
10
10
  ---
11
11
 
12
12
  <Callout>
@@ -63,6 +63,10 @@ The `DurableAgent` receives a function (`() => Promise<LanguageModel>`) instead
63
63
 
64
64
  ## How `@workflow/ai` Uses This
65
65
 
66
+ <Callout type="warn">
67
+ `@workflow/ai`'s pre-wrapped providers and `DurableAgent` are deprecated. AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) resolves models from AI Gateway model strings (e.g. `"openai/gpt-4o"`), which usually removes the need for a model factory — see the [migration guide](https://ai-sdk.dev/v7/docs/agents/workflow-agent#migrating-from-durableagent). The serialization pattern on this page still applies to any non-serializable dependency you own (for example, cloud SDK clients).
68
+ </Callout>
69
+
66
70
  The `@workflow/ai` package ships pre-wrapped providers for all major AI SDK backends. Each one follows the same pattern:
67
71
 
68
72
  ```typescript lineNumbers
@@ -141,7 +145,7 @@ async function uploadFile(
141
145
 
142
146
  ## Key APIs
143
147
 
144
- - [`"use step"`](/docs/api-reference/workflow/use-step) — marks a function for extraction and serialization
145
- - [`"use workflow"`](/docs/api-reference/workflow/use-workflow) — declares the orchestrator function
146
- - [`DurableAgent`](/docs/api-reference/workflow-ai/durable-agent) — accepts a model factory for durable AI agent streaming
148
+ - [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) — marks a function for extraction and serialization
149
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions) — declares the orchestrator function
150
+ - [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) — AI SDK's durable agent (resolves models via AI Gateway strings; replaces `DurableAgent`)
147
151
  - [Custom class serialization](/docs/foundations/serialization#custom-class-serialization) — the companion pattern for classes you own (`WORKFLOW_SERIALIZE` / `WORKFLOW_DESERIALIZE`)