workflow 5.0.0-beta.1 → 5.0.0-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -4
- package/dist/api-workflow.js +1 -1
- package/dist/api.js +1 -1
- package/dist/astro.js +1 -1
- package/dist/index.js +1 -1
- package/dist/internal/builtins.js +1 -1
- package/dist/internal/class-serialization.js +1 -1
- package/dist/internal/errors.js +1 -1
- package/dist/nest.js +1 -1
- package/dist/next.cjs +1 -1
- package/dist/nitro.js +1 -1
- package/dist/nuxt.js +1 -1
- package/dist/observability.js +1 -1
- package/dist/runtime.js +1 -1
- package/dist/stdlib.js +1 -1
- package/dist/sveltekit.js +1 -1
- package/dist/typescript-plugin.cjs +1 -1
- package/dist/vite.js +1 -1
- package/dist/workflow.js +1 -1
- package/docs/ai/resumable-streams.mdx +1 -1
- package/docs/api-reference/workflow/create-webhook.mdx +37 -18
- package/docs/api-reference/workflow/get-workflow-metadata.mdx +34 -0
- package/docs/api-reference/workflow-ai/durable-agent.mdx +0 -4
- package/docs/api-reference/workflow-ai/index.mdx +0 -5
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +0 -4
- package/docs/cookbook/advanced/custom-serialization.mdx +168 -0
- package/docs/cookbook/advanced/durable-objects.mdx +148 -0
- package/docs/cookbook/advanced/isomorphic-packages.mdx +145 -0
- package/docs/cookbook/advanced/meta.json +10 -0
- package/docs/cookbook/advanced/publishing-libraries.mdx +279 -0
- package/docs/cookbook/advanced/serializable-steps.mdx +135 -0
- package/docs/cookbook/agent-patterns/durable-agent.mdx +191 -0
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +278 -0
- package/docs/cookbook/agent-patterns/meta.json +10 -0
- package/docs/cookbook/agent-patterns/stop-workflow.mdx +216 -0
- package/docs/cookbook/agent-patterns/tool-orchestration.mdx +255 -0
- package/docs/cookbook/agent-patterns/tool-streaming.mdx +181 -0
- package/docs/cookbook/common-patterns/batching.mdx +179 -0
- package/docs/cookbook/common-patterns/child-workflows.mdx +372 -0
- package/docs/cookbook/common-patterns/content-router.mdx +207 -0
- package/docs/cookbook/common-patterns/fan-out.mdx +208 -0
- package/docs/cookbook/common-patterns/idempotency.mdx +107 -0
- package/docs/cookbook/common-patterns/meta.json +15 -0
- package/docs/cookbook/common-patterns/rate-limiting.mdx +228 -0
- package/docs/cookbook/common-patterns/saga.mdx +152 -0
- package/docs/cookbook/common-patterns/scheduling.mdx +249 -0
- package/docs/cookbook/common-patterns/webhooks.mdx +185 -0
- package/docs/cookbook/index.mdx +41 -0
- package/docs/cookbook/integrations/ai-sdk.mdx +204 -0
- package/docs/cookbook/integrations/chat-sdk.mdx +203 -0
- package/docs/cookbook/integrations/meta.json +4 -0
- package/docs/cookbook/integrations/sandbox.mdx +128 -0
- package/docs/cookbook/meta.json +5 -0
- package/docs/deploying/world/local-world.mdx +1 -1
- package/docs/deploying/world/postgres-world.mdx +1 -1
- package/docs/deploying/world/vercel-world.mdx +1 -1
- package/docs/errors/start-invalid-workflow-function.mdx +1 -1
- package/docs/getting-started/index.mdx +8 -1
- package/docs/getting-started/meta.json +2 -1
- package/docs/getting-started/python.mdx +165 -0
- package/docs/meta.json +1 -0
- package/docs/migration-guides/index.mdx +34 -0
- package/docs/migration-guides/meta.json +9 -0
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +311 -0
- package/docs/migration-guides/migrating-from-inngest.mdx +282 -0
- package/docs/migration-guides/migrating-from-temporal.mdx +284 -0
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +296 -0
- package/package.json +13 -13
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Batching & Parallel Processing
|
|
3
|
+
description: Process large collections in parallel batches with failure isolation between groups.
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Split items into fixed-size batches, process each batch concurrently with Promise.allSettled, and pace batches with sleep to avoid overloading downstream services.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Use batching when you need to process a large list of items in parallel while controlling concurrency. Items are split into fixed-size batches, each batch runs concurrently, and failures in one batch don't affect others.
|
|
9
|
+
|
|
10
|
+
## When to use this
|
|
11
|
+
|
|
12
|
+
- Processing hundreds or thousands of items (orders, images, records)
|
|
13
|
+
- Calling rate-limited APIs where you need to control concurrency
|
|
14
|
+
- Any fan-out where you want failure isolation between groups
|
|
15
|
+
|
|
16
|
+
## Pattern
|
|
17
|
+
|
|
18
|
+
The workflow splits items into chunks and processes each chunk with `Promise.allSettled()`. A `sleep()` between chunks prevents overloading downstream services.
|
|
19
|
+
|
|
20
|
+
```typescript
|
|
21
|
+
import { sleep } from "workflow";
|
|
22
|
+
|
|
23
|
+
declare function processItem(item: string): Promise<{ item: string; ok: boolean }>; // @setup
|
|
24
|
+
|
|
25
|
+
export async function processBatch(items: string[], batchSize: number = 5) {
|
|
26
|
+
"use workflow";
|
|
27
|
+
|
|
28
|
+
const results = [];
|
|
29
|
+
|
|
30
|
+
for (let i = 0; i < items.length; i += batchSize) {
|
|
31
|
+
const batch = items.slice(i, i + batchSize);
|
|
32
|
+
|
|
33
|
+
// Run batch in parallel -- failures are isolated
|
|
34
|
+
const outcomes = await Promise.allSettled( // [!code highlight]
|
|
35
|
+
batch.map((item) => processItem(item))
|
|
36
|
+
);
|
|
37
|
+
|
|
38
|
+
for (let j = 0; j < outcomes.length; j++) {
|
|
39
|
+
const outcome = outcomes[j];
|
|
40
|
+
results.push(
|
|
41
|
+
outcome.status === "fulfilled"
|
|
42
|
+
? outcome.value
|
|
43
|
+
: { item: batch[j], ok: false, error: String(outcome.reason) }
|
|
44
|
+
);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Pace between batches to avoid overload
|
|
48
|
+
if (i + batchSize < items.length) {
|
|
49
|
+
await sleep("1s"); // [!code highlight]
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const succeeded = results.filter((r) => r.ok).length;
|
|
54
|
+
return { total: results.length, succeeded, failed: results.length - succeeded };
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
### Step function
|
|
59
|
+
|
|
60
|
+
Each item is processed in its own step, giving it full Node.js access and automatic retries.
|
|
61
|
+
|
|
62
|
+
```typescript
|
|
63
|
+
async function processItem(item: string): Promise<{ item: string; ok: boolean }> {
|
|
64
|
+
"use step";
|
|
65
|
+
const res = await fetch(`https://api.example.com/process`, {
|
|
66
|
+
method: "POST",
|
|
67
|
+
body: JSON.stringify({ item }),
|
|
68
|
+
});
|
|
69
|
+
if (!res.ok) throw new Error(`Failed to process ${item}`);
|
|
70
|
+
return { item, ok: true };
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
## Variations
|
|
75
|
+
|
|
76
|
+
### Scatter-gather
|
|
77
|
+
|
|
78
|
+
When you need results from multiple independent sources before continuing, fan out in parallel and collect all results:
|
|
79
|
+
|
|
80
|
+
```typescript
|
|
81
|
+
export async function scatterGather(query: string) {
|
|
82
|
+
"use workflow";
|
|
83
|
+
|
|
84
|
+
const [web, database, cache] = await Promise.allSettled([ // [!code highlight]
|
|
85
|
+
searchWeb(query),
|
|
86
|
+
searchDatabase(query),
|
|
87
|
+
searchCache(query),
|
|
88
|
+
]);
|
|
89
|
+
|
|
90
|
+
return {
|
|
91
|
+
web: web.status === "fulfilled" ? web.value : null,
|
|
92
|
+
database: database.status === "fulfilled" ? database.value : null,
|
|
93
|
+
cache: cache.status === "fulfilled" ? cache.value : null,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async function searchWeb(query: string): Promise<string[]> {
|
|
98
|
+
"use step";
|
|
99
|
+
// Full Node.js access -- call external APIs
|
|
100
|
+
const res = await fetch(`https://search.example.com?q=${query}`);
|
|
101
|
+
return res.json();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async function searchDatabase(query: string): Promise<string[]> {
|
|
105
|
+
"use step";
|
|
106
|
+
// Query your database
|
|
107
|
+
return [`db-result-for-${query}`];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function searchCache(query: string): Promise<string[]> {
|
|
111
|
+
"use step";
|
|
112
|
+
return [`cached-result-for-${query}`];
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
## In-step concurrency control
|
|
117
|
+
|
|
118
|
+
When you need to process many items against a rate-limited API but want the entire operation to be a single atomic step, batch the work inside the step itself. This keeps the event log clean (one step instead of hundreds) while still controlling concurrency.
|
|
119
|
+
|
|
120
|
+
```typescript
|
|
121
|
+
async function processConcurrently<T>(
|
|
122
|
+
items: string[],
|
|
123
|
+
processor: (item: string) => Promise<T>,
|
|
124
|
+
maxConcurrent: number = 5,
|
|
125
|
+
): Promise<T[]> {
|
|
126
|
+
"use step";
|
|
127
|
+
const results: T[] = [];
|
|
128
|
+
|
|
129
|
+
for (let i = 0; i < items.length; i += maxConcurrent) {
|
|
130
|
+
const batch = items.slice(i, i + maxConcurrent);
|
|
131
|
+
const batchResults = await Promise.all(batch.map(processor)); // [!code highlight]
|
|
132
|
+
results.push(...batchResults);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return results;
|
|
136
|
+
}
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
Usage in a workflow:
|
|
140
|
+
|
|
141
|
+
```typescript
|
|
142
|
+
declare function processConcurrently<T>(items: string[], processor: (item: string) => Promise<T>, maxConcurrent?: number): Promise<T[]>; // @setup
|
|
143
|
+
|
|
144
|
+
export async function moderateImages(imageUrls: string[]) {
|
|
145
|
+
"use workflow";
|
|
146
|
+
|
|
147
|
+
const results = await processConcurrently(
|
|
148
|
+
imageUrls,
|
|
149
|
+
async (url) => {
|
|
150
|
+
const res = await fetch("https://api.example.com/moderate", {
|
|
151
|
+
method: "POST",
|
|
152
|
+
body: JSON.stringify({ url }),
|
|
153
|
+
});
|
|
154
|
+
return res.json();
|
|
155
|
+
},
|
|
156
|
+
3, // max 3 concurrent API calls
|
|
157
|
+
);
|
|
158
|
+
|
|
159
|
+
return { total: results.length, results };
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
**When to use in-step batching vs workflow-level batching:**
|
|
164
|
+
- **Workflow-level** (the pattern above): Each item is its own step with independent retries and failure isolation. Use when items are independent and individual failures should be retried.
|
|
165
|
+
- **In-step**: All items are processed in one step. Use when the items are tightly coupled (e.g., moderating all thumbnails for a single video) or when you want to minimize step overhead for large item counts.
|
|
166
|
+
|
|
167
|
+
## Tips
|
|
168
|
+
|
|
169
|
+
- **Use `Promise.allSettled` over `Promise.all`** when you want to continue even if some items fail. `Promise.all` rejects on the first failure; `allSettled` waits for everything and tells you what failed.
|
|
170
|
+
- **Tune batch size to your downstream API limits.** If the API allows 10 concurrent requests, use `batchSize: 10`.
|
|
171
|
+
- **Add pacing with `sleep()`** between batches to respect rate limits. The sleep is durable -- it survives cold starts.
|
|
172
|
+
- **Each `processItem` call is an independent step.** If one fails, it retries up to 3 times without affecting other items in the batch.
|
|
173
|
+
|
|
174
|
+
## Key APIs
|
|
175
|
+
|
|
176
|
+
- [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
|
|
177
|
+
- [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions that run with full Node.js access
|
|
178
|
+
- [`sleep()`](/docs/api-reference/workflow/sleep) -- pacing delay between batches
|
|
179
|
+
- [`Promise.allSettled()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) -- runs items in parallel, isolating failures
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Child Workflows
|
|
3
|
+
description: Spawn child workflows from a parent and poll their progress for batch processing, report generation, and other multi-workflow orchestration scenarios.
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Orchestrate independent child workflows from a parent workflow using start(), sleep(), and getRun() to fan out work with isolated failure boundaries.
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
Use child workflows when a single workflow needs to orchestrate many independent units of work. Each child runs as its own workflow with a separate event log, retry boundary, and failure scope -- if one child fails, it doesn't take down the parent or siblings.
|
|
9
|
+
|
|
10
|
+
## When to use child workflows
|
|
11
|
+
|
|
12
|
+
Child workflows are the right choice when:
|
|
13
|
+
|
|
14
|
+
- **Work units are independent.** Each child can run without knowing about the others (e.g., processing individual documents, generating separate reports).
|
|
15
|
+
- **You need isolated failure boundaries.** A failing child should not abort unrelated work. The parent decides how to handle failures.
|
|
16
|
+
- **You want massive fan-out.** Spawning 50 or 500 children is practical because each runs on its own infrastructure.
|
|
17
|
+
- **You need per-item observability.** Each child workflow has its own run ID, status, and event log for monitoring.
|
|
18
|
+
|
|
19
|
+
For simpler cases where steps share a single event log, use [direct await composition](/docs/foundations/common-patterns#direct-await-flattening) instead.
|
|
20
|
+
|
|
21
|
+
## Basic pattern: spawn and poll
|
|
22
|
+
|
|
23
|
+
The core pattern has three parts:
|
|
24
|
+
|
|
25
|
+
1. A **step** that calls `start()` to spawn a child workflow and returns the run ID
|
|
26
|
+
2. A **polling loop** in the parent workflow that checks child status with `getRun()`
|
|
27
|
+
3. A **step** that retrieves the child's return value once it completes
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import { sleep } from "workflow";
|
|
31
|
+
import { getRun, start } from "workflow/api";
|
|
32
|
+
|
|
33
|
+
declare function pollUntilComplete(runIds: string[]): Promise<void>; // @setup
|
|
34
|
+
declare function collectResults(runIds: string[]): Promise<Array<{ documentId: string; summary: string }>>; // @setup
|
|
35
|
+
|
|
36
|
+
// Child workflow -- processes a single document
|
|
37
|
+
export async function processDocument(documentId: string) {
|
|
38
|
+
"use workflow";
|
|
39
|
+
|
|
40
|
+
const content = await fetchDocument(documentId);
|
|
41
|
+
const analysis = await analyzeContent(content);
|
|
42
|
+
const summary = await generateSummary(analysis);
|
|
43
|
+
|
|
44
|
+
return { documentId, summary };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
async function fetchDocument(documentId: string): Promise<string> {
|
|
48
|
+
"use step";
|
|
49
|
+
const res = await fetch(`https://docs.example.com/api/${documentId}`);
|
|
50
|
+
return res.text();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
async function analyzeContent(content: string): Promise<string> {
|
|
54
|
+
"use step";
|
|
55
|
+
// Call analysis API
|
|
56
|
+
return `analysis of ${content.length} chars`;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function generateSummary(analysis: string): Promise<string> {
|
|
60
|
+
"use step";
|
|
61
|
+
// Generate summary from analysis
|
|
62
|
+
return `Summary: ${analysis}`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Parent workflow -- orchestrates document processing
|
|
66
|
+
export async function processDocumentBatch(documentIds: string[]) {
|
|
67
|
+
"use workflow";
|
|
68
|
+
|
|
69
|
+
// Spawn a child workflow for each document
|
|
70
|
+
const runIds = await spawnChildren(documentIds);
|
|
71
|
+
|
|
72
|
+
// Poll until all children complete
|
|
73
|
+
await pollUntilComplete(runIds);
|
|
74
|
+
|
|
75
|
+
// Collect results
|
|
76
|
+
const results = await collectResults(runIds);
|
|
77
|
+
|
|
78
|
+
return { processed: results.length, results };
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async function spawnChildren(
|
|
82
|
+
documentIds: string[]
|
|
83
|
+
): Promise<string[]> {
|
|
84
|
+
"use step"; // [!code highlight]
|
|
85
|
+
|
|
86
|
+
const runIds: string[] = [];
|
|
87
|
+
for (const docId of documentIds) {
|
|
88
|
+
const run = await start(processDocument, [docId]); // [!code highlight]
|
|
89
|
+
runIds.push(run.runId);
|
|
90
|
+
}
|
|
91
|
+
return runIds;
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Polling loop
|
|
96
|
+
|
|
97
|
+
The parent workflow polls child statuses in a loop, sleeping between checks. This is durable -- if the parent replays, the sleep and status checks replay from the event log.
|
|
98
|
+
|
|
99
|
+
```typescript
|
|
100
|
+
import { sleep } from "workflow";
|
|
101
|
+
import { getRun } from "workflow/api";
|
|
102
|
+
|
|
103
|
+
const POLL_INTERVAL = "30s";
|
|
104
|
+
const MAX_POLL_ITERATIONS = 120; // 60 minutes at 30s intervals
|
|
105
|
+
|
|
106
|
+
async function pollUntilComplete(runIds: string[]): Promise<void> {
|
|
107
|
+
let iteration = 0;
|
|
108
|
+
|
|
109
|
+
while (iteration < MAX_POLL_ITERATIONS) {
|
|
110
|
+
const status = await checkStatuses(runIds); // [!code highlight]
|
|
111
|
+
|
|
112
|
+
if (status.running === 0) {
|
|
113
|
+
if (status.failed > 0) {
|
|
114
|
+
throw new Error(
|
|
115
|
+
`${status.failed} of ${runIds.length} children failed`
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
return; // All completed successfully
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
iteration += 1;
|
|
122
|
+
await sleep(POLL_INTERVAL); // [!code highlight]
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
throw new Error("Timed out waiting for children to complete");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function checkStatuses(
|
|
129
|
+
runIds: string[]
|
|
130
|
+
): Promise<{ running: number; completed: number; failed: number }> {
|
|
131
|
+
"use step"; // [!code highlight]
|
|
132
|
+
|
|
133
|
+
let running = 0;
|
|
134
|
+
let completed = 0;
|
|
135
|
+
let failed = 0;
|
|
136
|
+
|
|
137
|
+
for (const runId of runIds) {
|
|
138
|
+
const run = getRun(runId); // [!code highlight]
|
|
139
|
+
const status = await run.status; // [!code highlight]
|
|
140
|
+
|
|
141
|
+
if (status === "completed") completed += 1;
|
|
142
|
+
else if (status === "failed" || status === "cancelled") failed += 1;
|
|
143
|
+
else running += 1; // pending, running
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
return { running, completed, failed };
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
async function collectResults(
|
|
150
|
+
runIds: string[]
|
|
151
|
+
): Promise<Array<{ documentId: string; summary: string }>> {
|
|
152
|
+
"use step";
|
|
153
|
+
|
|
154
|
+
const results = [];
|
|
155
|
+
for (const runId of runIds) {
|
|
156
|
+
const run = getRun(runId);
|
|
157
|
+
const value = await run.returnValue;
|
|
158
|
+
results.push(value as { documentId: string; summary: string });
|
|
159
|
+
}
|
|
160
|
+
return results;
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
## Fan-out pattern: chunked spawning
|
|
165
|
+
|
|
166
|
+
When spawning hundreds of children, batch the `start()` calls to avoid overwhelming the system. Use multiple spawn steps, each launching a chunk of children.
|
|
167
|
+
|
|
168
|
+
```typescript
|
|
169
|
+
import { start } from "workflow/api";
|
|
170
|
+
|
|
171
|
+
declare function pollUntilComplete(runIds: string[]): Promise<void>; // @setup
|
|
172
|
+
|
|
173
|
+
const CHUNK_SIZE = 10;
|
|
174
|
+
|
|
175
|
+
export async function largeReportBatch(reportConfigs: Array<{ id: string; query: string }>) {
|
|
176
|
+
"use workflow";
|
|
177
|
+
|
|
178
|
+
// Spawn children in chunks
|
|
179
|
+
const allRunIds: string[] = [];
|
|
180
|
+
for (let i = 0; i < reportConfigs.length; i += CHUNK_SIZE) {
|
|
181
|
+
const chunk = reportConfigs.slice(i, i + CHUNK_SIZE);
|
|
182
|
+
const runIds = await spawnReportChunk(chunk); // [!code highlight]
|
|
183
|
+
allRunIds.push(...runIds);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// Poll until all complete
|
|
187
|
+
await pollUntilComplete(allRunIds);
|
|
188
|
+
|
|
189
|
+
const results = await collectReportResults(allRunIds);
|
|
190
|
+
return { total: results.length, results };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function spawnReportChunk(
|
|
194
|
+
configs: Array<{ id: string; query: string }>
|
|
195
|
+
): Promise<string[]> {
|
|
196
|
+
"use step";
|
|
197
|
+
|
|
198
|
+
const runIds: string[] = [];
|
|
199
|
+
for (const config of configs) {
|
|
200
|
+
const run = await start(generateReport, [config.id, config.query]);
|
|
201
|
+
runIds.push(run.runId);
|
|
202
|
+
}
|
|
203
|
+
return runIds;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
async function generateReport(reportId: string, query: string) {
|
|
207
|
+
"use workflow";
|
|
208
|
+
|
|
209
|
+
const data = await queryDatabase(reportId, query);
|
|
210
|
+
const formatted = await formatReport(reportId, data);
|
|
211
|
+
return { reportId, formatted };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
declare function queryDatabase(reportId: string, query: string): Promise<string>; // @setup
|
|
215
|
+
declare function formatReport(reportId: string, data: string): Promise<string>; // @setup
|
|
216
|
+
|
|
217
|
+
declare function collectReportResults(
|
|
218
|
+
runIds: string[]
|
|
219
|
+
): Promise<Array<{ reportId: string; formatted: string }>>; // @setup
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Error handling
|
|
223
|
+
|
|
224
|
+
### Tolerating partial failures
|
|
225
|
+
|
|
226
|
+
Not every batch requires 100% success. Use `allowFailures` logic to let the parent continue when some children fail, while still surfacing the failures.
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
import { sleep } from "workflow";
|
|
230
|
+
import { getRun } from "workflow/api";
|
|
231
|
+
|
|
232
|
+
const POLL_INTERVAL = "30s";
|
|
233
|
+
const MAX_POLL_ITERATIONS = 120;
|
|
234
|
+
|
|
235
|
+
async function pollWithPartialFailures(
|
|
236
|
+
runIds: string[],
|
|
237
|
+
maxFailureRate: number
|
|
238
|
+
): Promise<{ completed: string[]; failed: string[] }> {
|
|
239
|
+
let iteration = 0;
|
|
240
|
+
const completedIds: string[] = [];
|
|
241
|
+
const failedIds: string[] = [];
|
|
242
|
+
|
|
243
|
+
while (iteration < MAX_POLL_ITERATIONS) {
|
|
244
|
+
const status = await checkDetailedStatuses(runIds);
|
|
245
|
+
|
|
246
|
+
completedIds.length = 0;
|
|
247
|
+
failedIds.length = 0;
|
|
248
|
+
|
|
249
|
+
for (const entry of status) {
|
|
250
|
+
if (entry.status === "completed") completedIds.push(entry.runId);
|
|
251
|
+
else if (entry.status === "failed" || entry.status === "cancelled")
|
|
252
|
+
failedIds.push(entry.runId);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const active = runIds.length - completedIds.length - failedIds.length;
|
|
256
|
+
|
|
257
|
+
// Check if failure rate exceeds threshold
|
|
258
|
+
const failureRate = failedIds.length / Math.max(1, runIds.length); // [!code highlight]
|
|
259
|
+
if (failureRate > maxFailureRate) { // [!code highlight]
|
|
260
|
+
throw new Error( // [!code highlight]
|
|
261
|
+
`Failure rate ${(failureRate * 100).toFixed(1)}% exceeds ` + // [!code highlight]
|
|
262
|
+
`threshold of ${(maxFailureRate * 100).toFixed(1)}%` // [!code highlight]
|
|
263
|
+
); // [!code highlight]
|
|
264
|
+
} // [!code highlight]
|
|
265
|
+
|
|
266
|
+
if (active === 0) {
|
|
267
|
+
return { completed: completedIds, failed: failedIds };
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
iteration += 1;
|
|
271
|
+
await sleep(POLL_INTERVAL);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
throw new Error("Timed out waiting for children");
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async function checkDetailedStatuses(
|
|
278
|
+
runIds: string[]
|
|
279
|
+
): Promise<Array<{ runId: string; status: string }>> {
|
|
280
|
+
"use step";
|
|
281
|
+
|
|
282
|
+
const statuses = [];
|
|
283
|
+
for (const runId of runIds) {
|
|
284
|
+
const run = getRun(runId);
|
|
285
|
+
const status = await run.status;
|
|
286
|
+
statuses.push({ runId, status });
|
|
287
|
+
}
|
|
288
|
+
return statuses;
|
|
289
|
+
}
|
|
290
|
+
```
|
|
291
|
+
|
|
292
|
+
### Retrying failed children
|
|
293
|
+
|
|
294
|
+
When a child fails, the parent can spawn a replacement and continue polling. Track restart counts to prevent infinite retry loops.
|
|
295
|
+
|
|
296
|
+
```typescript
|
|
297
|
+
import { sleep } from "workflow";
|
|
298
|
+
|
|
299
|
+
declare function checkDetailedStatuses(runIds: string[]): Promise<Array<{ runId: string; status: string }>>; // @setup
|
|
300
|
+
|
|
301
|
+
const POLL_INTERVAL = "30s";
|
|
302
|
+
const MAX_POLL_ITERATIONS = 120;
|
|
303
|
+
|
|
304
|
+
async function pollWithRetries(
|
|
305
|
+
initialRunIds: string[],
|
|
306
|
+
maxRestartsPerChild: number,
|
|
307
|
+
spawnReplacement: (index: number) => Promise<string>
|
|
308
|
+
): Promise<void> {
|
|
309
|
+
const activeRuns = new Map<number, string>();
|
|
310
|
+
const restartCounts = new Map<number, number>();
|
|
311
|
+
|
|
312
|
+
initialRunIds.forEach((runId, index) => activeRuns.set(index, runId));
|
|
313
|
+
|
|
314
|
+
let iteration = 0;
|
|
315
|
+
|
|
316
|
+
while (iteration < MAX_POLL_ITERATIONS) {
|
|
317
|
+
const statuses = await checkDetailedStatuses(
|
|
318
|
+
Array.from(activeRuns.values())
|
|
319
|
+
);
|
|
320
|
+
const statusByRunId = new Map(
|
|
321
|
+
statuses.map((s) => [s.runId, s.status])
|
|
322
|
+
);
|
|
323
|
+
|
|
324
|
+
for (const [index, runId] of activeRuns.entries()) {
|
|
325
|
+
const status = statusByRunId.get(runId) ?? "running";
|
|
326
|
+
|
|
327
|
+
if (status === "completed") {
|
|
328
|
+
activeRuns.delete(index);
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (status === "failed" || status === "cancelled") {
|
|
333
|
+
const restarts = (restartCounts.get(index) ?? 0) + 1; // [!code highlight]
|
|
334
|
+
restartCounts.set(index, restarts); // [!code highlight]
|
|
335
|
+
|
|
336
|
+
if (restarts > maxRestartsPerChild) { // [!code highlight]
|
|
337
|
+
throw new Error( // [!code highlight]
|
|
338
|
+
`Child ${index} exceeded restart limit (${maxRestartsPerChild})` // [!code highlight]
|
|
339
|
+
); // [!code highlight]
|
|
340
|
+
} // [!code highlight]
|
|
341
|
+
|
|
342
|
+
const newRunId = await spawnReplacement(index); // [!code highlight]
|
|
343
|
+
activeRuns.set(index, newRunId); // [!code highlight]
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
if (activeRuns.size === 0) return;
|
|
348
|
+
|
|
349
|
+
iteration += 1;
|
|
350
|
+
await sleep(POLL_INTERVAL);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
throw new Error("Timed out waiting for children");
|
|
354
|
+
}
|
|
355
|
+
```
|
|
356
|
+
|
|
357
|
+
## Tips
|
|
358
|
+
|
|
359
|
+
- **`start()` must be called from a step**, not directly from a workflow function. Wrap it in a `"use step"` function.
|
|
360
|
+
- **`getRun()` must also be called from a step.** The polling loop lives in the workflow, but the actual status check is a step.
|
|
361
|
+
- **Set a max iteration count on polling loops** to prevent runaway workflows. Calculate the count from your expected max duration and poll interval.
|
|
362
|
+
- **Use chunked spawning for large batches.** Spawning 500 children in a single step can time out. Break it into chunks of 10-50.
|
|
363
|
+
- **Each child has its own retry semantics.** Steps inside child workflows retry independently. The parent only sees the child's final status.
|
|
364
|
+
- **Use `deploymentId: "latest"`** if children should run on the most recent deployment. See the [`start()` API reference](/docs/api-reference/workflow-api/start#using-deploymentid-latest) for compatibility considerations.
|
|
365
|
+
|
|
366
|
+
## Key APIs
|
|
367
|
+
|
|
368
|
+
- [`start()`](/docs/api-reference/workflow-api/start) -- spawn a new workflow run and get its run ID
|
|
369
|
+
- [`getRun()`](/docs/api-reference/workflow-api/get-run) -- retrieve a workflow run's status and return value
|
|
370
|
+
- [`sleep()`](/docs/api-reference/workflow/sleep) -- durably pause between polling iterations
|
|
371
|
+
- [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
|
|
372
|
+
- [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions with full Node.js access
|