workflow 5.0.0-beta.6 → 5.0.0-beta.8
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.
- package/dist/internal/builtins.d.ts +17 -0
- package/dist/internal/builtins.d.ts.map +1 -1
- package/dist/internal/builtins.js +65 -1
- package/dist/observability.d.ts +1 -1
- package/dist/observability.js +2 -2
- package/docs/api-reference/workflow-next/with-workflow.mdx +2 -2
- package/docs/changelog/attributes-mvp.mdx +365 -0
- package/docs/cookbook/advanced/child-workflows.mdx +196 -244
- package/docs/cookbook/advanced/meta.json +6 -1
- package/docs/cookbook/advanced/upgrading-workflows.mdx +195 -0
- package/docs/cookbook/common-patterns/workflow-composition.mdx +4 -4
- package/docs/cookbook/index.mdx +1 -0
- package/docs/cookbook/integrations/ai-sdk.mdx +44 -25
- package/package.json +13 -13
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Child Workflows
|
|
3
|
-
description: Spawn child workflows from a parent and
|
|
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
|
|
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
6
|
---
|
|
7
7
|
|
|
8
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.
|
|
@@ -18,20 +18,68 @@ Child workflows are the right choice when:
|
|
|
18
18
|
|
|
19
19
|
For simpler cases where steps share a single event log, use [direct await composition](/cookbook/common-patterns/workflow-composition#direct-await-flattening) instead.
|
|
20
20
|
|
|
21
|
-
## Basic pattern: spawn and
|
|
21
|
+
## Basic pattern: spawn and wait via hook
|
|
22
22
|
|
|
23
|
-
The
|
|
23
|
+
The recommended pattern has four parts:
|
|
24
24
|
|
|
25
|
-
1. A
|
|
26
|
-
2. A **
|
|
27
|
-
3. A **
|
|
25
|
+
1. A **completion hook** the parent creates and awaits — zero compute while waiting
|
|
26
|
+
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`
|
|
27
|
+
3. A **`start()` call** that spawns the wrapped child with the hook token (directly from the workflow in v5)
|
|
28
|
+
4. A **`startAndWait()` helper** that ties the hook, spawn, and typed result together
|
|
28
29
|
|
|
29
30
|
```typescript
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
31
|
+
import { defineHook, getWorkflowMetadata } from "workflow";
|
|
32
|
+
import { start } from "workflow/api";
|
|
33
|
+
import { z } from "zod";
|
|
34
|
+
|
|
35
|
+
declare function fetchDocument(documentId: string): Promise<string>; // @setup
|
|
36
|
+
declare function analyzeContent(content: string): Promise<string>; // @setup
|
|
37
|
+
declare function generateSummary(analysis: string): Promise<string>; // @setup
|
|
38
|
+
|
|
39
|
+
const childCompletionHook = defineHook({
|
|
40
|
+
schema: z.discriminatedUnion("status", [
|
|
41
|
+
z.object({ status: z.literal("completed"), value: z.unknown() }),
|
|
42
|
+
z.object({ status: z.literal("failed"), error: z.string() }),
|
|
43
|
+
]),
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
function completionToken(parentRunId: string, key: string) {
|
|
47
|
+
return `child-completion:${parentRunId}:${key}`;
|
|
48
|
+
}
|
|
32
49
|
|
|
33
|
-
|
|
34
|
-
|
|
50
|
+
async function resumeParentCompletion(
|
|
51
|
+
token: string,
|
|
52
|
+
result:
|
|
53
|
+
| { status: "completed"; value: unknown }
|
|
54
|
+
| { status: "failed"; error: string }
|
|
55
|
+
) {
|
|
56
|
+
"use step";
|
|
57
|
+
await childCompletionHook.resume(token, result);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
async function withChildCompletionHook<TResult>(
|
|
61
|
+
runChild: () => Promise<TResult>,
|
|
62
|
+
completionTokenArg: string
|
|
63
|
+
) {
|
|
64
|
+
let result:
|
|
65
|
+
| { status: "completed"; value: TResult }
|
|
66
|
+
| { status: "failed"; error: string }
|
|
67
|
+
| undefined;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const value = await runChild();
|
|
71
|
+
result = { status: "completed", value };
|
|
72
|
+
} catch (error) {
|
|
73
|
+
result = {
|
|
74
|
+
status: "failed",
|
|
75
|
+
error: error instanceof Error ? error.message : String(error),
|
|
76
|
+
};
|
|
77
|
+
} finally {
|
|
78
|
+
if (result) {
|
|
79
|
+
await resumeParentCompletion(completionTokenArg, result);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
35
83
|
|
|
36
84
|
// Child workflow -- processes a single document
|
|
37
85
|
export async function processDocument(documentId: string) {
|
|
@@ -44,152 +92,113 @@ export async function processDocument(documentId: string) {
|
|
|
44
92
|
return { documentId, summary };
|
|
45
93
|
}
|
|
46
94
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
95
|
+
// Spawnable wrapper -- explicit export so `start()` can register it
|
|
96
|
+
export async function processDocumentWithCompletion(
|
|
97
|
+
documentId: string,
|
|
98
|
+
completionTokenArg: string
|
|
99
|
+
) {
|
|
100
|
+
"use workflow";
|
|
52
101
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
102
|
+
await withChildCompletionHook(
|
|
103
|
+
() => processDocument(documentId),
|
|
104
|
+
completionTokenArg
|
|
105
|
+
);
|
|
57
106
|
}
|
|
58
107
|
|
|
59
|
-
async function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
108
|
+
async function startAndWait<TResult>(
|
|
109
|
+
key: string,
|
|
110
|
+
startChild: (completionTokenArg: string) => Promise<void>
|
|
111
|
+
): Promise<TResult> {
|
|
112
|
+
const { workflowRunId } = getWorkflowMetadata();
|
|
113
|
+
const token = completionToken(workflowRunId, key);
|
|
114
|
+
const hook = childCompletionHook.create({ token }); // [!code highlight]
|
|
115
|
+
|
|
116
|
+
await startChild(token);
|
|
117
|
+
|
|
118
|
+
const completion = await hook; // [!code highlight]
|
|
119
|
+
if (completion.status === "failed") {
|
|
120
|
+
throw new Error(completion.error);
|
|
121
|
+
}
|
|
122
|
+
return completion.value as TResult;
|
|
63
123
|
}
|
|
64
124
|
|
|
65
125
|
// Parent workflow -- orchestrates document processing
|
|
66
126
|
export async function processDocumentBatch(documentIds: string[]) {
|
|
67
127
|
"use workflow";
|
|
68
128
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
// Poll until all children complete
|
|
77
|
-
await pollUntilComplete(runIds);
|
|
78
|
-
|
|
79
|
-
// Collect results
|
|
80
|
-
const results = await collectResults(runIds);
|
|
129
|
+
const results = await Promise.all(
|
|
130
|
+
documentIds.map((documentId) =>
|
|
131
|
+
startAndWait<{ documentId: string; summary: string }>(documentId, (token) =>
|
|
132
|
+
start(processDocumentWithCompletion, [documentId, token]).then(() => undefined) // [!code highlight]
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
);
|
|
81
136
|
|
|
82
137
|
return { processed: results.length, results };
|
|
83
138
|
}
|
|
84
139
|
```
|
|
85
140
|
|
|
86
|
-
###
|
|
87
|
-
|
|
88
|
-
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.
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
import { sleep } from "workflow";
|
|
92
|
-
import { getRun } from "workflow/api";
|
|
93
|
-
|
|
94
|
-
const POLL_INTERVAL = "30s";
|
|
95
|
-
const MAX_POLL_ITERATIONS = 120; // 60 minutes at 30s intervals
|
|
96
|
-
|
|
97
|
-
async function pollUntilComplete(runIds: string[]): Promise<void> {
|
|
98
|
-
let iteration = 0;
|
|
99
|
-
|
|
100
|
-
while (iteration < MAX_POLL_ITERATIONS) {
|
|
101
|
-
const status = await checkStatuses(runIds); // [!code highlight]
|
|
102
|
-
|
|
103
|
-
if (status.running === 0) {
|
|
104
|
-
if (status.failed > 0) {
|
|
105
|
-
throw new Error(
|
|
106
|
-
`${status.failed} of ${runIds.length} children failed`
|
|
107
|
-
);
|
|
108
|
-
}
|
|
109
|
-
return; // All completed successfully
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
iteration += 1;
|
|
113
|
-
await sleep(POLL_INTERVAL); // [!code highlight]
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
throw new Error("Timed out waiting for children to complete");
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
async function checkStatuses(
|
|
120
|
-
runIds: string[]
|
|
121
|
-
): Promise<{ running: number; completed: number; failed: number }> {
|
|
122
|
-
"use step"; // [!code highlight]
|
|
141
|
+
### Why hooks instead of polling?
|
|
123
142
|
|
|
124
|
-
|
|
125
|
-
let completed = 0;
|
|
126
|
-
let failed = 0;
|
|
143
|
+
Polling with `getRun().status` in a `sleep()` loop works, but hook resume is preferable because:
|
|
127
144
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
145
|
+
- **Zero compute while waiting** — the parent suspends on the hook instead of waking every poll interval
|
|
146
|
+
- **Immediate wake-up** — the parent resumes as soon as the child finishes, not on the next poll tick
|
|
147
|
+
- **Typed payloads** — the child sends `{ status, value | error }` directly; no separate `returnValue` fetch step
|
|
148
|
+
- **No worker-pool pressure** — `Run#returnValue` polling inside steps can hold worker slots while waiting for children (see [Eager Processing](/changelog/eager-processing))
|
|
131
149
|
|
|
132
|
-
|
|
133
|
-
else if (status === "failed" || status === "cancelled") failed += 1;
|
|
134
|
-
else running += 1; // pending, running
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
return { running, completed, failed };
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
async function collectResults(
|
|
141
|
-
runIds: string[]
|
|
142
|
-
): Promise<Array<{ documentId: string; summary: string }>> {
|
|
143
|
-
"use step";
|
|
144
|
-
|
|
145
|
-
const results = [];
|
|
146
|
-
for (const runId of runIds) {
|
|
147
|
-
const run = getRun(runId);
|
|
148
|
-
const value = await run.returnValue;
|
|
149
|
-
results.push(value as { documentId: string; summary: string });
|
|
150
|
-
}
|
|
151
|
-
return results;
|
|
152
|
-
}
|
|
153
|
-
```
|
|
150
|
+
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()`.
|
|
154
151
|
|
|
155
152
|
## Fan-out pattern: chunked spawning
|
|
156
153
|
|
|
157
|
-
When spawning hundreds of children, batch the `start()` calls to avoid overwhelming the system.
|
|
154
|
+
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).
|
|
158
155
|
|
|
159
156
|
```typescript
|
|
160
157
|
import { start } from "workflow/api";
|
|
161
158
|
|
|
162
|
-
declare function
|
|
159
|
+
declare function startAndWait<TResult>(
|
|
160
|
+
key: string,
|
|
161
|
+
startChild: (completionTokenArg: string) => Promise<void>
|
|
162
|
+
): Promise<TResult>; // @setup
|
|
163
163
|
|
|
164
164
|
const CHUNK_SIZE = 10;
|
|
165
165
|
|
|
166
|
-
export async function largeReportBatch(
|
|
166
|
+
export async function largeReportBatch(
|
|
167
|
+
reportConfigs: Array<{ id: string; query: string }>
|
|
168
|
+
) {
|
|
167
169
|
"use workflow";
|
|
168
170
|
|
|
169
|
-
|
|
170
|
-
const allRunIds: string[] = [];
|
|
171
|
+
const results = [];
|
|
171
172
|
for (let i = 0; i < reportConfigs.length; i += CHUNK_SIZE) {
|
|
172
173
|
const chunk = reportConfigs.slice(i, i + CHUNK_SIZE);
|
|
173
|
-
const
|
|
174
|
-
|
|
174
|
+
const chunkResults = await Promise.all(
|
|
175
|
+
chunk.map((config) =>
|
|
176
|
+
startAndWait<{ reportId: string; formatted: string }>(config.id, (token) =>
|
|
177
|
+
start(generateReportWithCompletion, [
|
|
178
|
+
config.id,
|
|
179
|
+
config.query,
|
|
180
|
+
token,
|
|
181
|
+
]).then(() => undefined)
|
|
182
|
+
)
|
|
183
|
+
)
|
|
184
|
+
);
|
|
185
|
+
results.push(...chunkResults);
|
|
175
186
|
}
|
|
176
187
|
|
|
177
|
-
// Poll until all complete
|
|
178
|
-
await pollUntilComplete(allRunIds);
|
|
179
|
-
|
|
180
|
-
const results = await collectReportResults(allRunIds);
|
|
181
188
|
return { total: results.length, results };
|
|
182
189
|
}
|
|
183
190
|
|
|
184
|
-
async function
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
191
|
+
async function generateReportWithCompletion(
|
|
192
|
+
reportId: string,
|
|
193
|
+
query: string,
|
|
194
|
+
completionTokenArg: string
|
|
195
|
+
) {
|
|
196
|
+
"use workflow";
|
|
197
|
+
|
|
198
|
+
await withChildCompletionHook(
|
|
199
|
+
() => generateReport(reportId, query),
|
|
200
|
+
completionTokenArg
|
|
201
|
+
);
|
|
193
202
|
}
|
|
194
203
|
|
|
195
204
|
async function generateReport(reportId: string, query: string) {
|
|
@@ -202,160 +211,103 @@ async function generateReport(reportId: string, query: string) {
|
|
|
202
211
|
|
|
203
212
|
declare function queryDatabase(reportId: string, query: string): Promise<string>; // @setup
|
|
204
213
|
declare function formatReport(reportId: string, data: string): Promise<string>; // @setup
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
): Promise<
|
|
214
|
+
declare function withChildCompletionHook<TResult>(
|
|
215
|
+
runChild: () => Promise<TResult>,
|
|
216
|
+
completionTokenArg: string
|
|
217
|
+
): Promise<void>; // @setup
|
|
209
218
|
```
|
|
210
219
|
|
|
211
220
|
## Error handling
|
|
212
221
|
|
|
213
222
|
### Tolerating partial failures
|
|
214
223
|
|
|
215
|
-
|
|
224
|
+
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.
|
|
216
225
|
|
|
217
226
|
```typescript
|
|
218
|
-
import {
|
|
219
|
-
import { getRun } from "workflow/api";
|
|
220
|
-
|
|
221
|
-
const POLL_INTERVAL = "30s";
|
|
222
|
-
const MAX_POLL_ITERATIONS = 120;
|
|
223
|
-
|
|
224
|
-
async function pollWithPartialFailures(
|
|
225
|
-
runIds: string[],
|
|
226
|
-
maxFailureRate: number
|
|
227
|
-
): Promise<{ completed: string[]; failed: string[] }> {
|
|
228
|
-
let iteration = 0;
|
|
229
|
-
const completedIds: string[] = [];
|
|
230
|
-
const failedIds: string[] = [];
|
|
231
|
-
|
|
232
|
-
while (iteration < MAX_POLL_ITERATIONS) {
|
|
233
|
-
const status = await checkDetailedStatuses(runIds);
|
|
234
|
-
|
|
235
|
-
completedIds.length = 0;
|
|
236
|
-
failedIds.length = 0;
|
|
237
|
-
|
|
238
|
-
for (const entry of status) {
|
|
239
|
-
if (entry.status === "completed") completedIds.push(entry.runId);
|
|
240
|
-
else if (entry.status === "failed" || entry.status === "cancelled")
|
|
241
|
-
failedIds.push(entry.runId);
|
|
242
|
-
}
|
|
243
|
-
|
|
244
|
-
const active = runIds.length - completedIds.length - failedIds.length;
|
|
245
|
-
|
|
246
|
-
// Check if failure rate exceeds threshold
|
|
247
|
-
const failureRate = failedIds.length / Math.max(1, runIds.length); // [!code highlight]
|
|
248
|
-
if (failureRate > maxFailureRate) { // [!code highlight]
|
|
249
|
-
throw new Error( // [!code highlight]
|
|
250
|
-
`Failure rate ${(failureRate * 100).toFixed(1)}% exceeds ` + // [!code highlight]
|
|
251
|
-
`threshold of ${(maxFailureRate * 100).toFixed(1)}%` // [!code highlight]
|
|
252
|
-
); // [!code highlight]
|
|
253
|
-
} // [!code highlight]
|
|
227
|
+
import { start } from "workflow/api";
|
|
254
228
|
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
229
|
+
declare function startAndWait<TResult>(
|
|
230
|
+
key: string,
|
|
231
|
+
startChild: (completionTokenArg: string) => Promise<void>
|
|
232
|
+
): Promise<TResult>; // @setup
|
|
233
|
+
declare function processDocumentWithCompletion(
|
|
234
|
+
documentId: string,
|
|
235
|
+
completionTokenArg: string
|
|
236
|
+
): Promise<void>; // @setup
|
|
258
237
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
throw new Error("Timed out waiting for children");
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
async function checkDetailedStatuses(
|
|
267
|
-
runIds: string[]
|
|
268
|
-
): Promise<Array<{ runId: string; status: string }>> {
|
|
269
|
-
"use step";
|
|
238
|
+
export async function processDocumentBatchTolerant(documentIds: string[]) {
|
|
239
|
+
"use workflow";
|
|
270
240
|
|
|
271
|
-
const
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
241
|
+
const settled = await Promise.allSettled(
|
|
242
|
+
documentIds.map((documentId) =>
|
|
243
|
+
startAndWait<{ documentId: string; summary: string }>(documentId, (token) =>
|
|
244
|
+
start(processDocumentWithCompletion, [documentId, token]).then(
|
|
245
|
+
() => undefined
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
)
|
|
249
|
+
);
|
|
250
|
+
|
|
251
|
+
const results = settled
|
|
252
|
+
.filter(
|
|
253
|
+
(entry): entry is PromiseFulfilledResult<{ documentId: string; summary: string }> =>
|
|
254
|
+
entry.status === "fulfilled"
|
|
255
|
+
)
|
|
256
|
+
.map((entry) => entry.value);
|
|
257
|
+
|
|
258
|
+
const failed = settled.filter((entry) => entry.status === "rejected").length;
|
|
259
|
+
|
|
260
|
+
return { processed: results.length, failed, results };
|
|
278
261
|
}
|
|
279
262
|
```
|
|
280
263
|
|
|
281
264
|
### Retrying failed children
|
|
282
265
|
|
|
283
|
-
When a child fails,
|
|
266
|
+
When a child fails, spawn a replacement with a fresh hook token. Track restart counts to prevent infinite retry loops.
|
|
284
267
|
|
|
285
268
|
```typescript
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
Array.from(activeRuns.values())
|
|
308
|
-
);
|
|
309
|
-
const statusByRunId = new Map(
|
|
310
|
-
statuses.map((s) => [s.runId, s.status])
|
|
311
|
-
);
|
|
312
|
-
|
|
313
|
-
for (const [index, runId] of activeRuns.entries()) {
|
|
314
|
-
const status = statusByRunId.get(runId) ?? "running";
|
|
315
|
-
|
|
316
|
-
if (status === "completed") {
|
|
317
|
-
activeRuns.delete(index);
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
if (status === "failed" || status === "cancelled") {
|
|
322
|
-
const restarts = (restartCounts.get(index) ?? 0) + 1; // [!code highlight]
|
|
323
|
-
restartCounts.set(index, restarts); // [!code highlight]
|
|
324
|
-
|
|
325
|
-
if (restarts > maxRestartsPerChild) { // [!code highlight]
|
|
326
|
-
throw new Error( // [!code highlight]
|
|
327
|
-
`Child ${index} exceeded restart limit (${maxRestartsPerChild})` // [!code highlight]
|
|
328
|
-
); // [!code highlight]
|
|
329
|
-
} // [!code highlight]
|
|
330
|
-
|
|
331
|
-
const newRunId = await spawnReplacement(index); // [!code highlight]
|
|
332
|
-
activeRuns.set(index, newRunId); // [!code highlight]
|
|
333
|
-
}
|
|
269
|
+
declare function startAndWait<TResult>(
|
|
270
|
+
key: string,
|
|
271
|
+
startChild: (completionTokenArg: string) => Promise<void>
|
|
272
|
+
): Promise<TResult>; // @setup
|
|
273
|
+
declare function spawnProcessDocument(
|
|
274
|
+
documentId: string,
|
|
275
|
+
completionTokenArg: string
|
|
276
|
+
): Promise<void>; // @setup
|
|
277
|
+
|
|
278
|
+
async function startAndWaitWithRetries(
|
|
279
|
+
documentId: string,
|
|
280
|
+
maxRestarts: number
|
|
281
|
+
): Promise<{ documentId: string; summary: string }> {
|
|
282
|
+
for (let attempt = 0; attempt <= maxRestarts; attempt++) {
|
|
283
|
+
try {
|
|
284
|
+
return await startAndWait<{ documentId: string; summary: string }>(
|
|
285
|
+
`${documentId}:${attempt}`,
|
|
286
|
+
(token) => spawnProcessDocument(documentId, token)
|
|
287
|
+
);
|
|
288
|
+
} catch (error) {
|
|
289
|
+
if (attempt === maxRestarts) throw error;
|
|
334
290
|
}
|
|
335
|
-
|
|
336
|
-
if (activeRuns.size === 0) return;
|
|
337
|
-
|
|
338
|
-
iteration += 1;
|
|
339
|
-
await sleep(POLL_INTERVAL);
|
|
340
291
|
}
|
|
341
292
|
|
|
342
|
-
throw new Error("
|
|
293
|
+
throw new Error("unreachable");
|
|
343
294
|
}
|
|
344
295
|
```
|
|
345
296
|
|
|
346
297
|
## Tips
|
|
347
298
|
|
|
348
|
-
- **`
|
|
349
|
-
-
|
|
350
|
-
- **
|
|
299
|
+
- **`defineHook().resume()` must be called from a step.** The wrapped child's `finally` block calls a step that resumes the parent hook.
|
|
300
|
+
- **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()`.
|
|
301
|
+
- **Use stable hook keys** — document ID, job ID, or index — so parallel children inside one parent run don't collide on tokens.
|
|
351
302
|
- **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.
|
|
352
|
-
- **Each child has its own retry semantics.** Steps inside child workflows retry independently. The parent
|
|
303
|
+
- **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.
|
|
353
304
|
- **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.
|
|
354
305
|
|
|
355
306
|
## Key APIs
|
|
356
307
|
|
|
357
308
|
- [`start()`](/docs/api-reference/workflow-api/start) -- spawn a new workflow run and get its run ID
|
|
358
|
-
- [`
|
|
359
|
-
- [`
|
|
309
|
+
- [`defineHook()`](/docs/api-reference/workflow/define-hook) -- typed hook for parent/child completion handshakes
|
|
310
|
+
- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) -- resume a waiting parent from a step (called by the child wrapper)
|
|
311
|
+
- [`getWorkflowMetadata()`](/docs/api-reference/workflow/get-workflow-metadata) -- read the parent run ID for deterministic hook tokens
|
|
360
312
|
- [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
|
|
361
313
|
- [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions with full Node.js access
|