workflow 5.0.0-beta.2 → 5.0.0-beta.21
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/api-workflow.d.ts +1 -1
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +2 -2
- package/dist/api.d.ts +5 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +14 -2
- 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/dist/runtime.d.ts +1 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +2 -2
- package/docs/ai/index.mdx +27 -23
- package/docs/api-reference/index.mdx +24 -0
- package/docs/api-reference/meta.json +8 -0
- package/docs/api-reference/vitest/index.mdx +28 -7
- package/docs/api-reference/workflow/create-hook.mdx +38 -0
- package/docs/api-reference/workflow/create-webhook.mdx +1 -0
- package/docs/api-reference/workflow/experimental-set-attributes.mdx +65 -0
- package/docs/api-reference/workflow/fetch.mdx +5 -0
- package/docs/api-reference/workflow/index.mdx +3 -0
- package/docs/api-reference/workflow-ai/durable-agent.mdx +7 -45
- package/docs/api-reference/workflow-api/get-hook-by-token.mdx +7 -0
- package/docs/api-reference/workflow-api/get-run.mdx +6 -0
- package/docs/api-reference/workflow-api/index.mdx +6 -8
- package/docs/api-reference/workflow-api/resume-hook.mdx +57 -0
- package/docs/api-reference/workflow-api/start.mdx +13 -5
- package/docs/api-reference/workflow-astro/index.mdx +18 -0
- package/docs/api-reference/workflow-astro/meta.json +4 -0
- package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
- package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
- package/docs/api-reference/workflow-errors/index.mdx +85 -0
- package/docs/api-reference/workflow-errors/meta.json +5 -0
- package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
- package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
- package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
- package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
- package/docs/api-reference/workflow-nest/index.mdx +31 -0
- package/docs/api-reference/workflow-nest/meta.json +9 -0
- package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
- package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
- package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
- package/docs/api-reference/workflow-next/with-workflow.mdx +56 -2
- package/docs/api-reference/workflow-nitro/index.mdx +59 -0
- package/docs/api-reference/workflow-nuxt/index.mdx +47 -0
- package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
- package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
- package/docs/api-reference/workflow-observability/index.mdx +64 -0
- package/docs/api-reference/workflow-observability/meta.json +11 -0
- package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
- package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
- package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
- package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
- package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
- package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +7 -10
- package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
- package/docs/api-reference/workflow-runtime/index.mdx +43 -0
- package/docs/api-reference/workflow-runtime/meta.json +12 -0
- package/docs/api-reference/workflow-runtime/set-world.mdx +49 -0
- package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +42 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/index.mdx +5 -8
- package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/queue.mdx +2 -2
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +11 -4
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +2 -2
- package/docs/api-reference/workflow-serde/index.mdx +0 -1
- package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +1 -2
- package/docs/api-reference/workflow-serde/workflow-serialize.mdx +1 -2
- package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
- package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
- package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
- package/docs/api-reference/workflow-vite/index.mdx +18 -0
- package/docs/api-reference/workflow-vite/meta.json +4 -0
- package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
- package/docs/changelog/attributes-mvp.mdx +380 -0
- package/docs/changelog/eager-processing.mdx +269 -0
- package/docs/changelog/index.mdx +2 -1
- package/docs/changelog/lazy-event-creation.md +127 -0
- package/docs/changelog/meta.json +7 -1
- package/docs/changelog/resilient-start.mdx +31 -283
- package/docs/changelog/turbo-mode.md +87 -0
- package/docs/cookbook/advanced/child-workflows.mdx +315 -0
- package/docs/cookbook/advanced/meta.json +2 -3
- package/docs/cookbook/advanced/publishing-libraries.mdx +87 -29
- package/docs/cookbook/advanced/serializable-steps.mdx +17 -5
- package/docs/cookbook/advanced/upgrading-workflows.mdx +195 -0
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +156 -0
- package/docs/cookbook/agent-patterns/durable-agent.mdx +11 -184
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +150 -173
- package/docs/cookbook/agent-patterns/meta.json +1 -7
- package/docs/cookbook/common-patterns/batching.mdx +44 -118
- package/docs/cookbook/common-patterns/idempotency.mdx +36 -52
- package/docs/cookbook/common-patterns/meta.json +4 -4
- package/docs/cookbook/common-patterns/rate-limiting.mdx +1 -1
- package/docs/cookbook/common-patterns/saga.mdx +128 -33
- package/docs/cookbook/common-patterns/scheduling.mdx +77 -193
- package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +155 -0
- package/docs/cookbook/common-patterns/timeouts.mdx +100 -0
- package/docs/cookbook/common-patterns/workflow-composition.mdx +117 -0
- package/docs/cookbook/index.mdx +14 -17
- package/docs/cookbook/integrations/ai-sdk.mdx +330 -142
- package/docs/cookbook/integrations/chat-sdk.mdx +264 -151
- package/docs/cookbook/integrations/sandbox.mdx +482 -81
- package/docs/cookbook/meta.json +1 -1
- package/docs/deploying/building-a-world.mdx +1 -1
- package/docs/deploying/world/postgres-world.mdx +5 -3
- package/docs/deploying/world/vercel-world.mdx +2 -0
- package/docs/errors/abort-signal-timeout-in-workflow.mdx +80 -0
- package/docs/errors/corrupted-event-log.mdx +5 -5
- package/docs/errors/hook-conflict.mdx +56 -4
- package/docs/errors/index.mdx +9 -0
- package/docs/errors/replay-divergence.mdx +27 -0
- package/docs/errors/runtime-decryption-failed.mdx +77 -0
- package/docs/errors/step-executed-multiple-times.mdx +23 -0
- package/docs/errors/step-not-registered.mdx +1 -1
- package/docs/foundations/cancellation.mdx +459 -0
- package/docs/foundations/errors-and-retries.mdx +7 -3
- package/docs/foundations/hooks.mdx +29 -0
- package/docs/foundations/idempotency.mdx +236 -11
- package/docs/foundations/index.mdx +3 -3
- package/docs/foundations/meta.json +3 -2
- package/docs/foundations/serialization.mdx +78 -42
- package/docs/foundations/starting-workflows.mdx +6 -2
- package/docs/foundations/streaming.mdx +14 -23
- package/docs/foundations/versioning.mdx +263 -0
- package/docs/getting-started/astro.mdx +6 -0
- package/docs/getting-started/index.mdx +6 -7
- package/docs/getting-started/meta.json +1 -0
- package/docs/getting-started/nestjs.mdx +9 -0
- package/docs/getting-started/next.mdx +5 -3
- package/docs/getting-started/nitro.mdx +22 -0
- package/docs/getting-started/sveltekit.mdx +6 -0
- package/docs/getting-started/tanstack-start.mdx +241 -0
- package/docs/how-it-works/cancellation.mdx +287 -0
- package/docs/how-it-works/code-transform.mdx +2 -2
- package/docs/how-it-works/encryption.mdx +2 -2
- package/docs/how-it-works/event-sourcing.mdx +2 -2
- package/docs/how-it-works/meta.json +2 -1
- package/docs/internal/index.mdx +21 -0
- package/docs/internal/meta.json +10 -0
- package/docs/internal/nitro-native-build.mdx +38 -0
- package/docs/internal/nitro-web-ui.mdx +24 -0
- package/docs/internal/serializable-abort-controller.mdx +148 -0
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +63 -16
- package/docs/migration-guides/migrating-from-inngest.mdx +44 -22
- package/docs/migration-guides/migrating-from-temporal.mdx +43 -14
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +59 -27
- package/docs/observability/attributes.mdx +87 -0
- package/docs/observability/index.mdx +25 -1
- package/docs/observability/meta.json +1 -1
- package/docs/observability/tracing.mdx +106 -0
- package/docs/testing/index.mdx +2 -2
- package/package.json +14 -13
- package/docs/api-reference/workflow-api/world/meta.json +0 -4
- package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
- package/docs/cookbook/advanced/custom-serialization.mdx +0 -168
- package/docs/cookbook/advanced/durable-objects.mdx +0 -148
- package/docs/cookbook/advanced/isomorphic-packages.mdx +0 -145
- package/docs/cookbook/agent-patterns/stop-workflow.mdx +0 -216
- package/docs/cookbook/agent-patterns/tool-orchestration.mdx +0 -255
- package/docs/cookbook/agent-patterns/tool-streaming.mdx +0 -181
- package/docs/cookbook/common-patterns/child-workflows.mdx +0 -372
- package/docs/cookbook/common-patterns/content-router.mdx +0 -207
- package/docs/cookbook/common-patterns/fan-out.mdx +0 -208
- package/docs/foundations/common-patterns.mdx +0 -265
|
@@ -1,372 +0,0 @@
|
|
|
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
|
|
@@ -1,207 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
title: Conditional Routing
|
|
3
|
-
description: Inspect a payload and route it to different step handlers based on its content.
|
|
4
|
-
type: guide
|
|
5
|
-
summary: Classify incoming messages and branch to specialized handlers using standard if/else logic in the workflow function.
|
|
6
|
-
---
|
|
7
|
-
|
|
8
|
-
Use conditional routing when incoming messages need different processing paths depending on their content. A support ticket about billing goes to the billing handler; a bug report goes to engineering. The workflow inspects the payload and branches with standard JavaScript control flow.
|
|
9
|
-
|
|
10
|
-
## When to use this
|
|
11
|
-
|
|
12
|
-
- Support ticket routing by category
|
|
13
|
-
- Order processing with different flows per product type
|
|
14
|
-
- Event handling where different event types need different logic
|
|
15
|
-
- Any message-driven system where the handler depends on the content
|
|
16
|
-
|
|
17
|
-
## Pattern: Content-based router
|
|
18
|
-
|
|
19
|
-
The workflow classifies the input, then branches with `if`/`else` to call the appropriate step:
|
|
20
|
-
|
|
21
|
-
```typescript
|
|
22
|
-
declare function classifyTicket(ticketId: string, subject: string): Promise<{ ticketType: string }>; // @setup
|
|
23
|
-
declare function handleBilling(ticketId: string): Promise<void>; // @setup
|
|
24
|
-
declare function handleTechnical(ticketId: string): Promise<void>; // @setup
|
|
25
|
-
declare function handleAccount(ticketId: string): Promise<void>; // @setup
|
|
26
|
-
declare function handleFeedback(ticketId: string): Promise<void>; // @setup
|
|
27
|
-
|
|
28
|
-
export async function routeTicket(ticketId: string, subject: string) {
|
|
29
|
-
"use workflow";
|
|
30
|
-
|
|
31
|
-
const { ticketType } = await classifyTicket(ticketId, subject); // [!code highlight]
|
|
32
|
-
|
|
33
|
-
if (ticketType === "billing") { // [!code highlight]
|
|
34
|
-
await handleBilling(ticketId);
|
|
35
|
-
} else if (ticketType === "technical") {
|
|
36
|
-
await handleTechnical(ticketId);
|
|
37
|
-
} else if (ticketType === "account") {
|
|
38
|
-
await handleAccount(ticketId);
|
|
39
|
-
} else {
|
|
40
|
-
await handleFeedback(ticketId);
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
return { ticketId, routedTo: ticketType };
|
|
44
|
-
}
|
|
45
|
-
```
|
|
46
|
-
|
|
47
|
-
### Step functions
|
|
48
|
-
|
|
49
|
-
Each handler is a separate `"use step"` function. The classification step can use an LLM, keyword matching, or any logic you need:
|
|
50
|
-
|
|
51
|
-
```typescript
|
|
52
|
-
async function classifyTicket(
|
|
53
|
-
ticketId: string,
|
|
54
|
-
subject: string
|
|
55
|
-
): Promise<{ ticketType: string }> {
|
|
56
|
-
"use step";
|
|
57
|
-
|
|
58
|
-
// Example: simple keyword classification
|
|
59
|
-
// In production, this could call an LLM or ML model
|
|
60
|
-
const lower = subject.toLowerCase();
|
|
61
|
-
if (lower.includes("invoice") || lower.includes("charge") || lower.includes("refund")) {
|
|
62
|
-
return { ticketType: "billing" };
|
|
63
|
-
}
|
|
64
|
-
if (lower.includes("error") || lower.includes("bug") || lower.includes("crash")) {
|
|
65
|
-
return { ticketType: "technical" };
|
|
66
|
-
}
|
|
67
|
-
if (lower.includes("password") || lower.includes("login") || lower.includes("access")) {
|
|
68
|
-
return { ticketType: "account" };
|
|
69
|
-
}
|
|
70
|
-
return { ticketType: "feedback" };
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
async function handleBilling(ticketId: string): Promise<void> {
|
|
74
|
-
"use step";
|
|
75
|
-
// Look up billing records, process refund, etc.
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
async function handleTechnical(ticketId: string): Promise<void> {
|
|
79
|
-
"use step";
|
|
80
|
-
// Create bug report, notify engineering, etc.
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
async function handleAccount(ticketId: string): Promise<void> {
|
|
84
|
-
"use step";
|
|
85
|
-
// Reset password, update permissions, etc.
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
async function handleFeedback(ticketId: string): Promise<void> {
|
|
89
|
-
"use step";
|
|
90
|
-
// Log feedback, notify product team, etc.
|
|
91
|
-
}
|
|
92
|
-
```
|
|
93
|
-
|
|
94
|
-
## Pattern: Enrichment before routing
|
|
95
|
-
|
|
96
|
-
When downstream handlers need more context than the raw input provides, enrich the message in parallel before routing:
|
|
97
|
-
|
|
98
|
-
```typescript
|
|
99
|
-
export async function enrichAndRoute(email: string) {
|
|
100
|
-
"use workflow";
|
|
101
|
-
|
|
102
|
-
// Step 1: Look up base data
|
|
103
|
-
const contact = await lookupContact(email);
|
|
104
|
-
|
|
105
|
-
// Step 2: Enrich from multiple sources in parallel
|
|
106
|
-
const [crm, social] = await Promise.allSettled([ // [!code highlight]
|
|
107
|
-
fetchCrmData(contact),
|
|
108
|
-
fetchSocialData(contact),
|
|
109
|
-
]);
|
|
110
|
-
|
|
111
|
-
const enriched = {
|
|
112
|
-
...contact,
|
|
113
|
-
crm: crm.status === "fulfilled" ? crm.value : null,
|
|
114
|
-
social: social.status === "fulfilled" ? social.value : null,
|
|
115
|
-
};
|
|
116
|
-
|
|
117
|
-
// Step 3: Route based on enriched data
|
|
118
|
-
if (enriched.crm?.segment === "enterprise") { // [!code highlight]
|
|
119
|
-
await routeToEnterpriseSales(enriched);
|
|
120
|
-
} else {
|
|
121
|
-
await routeToSelfServe(enriched);
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
return { email, segment: enriched.crm?.segment ?? "self-serve" };
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
async function lookupContact(email: string): Promise<{ email: string; domain: string }> {
|
|
128
|
-
"use step";
|
|
129
|
-
return { email, domain: email.split("@")[1] ?? "unknown" };
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
async function fetchCrmData(contact: { email: string }): Promise<{ segment: string }> {
|
|
133
|
-
"use step";
|
|
134
|
-
const res = await fetch(`https://crm.example.com/lookup?email=${contact.email}`);
|
|
135
|
-
return res.json();
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
async function fetchSocialData(contact: { email: string }): Promise<{ followers: number }> {
|
|
139
|
-
"use step";
|
|
140
|
-
const res = await fetch(`https://social.example.com/lookup?email=${contact.email}`);
|
|
141
|
-
return res.json();
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
async function routeToEnterpriseSales(enriched: unknown): Promise<void> {
|
|
145
|
-
"use step";
|
|
146
|
-
// Assign to enterprise sales team
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
async function routeToSelfServe(enriched: unknown): Promise<void> {
|
|
150
|
-
"use step";
|
|
151
|
-
// Add to self-serve onboarding flow
|
|
152
|
-
}
|
|
153
|
-
```
|
|
154
|
-
|
|
155
|
-
## Pattern: Multiple event sources
|
|
156
|
-
|
|
157
|
-
When a workflow must wait for signals from different systems before proceeding, create one hook per source and use `Promise.all` with a deadline:
|
|
158
|
-
|
|
159
|
-
```typescript
|
|
160
|
-
import { defineHook, sleep } from "workflow";
|
|
161
|
-
|
|
162
|
-
export const orderSignal = defineHook<{ ok: true }>();
|
|
163
|
-
|
|
164
|
-
const SIGNALS = ["payment", "inventory", "fraud"] as const;
|
|
165
|
-
|
|
166
|
-
export async function waitForAllSignals(orderId: string) {
|
|
167
|
-
"use workflow";
|
|
168
|
-
|
|
169
|
-
const hooks = SIGNALS.map((kind) =>
|
|
170
|
-
orderSignal.create({ token: `${kind}:${orderId}` }) // [!code highlight]
|
|
171
|
-
);
|
|
172
|
-
|
|
173
|
-
const outcome = await Promise.race([ // [!code highlight]
|
|
174
|
-
Promise.all(hooks).then(() => ({ type: "ready" as const })), // [!code highlight]
|
|
175
|
-
sleep("5m").then(() => ({ type: "timeout" as const })), // [!code highlight]
|
|
176
|
-
]);
|
|
177
|
-
|
|
178
|
-
if (outcome.type === "timeout") {
|
|
179
|
-
return { orderId, status: "timeout" };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
await shipOrder(orderId);
|
|
183
|
-
return { orderId, status: "shipped" };
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
async function shipOrder(orderId: string): Promise<void> {
|
|
187
|
-
"use step";
|
|
188
|
-
await fetch(`https://shipping.example.com/ship`, {
|
|
189
|
-
method: "POST",
|
|
190
|
-
body: JSON.stringify({ orderId }),
|
|
191
|
-
});
|
|
192
|
-
}
|
|
193
|
-
```
|
|
194
|
-
|
|
195
|
-
## Tips
|
|
196
|
-
|
|
197
|
-
- **Workflow functions use standard JavaScript.** `if`/`else`, `switch`, ternaries -- any branching logic works. No special routing DSL needed.
|
|
198
|
-
- **Each handler is an independent step.** This means each gets its own retries, its own error handling, and its own entry in the event log.
|
|
199
|
-
- **Combine with enrichment** when downstream handlers need data from multiple sources. Fan out enrichment with `Promise.allSettled`, then route on the merged result.
|
|
200
|
-
- **Use `defineHook` for event gateways** when the routing decision depends on external signals arriving asynchronously.
|
|
201
|
-
|
|
202
|
-
## Key APIs
|
|
203
|
-
|
|
204
|
-
- [`"use workflow"`](/docs/api-reference/workflow/use-workflow) -- marks the orchestrator function
|
|
205
|
-
- [`"use step"`](/docs/api-reference/workflow/use-step) -- marks each handler as a durable step
|
|
206
|
-
- [`defineHook()`](/docs/api-reference/workflow/define-hook) -- creates hooks for event gateway patterns
|
|
207
|
-
- [`sleep()`](/docs/api-reference/workflow/sleep) -- durable deadline for event gateways
|