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.
- package/dist/api-workflow.d.ts +1 -1
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +1 -1
- package/dist/api.d.ts +5 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +12 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- 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 +2 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +5 -2
- package/docs/ai/chat-session-modeling.mdx +7 -3
- package/docs/ai/defining-tools.mdx +2 -2
- package/docs/ai/index.mdx +28 -23
- package/docs/ai/message-queueing.mdx +10 -10
- package/docs/ai/resumable-streams.mdx +9 -1
- 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-ai/index.mdx +2 -2
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +44 -1
- package/docs/api-reference/workflow-api/get-hook-by-token.mdx +7 -0
- package/docs/api-reference/workflow-api/get-run.mdx +25 -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 +60 -0
- package/docs/api-reference/workflow-nuxt/index.mdx +48 -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 +4 -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 +8 -1
- package/docs/changelog/resilient-start.mdx +31 -283
- package/docs/changelog/step-message-ownership.mdx +360 -0
- package/docs/changelog/turbo-mode.md +87 -0
- package/docs/configuration/build-and-diagnostics.mdx +51 -0
- package/docs/configuration/cli-and-web-ui.mdx +154 -0
- package/docs/configuration/framework-options.mdx +165 -0
- package/docs/configuration/index.mdx +32 -0
- package/docs/configuration/meta.json +12 -0
- package/docs/configuration/runtime-tuning.mdx +156 -0
- package/docs/configuration/worlds.mdx +228 -0
- package/docs/cookbook/advanced/child-workflows.mdx +199 -256
- package/docs/cookbook/advanced/meta.json +1 -1
- package/docs/cookbook/advanced/publishing-libraries.mdx +13 -12
- package/docs/cookbook/advanced/serializable-steps.mdx +8 -4
- package/docs/cookbook/advanced/upgrading-workflows.mdx +195 -0
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +31 -76
- package/docs/cookbook/agent-patterns/durable-agent.mdx +11 -143
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +7 -3
- package/docs/cookbook/common-patterns/idempotency.mdx +36 -52
- package/docs/cookbook/common-patterns/rate-limiting.mdx +1 -1
- package/docs/cookbook/common-patterns/saga.mdx +2 -2
- package/docs/cookbook/common-patterns/scheduling.mdx +8 -0
- package/docs/cookbook/common-patterns/timeouts.mdx +2 -1
- package/docs/cookbook/common-patterns/workflow-composition.mdx +15 -16
- package/docs/cookbook/index.mdx +3 -3
- package/docs/cookbook/integrations/ai-sdk.mdx +60 -28
- package/docs/cookbook/integrations/chat-sdk.mdx +13 -0
- package/docs/cookbook/integrations/sandbox.mdx +13 -0
- package/docs/deploying/building-a-world.mdx +1 -1
- package/docs/deploying/index.mdx +1 -0
- package/docs/deploying/world/local-world.mdx +23 -6
- package/docs/deploying/world/postgres-world.mdx +79 -13
- package/docs/deploying/world/vercel-world.mdx +47 -12
- 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 +1 -35
- 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 +1 -23
- package/docs/foundations/meta.json +3 -1
- package/docs/foundations/serialization.mdx +77 -41
- package/docs/foundations/starting-workflows.mdx +5 -1
- 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/meta.json +1 -1
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +7 -12
- package/docs/migration-guides/migrating-from-inngest.mdx +7 -17
- package/docs/migration-guides/migrating-from-temporal.mdx +6 -11
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +8 -17
- package/docs/observability/attributes.mdx +87 -0
- package/docs/observability/index.mdx +18 -1
- package/docs/observability/meta.json +1 -1
- package/docs/observability/tracing.mdx +119 -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/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
|
|
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
|
+
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
|
|
23
|
+
## Basic pattern: spawn and wait via hook
|
|
22
24
|
|
|
23
|
-
The
|
|
25
|
+
The recommended pattern has four parts:
|
|
24
26
|
|
|
25
|
-
1. A **
|
|
26
|
-
2. A **
|
|
27
|
-
3. A **
|
|
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 {
|
|
31
|
-
import {
|
|
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
|
-
|
|
34
|
-
|
|
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
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
104
|
+
await withChildCompletionHook(
|
|
105
|
+
() => processDocument(documentId),
|
|
106
|
+
completionTokenArg
|
|
107
|
+
);
|
|
57
108
|
}
|
|
58
109
|
|
|
59
|
-
async function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
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
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
###
|
|
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
|
-
|
|
147
|
-
}
|
|
145
|
+
Polling with `getRun().status` in a `sleep()` loop works, but hook resume is preferable because:
|
|
148
146
|
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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(
|
|
168
|
+
export async function largeReportBatch(
|
|
169
|
+
reportConfigs: Array<{ id: string; query: string }>
|
|
170
|
+
) {
|
|
176
171
|
"use workflow";
|
|
177
172
|
|
|
178
|
-
|
|
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
|
|
183
|
-
|
|
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
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
193
|
+
async function generateReportWithCompletion(
|
|
194
|
+
reportId: string,
|
|
195
|
+
query: string,
|
|
196
|
+
completionTokenArg: string
|
|
197
|
+
) {
|
|
198
|
+
"use workflow";
|
|
197
199
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
218
|
-
|
|
219
|
-
): Promise<
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
|
278
|
-
|
|
279
|
-
): Promise<Array<{ runId: string; status: string }>> {
|
|
280
|
-
"use step";
|
|
240
|
+
export async function processDocumentBatchTolerant(documentIds: string[]) {
|
|
241
|
+
"use workflow";
|
|
281
242
|
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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,
|
|
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
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
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("
|
|
295
|
+
throw new Error("unreachable");
|
|
354
296
|
}
|
|
355
297
|
```
|
|
356
298
|
|
|
357
299
|
## Tips
|
|
358
300
|
|
|
359
|
-
- **`
|
|
360
|
-
-
|
|
361
|
-
- **
|
|
362
|
-
- **Use chunked spawning for large batches.**
|
|
363
|
-
- **Each child has its own retry semantics.** Steps inside child workflows retry independently. The parent
|
|
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
|
-
- [`
|
|
370
|
-
- [`
|
|
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
|
|
@@ -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* `
|
|
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
|
|
276
|
+
async function getWorkflowStepId(): Promise<string | null> { // [!code highlight]
|
|
277
277
|
try {
|
|
278
278
|
const wf = await import("workflow");
|
|
279
|
-
const {
|
|
280
|
-
return
|
|
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
|
|
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
|
|
295
|
-
const idempotencyKey =
|
|
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
|
|
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/
|
|
334
|
-
- [`"use step"`](/docs/
|
|
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/
|
|
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/
|
|
145
|
-
- [`"use workflow"`](/docs/
|
|
146
|
-
- [`
|
|
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`)
|