workflow 5.0.0-beta.4 → 5.0.0-beta.41
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 +63 -20
- 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/internal/errors.d.ts +1 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +2 -2
- package/dist/nest-builder.d.ts +2 -0
- package/dist/nest-builder.d.ts.map +1 -0
- package/dist/nest-builder.js +2 -0
- package/dist/nest-vercel-builder.d.ts +2 -0
- package/dist/nest-vercel-builder.d.ts.map +1 -0
- package/dist/nest-vercel-builder.js +2 -0
- 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 +30 -25
- 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 +79 -0
- package/docs/api-reference/workflow/create-webhook.mdx +1 -0
- package/docs/api-reference/workflow/define-hook.mdx +26 -24
- package/docs/api-reference/workflow/fatal-error.mdx +29 -7
- package/docs/api-reference/workflow/fetch.mdx +8 -4
- package/docs/api-reference/workflow/index.mdx +3 -0
- package/docs/api-reference/workflow/set-attributes.mdx +61 -0
- package/docs/api-reference/workflow/sleep.mdx +1 -1
- 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 +9 -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 +60 -1
- package/docs/api-reference/workflow-api/resume-webhook.mdx +6 -4
- package/docs/api-reference/workflow-api/start.mdx +39 -6
- 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 +88 -0
- package/docs/api-reference/workflow-errors/meta.json +6 -0
- package/docs/api-reference/workflow-errors/precondition-failed-error.mdx +68 -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-globals.mdx +4 -1
- 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-runtime/world/analytics.mdx +138 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/index.mdx +10 -10
- package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/queue.mdx +8 -6
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +24 -7
- 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 +2 -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 +3 -1
- package/docs/changelog/lazy-event-creation.md +127 -0
- package/docs/changelog/meta.json +9 -1
- package/docs/changelog/resilient-resume.mdx +22 -0
- 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/comparisons/index.mdx +66 -0
- package/docs/comparisons/meta.json +11 -0
- package/docs/comparisons/workflow-sdk-vs-aws-agentcore.mdx +55 -0
- package/docs/comparisons/workflow-sdk-vs-aws-step-functions.mdx +111 -0
- package/docs/comparisons/workflow-sdk-vs-cloudflare-workflows.mdx +71 -0
- package/docs/comparisons/workflow-sdk-vs-inngest.mdx +102 -0
- package/docs/comparisons/workflow-sdk-vs-temporal.mdx +123 -0
- package/docs/comparisons/workflow-sdk-vs-trigger-dev.mdx +103 -0
- package/docs/configuration/build-and-diagnostics.mdx +70 -0
- package/docs/configuration/cli-and-web-ui.mdx +182 -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 +281 -0
- package/docs/configuration/worlds.mdx +283 -0
- package/docs/cookbook/advanced/child-workflows.mdx +203 -256
- package/docs/cookbook/advanced/meta.json +1 -1
- package/docs/cookbook/advanced/publishing-libraries.mdx +33 -24
- package/docs/cookbook/advanced/serializable-steps.mdx +12 -4
- package/docs/cookbook/advanced/upgrading-workflows.mdx +199 -0
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +34 -75
- package/docs/cookbook/agent-patterns/durable-agent.mdx +14 -142
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +11 -3
- package/docs/cookbook/common-patterns/batching.mdx +4 -0
- package/docs/cookbook/common-patterns/idempotency.mdx +40 -52
- package/docs/cookbook/common-patterns/rate-limiting.mdx +5 -1
- package/docs/cookbook/common-patterns/saga.mdx +6 -2
- package/docs/cookbook/common-patterns/scheduling.mdx +12 -0
- package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +4 -0
- package/docs/cookbook/common-patterns/timeouts.mdx +6 -1
- package/docs/cookbook/common-patterns/webhooks.mdx +4 -0
- package/docs/cookbook/common-patterns/workflow-composition.mdx +19 -16
- package/docs/cookbook/index.mdx +3 -3
- package/docs/cookbook/integrations/ai-sdk.mdx +64 -28
- package/docs/cookbook/integrations/chat-sdk.mdx +17 -0
- package/docs/cookbook/integrations/sandbox.mdx +17 -0
- package/docs/{deploying/index.mdx → deploying.mdx} +7 -7
- package/docs/errors/abort-signal-timeout-in-workflow.mdx +84 -0
- package/docs/errors/corrupted-event-log.mdx +11 -8
- package/docs/errors/deployment-mismatch.mdx +71 -0
- package/docs/errors/fetch-in-workflow.mdx +4 -0
- package/docs/errors/hook-conflict.mdx +60 -4
- package/docs/errors/index.mdx +1 -35
- package/docs/errors/node-js-module-in-workflow.mdx +4 -0
- package/docs/errors/replay-divergence.mdx +27 -0
- package/docs/errors/runtime-decryption-failed.mdx +77 -0
- package/docs/errors/serialization-failed.mdx +4 -0
- package/docs/errors/start-invalid-workflow-function.mdx +4 -0
- package/docs/errors/step-executed-multiple-times.mdx +23 -0
- package/docs/errors/step-not-registered.mdx +1 -1
- package/docs/errors/timeout-in-workflow.mdx +4 -0
- package/docs/errors/webhook-response-not-sent.mdx +4 -0
- 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 +243 -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 +79 -2
- package/docs/foundations/streaming.mdx +14 -23
- package/docs/foundations/versioning.mdx +263 -0
- package/docs/getting-started/astro.mdx +12 -2
- package/docs/getting-started/express.mdx +6 -2
- package/docs/getting-started/fastify.mdx +6 -2
- package/docs/getting-started/hono.mdx +6 -2
- package/docs/getting-started/index.mdx +16 -10
- package/docs/getting-started/meta.json +4 -1
- package/docs/getting-started/nestjs.mdx +77 -3
- package/docs/getting-started/next.mdx +12 -6
- package/docs/getting-started/nitro.mdx +28 -2
- package/docs/getting-started/nuxt.mdx +6 -2
- package/docs/getting-started/python.mdx +24 -18
- package/docs/getting-started/react-router/index.mdx +33 -0
- package/docs/getting-started/react-router/meta.json +5 -0
- package/docs/getting-started/react-router/v7.mdx +237 -0
- package/docs/getting-started/react-router/v8.mdx +232 -0
- package/docs/getting-started/sveltekit.mdx +12 -2
- package/docs/getting-started/tanstack-start.mdx +245 -0
- package/docs/getting-started/vite.mdx +6 -2
- package/docs/how-it-works/cancellation.mdx +287 -0
- package/docs/how-it-works/code-transform.mdx +21 -17
- package/docs/how-it-works/encryption.mdx +5 -5
- package/docs/how-it-works/event-sourcing.mdx +17 -9
- package/docs/how-it-works/framework-integrations.mdx +96 -337
- 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 +2 -2
- package/docs/observability/attributes.mdx +112 -0
- package/docs/observability/index.mdx +20 -1
- package/docs/observability/meta.json +1 -1
- package/docs/observability/tracing.mdx +124 -0
- package/docs/testing/index.mdx +2 -2
- package/package.json +17 -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
- package/docs/deploying/building-a-world.mdx +0 -251
- package/docs/deploying/meta.json +0 -4
- package/docs/deploying/world/local-world.mdx +0 -84
- package/docs/deploying/world/meta.json +0 -4
- package/docs/deploying/world/postgres-world.mdx +0 -222
- package/docs/deploying/world/vercel-world.mdx +0 -179
- package/docs/migration-guides/index.mdx +0 -34
- package/docs/migration-guides/meta.json +0 -9
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +0 -363
- package/docs/migration-guides/migrating-from-inngest.mdx +0 -314
- package/docs/migration-guides/migrating-from-temporal.mdx +0 -318
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +0 -337
|
@@ -1,10 +1,16 @@
|
|
|
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
|
|
|
10
|
+
<CopyPrompt
|
|
11
|
+
text="Refactor this workflow to use child workflows. Keep the parent as an exported `"use workflow"` function. Move independent units of durable work into separate exported child workflow functions. From the parent, call `start(childWorkflow, [args])` from `workflow/api` or the documented `startAndWait`/hook pattern where completion must resume the parent. Pass only serializable state to children. For fan-out, start children in parallel with `Promise.all` or bounded batches, collect run IDs, handle partial failures with `Promise.allSettled`, and use `getRun(runId)` when status, cancellation, streams, or return values are needed. Verify child start, completion, failure, and parent resume behavior."
|
|
12
|
+
/>
|
|
13
|
+
|
|
8
14
|
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
15
|
|
|
10
16
|
## When to use child workflows
|
|
@@ -18,20 +24,68 @@ Child workflows are the right choice when:
|
|
|
18
24
|
|
|
19
25
|
For simpler cases where steps share a single event log, use [direct await composition](/cookbook/common-patterns/workflow-composition#direct-await-flattening) instead.
|
|
20
26
|
|
|
21
|
-
## Basic pattern: spawn and
|
|
27
|
+
## Basic pattern: spawn and wait via hook
|
|
22
28
|
|
|
23
|
-
The
|
|
29
|
+
The recommended pattern has four parts:
|
|
24
30
|
|
|
25
|
-
1. A **
|
|
26
|
-
2. A **
|
|
27
|
-
3. A **
|
|
31
|
+
1. A **completion hook** the parent creates and awaits — zero compute while waiting
|
|
32
|
+
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`
|
|
33
|
+
3. A **`start()` call** that spawns the wrapped child with the hook token (directly from the workflow in v5)
|
|
34
|
+
4. A **`startAndWait()` helper** that ties the hook, spawn, and typed result together
|
|
28
35
|
|
|
29
36
|
```typescript
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
37
|
+
import { defineHook, getWorkflowMetadata } from "workflow";
|
|
38
|
+
import { start } from "workflow/api";
|
|
39
|
+
import { z } from "zod";
|
|
40
|
+
|
|
41
|
+
declare function fetchDocument(documentId: string): Promise<string>; // @setup
|
|
42
|
+
declare function analyzeContent(content: string): Promise<string>; // @setup
|
|
43
|
+
declare function generateSummary(analysis: string): Promise<string>; // @setup
|
|
32
44
|
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
const childCompletionHook = defineHook({
|
|
46
|
+
schema: z.discriminatedUnion("status", [
|
|
47
|
+
z.object({ status: z.literal("completed"), value: z.unknown() }),
|
|
48
|
+
z.object({ status: z.literal("failed"), error: z.string() }),
|
|
49
|
+
]),
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
function completionToken(parentRunId: string, key: string) {
|
|
53
|
+
return `child-completion:${parentRunId}:${key}`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function resumeParentCompletion(
|
|
57
|
+
token: string,
|
|
58
|
+
result:
|
|
59
|
+
| { status: "completed"; value: unknown }
|
|
60
|
+
| { status: "failed"; error: string }
|
|
61
|
+
) {
|
|
62
|
+
"use step";
|
|
63
|
+
await childCompletionHook.resume(token, result);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function withChildCompletionHook<TResult>(
|
|
67
|
+
runChild: () => Promise<TResult>,
|
|
68
|
+
completionTokenArg: string
|
|
69
|
+
) {
|
|
70
|
+
let result:
|
|
71
|
+
| { status: "completed"; value: TResult }
|
|
72
|
+
| { status: "failed"; error: string }
|
|
73
|
+
| undefined;
|
|
74
|
+
|
|
75
|
+
try {
|
|
76
|
+
const value = await runChild();
|
|
77
|
+
result = { status: "completed", value };
|
|
78
|
+
} catch (error) {
|
|
79
|
+
result = {
|
|
80
|
+
status: "failed",
|
|
81
|
+
error: error instanceof Error ? error.message : String(error),
|
|
82
|
+
};
|
|
83
|
+
} finally {
|
|
84
|
+
if (result) {
|
|
85
|
+
await resumeParentCompletion(completionTokenArg, result);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
35
89
|
|
|
36
90
|
// Child workflow -- processes a single document
|
|
37
91
|
export async function processDocument(documentId: string) {
|
|
@@ -44,163 +98,113 @@ export async function processDocument(documentId: string) {
|
|
|
44
98
|
return { documentId, summary };
|
|
45
99
|
}
|
|
46
100
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
101
|
+
// Spawnable wrapper -- explicit export so `start()` can register it
|
|
102
|
+
export async function processDocumentWithCompletion(
|
|
103
|
+
documentId: string,
|
|
104
|
+
completionTokenArg: string
|
|
105
|
+
) {
|
|
106
|
+
"use workflow";
|
|
52
107
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
108
|
+
await withChildCompletionHook(
|
|
109
|
+
() => processDocument(documentId),
|
|
110
|
+
completionTokenArg
|
|
111
|
+
);
|
|
57
112
|
}
|
|
58
113
|
|
|
59
|
-
async function
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
114
|
+
async function startAndWait<TResult>(
|
|
115
|
+
key: string,
|
|
116
|
+
startChild: (completionTokenArg: string) => Promise<void>
|
|
117
|
+
): Promise<TResult> {
|
|
118
|
+
const { workflowRunId } = getWorkflowMetadata();
|
|
119
|
+
const token = completionToken(workflowRunId, key);
|
|
120
|
+
const hook = childCompletionHook.create({ token }); // [!code highlight]
|
|
121
|
+
|
|
122
|
+
await startChild(token);
|
|
123
|
+
|
|
124
|
+
const completion = await hook; // [!code highlight]
|
|
125
|
+
if (completion.status === "failed") {
|
|
126
|
+
throw new Error(completion.error);
|
|
127
|
+
}
|
|
128
|
+
return completion.value as TResult;
|
|
63
129
|
}
|
|
64
130
|
|
|
65
131
|
// Parent workflow -- orchestrates document processing
|
|
66
132
|
export async function processDocumentBatch(documentIds: string[]) {
|
|
67
133
|
"use workflow";
|
|
68
134
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
const results = await collectResults(runIds);
|
|
135
|
+
const results = await Promise.all(
|
|
136
|
+
documentIds.map((documentId) =>
|
|
137
|
+
startAndWait<{ documentId: string; summary: string }>(documentId, (token) =>
|
|
138
|
+
start(processDocumentWithCompletion, [documentId, token]).then(() => undefined) // [!code highlight]
|
|
139
|
+
)
|
|
140
|
+
)
|
|
141
|
+
);
|
|
77
142
|
|
|
78
143
|
return { processed: results.length, results };
|
|
79
144
|
}
|
|
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
145
|
```
|
|
94
146
|
|
|
95
|
-
###
|
|
147
|
+
### Why hooks instead of polling?
|
|
96
148
|
|
|
97
|
-
|
|
149
|
+
Polling with `getRun().status` in a `sleep()` loop works, but hook resume is preferable because:
|
|
98
150
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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
|
-
}
|
|
151
|
+
- **Zero compute while waiting** — the parent suspends on the hook instead of waking every poll interval
|
|
152
|
+
- **Immediate wake-up** — the parent resumes as soon as the child finishes, not on the next poll tick
|
|
153
|
+
- **Typed payloads** — the child sends `{ status, value | error }` directly; no separate `returnValue` fetch step
|
|
154
|
+
- **No worker-pool pressure** — `Run#returnValue` polling inside steps can hold worker slots while waiting for children (see [Eager Processing](/docs/changelog/eager-processing))
|
|
120
155
|
|
|
121
|
-
|
|
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
|
-
```
|
|
156
|
+
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
157
|
|
|
164
158
|
## Fan-out pattern: chunked spawning
|
|
165
159
|
|
|
166
|
-
When spawning hundreds of children, batch the `start()` calls to avoid overwhelming the system.
|
|
160
|
+
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
161
|
|
|
168
162
|
```typescript
|
|
169
163
|
import { start } from "workflow/api";
|
|
170
164
|
|
|
171
|
-
declare function
|
|
165
|
+
declare function startAndWait<TResult>(
|
|
166
|
+
key: string,
|
|
167
|
+
startChild: (completionTokenArg: string) => Promise<void>
|
|
168
|
+
): Promise<TResult>; // @setup
|
|
172
169
|
|
|
173
170
|
const CHUNK_SIZE = 10;
|
|
174
171
|
|
|
175
|
-
export async function largeReportBatch(
|
|
172
|
+
export async function largeReportBatch(
|
|
173
|
+
reportConfigs: Array<{ id: string; query: string }>
|
|
174
|
+
) {
|
|
176
175
|
"use workflow";
|
|
177
176
|
|
|
178
|
-
|
|
179
|
-
const allRunIds: string[] = [];
|
|
177
|
+
const results = [];
|
|
180
178
|
for (let i = 0; i < reportConfigs.length; i += CHUNK_SIZE) {
|
|
181
179
|
const chunk = reportConfigs.slice(i, i + CHUNK_SIZE);
|
|
182
|
-
const
|
|
183
|
-
|
|
180
|
+
const chunkResults = await Promise.all(
|
|
181
|
+
chunk.map((config) =>
|
|
182
|
+
startAndWait<{ reportId: string; formatted: string }>(config.id, (token) =>
|
|
183
|
+
start(generateReportWithCompletion, [
|
|
184
|
+
config.id,
|
|
185
|
+
config.query,
|
|
186
|
+
token,
|
|
187
|
+
]).then(() => undefined)
|
|
188
|
+
)
|
|
189
|
+
)
|
|
190
|
+
);
|
|
191
|
+
results.push(...chunkResults);
|
|
184
192
|
}
|
|
185
193
|
|
|
186
|
-
// Poll until all complete
|
|
187
|
-
await pollUntilComplete(allRunIds);
|
|
188
|
-
|
|
189
|
-
const results = await collectReportResults(allRunIds);
|
|
190
194
|
return { total: results.length, results };
|
|
191
195
|
}
|
|
192
196
|
|
|
193
|
-
async function
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
+
async function generateReportWithCompletion(
|
|
198
|
+
reportId: string,
|
|
199
|
+
query: string,
|
|
200
|
+
completionTokenArg: string
|
|
201
|
+
) {
|
|
202
|
+
"use workflow";
|
|
197
203
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
}
|
|
203
|
-
return runIds;
|
|
204
|
+
await withChildCompletionHook(
|
|
205
|
+
() => generateReport(reportId, query),
|
|
206
|
+
completionTokenArg
|
|
207
|
+
);
|
|
204
208
|
}
|
|
205
209
|
|
|
206
210
|
async function generateReport(reportId: string, query: string) {
|
|
@@ -213,160 +217,103 @@ async function generateReport(reportId: string, query: string) {
|
|
|
213
217
|
|
|
214
218
|
declare function queryDatabase(reportId: string, query: string): Promise<string>; // @setup
|
|
215
219
|
declare function formatReport(reportId: string, data: string): Promise<string>; // @setup
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
): Promise<
|
|
220
|
+
declare function withChildCompletionHook<TResult>(
|
|
221
|
+
runChild: () => Promise<TResult>,
|
|
222
|
+
completionTokenArg: string
|
|
223
|
+
): Promise<void>; // @setup
|
|
220
224
|
```
|
|
221
225
|
|
|
222
226
|
## Error handling
|
|
223
227
|
|
|
224
228
|
### Tolerating partial failures
|
|
225
229
|
|
|
226
|
-
|
|
230
|
+
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
231
|
|
|
228
232
|
```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
|
-
}
|
|
233
|
+
import { start } from "workflow/api";
|
|
273
234
|
|
|
274
|
-
|
|
275
|
-
|
|
235
|
+
declare function startAndWait<TResult>(
|
|
236
|
+
key: string,
|
|
237
|
+
startChild: (completionTokenArg: string) => Promise<void>
|
|
238
|
+
): Promise<TResult>; // @setup
|
|
239
|
+
declare function processDocumentWithCompletion(
|
|
240
|
+
documentId: string,
|
|
241
|
+
completionTokenArg: string
|
|
242
|
+
): Promise<void>; // @setup
|
|
276
243
|
|
|
277
|
-
async function
|
|
278
|
-
|
|
279
|
-
): Promise<Array<{ runId: string; status: string }>> {
|
|
280
|
-
"use step";
|
|
244
|
+
export async function processDocumentBatchTolerant(documentIds: string[]) {
|
|
245
|
+
"use workflow";
|
|
281
246
|
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
247
|
+
const settled = await Promise.allSettled(
|
|
248
|
+
documentIds.map((documentId) =>
|
|
249
|
+
startAndWait<{ documentId: string; summary: string }>(documentId, (token) =>
|
|
250
|
+
start(processDocumentWithCompletion, [documentId, token]).then(
|
|
251
|
+
() => undefined
|
|
252
|
+
)
|
|
253
|
+
)
|
|
254
|
+
)
|
|
255
|
+
);
|
|
256
|
+
|
|
257
|
+
const results = settled
|
|
258
|
+
.filter(
|
|
259
|
+
(entry): entry is PromiseFulfilledResult<{ documentId: string; summary: string }> =>
|
|
260
|
+
entry.status === "fulfilled"
|
|
261
|
+
)
|
|
262
|
+
.map((entry) => entry.value);
|
|
263
|
+
|
|
264
|
+
const failed = settled.filter((entry) => entry.status === "rejected").length;
|
|
265
|
+
|
|
266
|
+
return { processed: results.length, failed, results };
|
|
289
267
|
}
|
|
290
268
|
```
|
|
291
269
|
|
|
292
270
|
### Retrying failed children
|
|
293
271
|
|
|
294
|
-
When a child fails,
|
|
272
|
+
When a child fails, spawn a replacement with a fresh hook token. Track restart counts to prevent infinite retry loops.
|
|
295
273
|
|
|
296
274
|
```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
|
-
}
|
|
275
|
+
declare function startAndWait<TResult>(
|
|
276
|
+
key: string,
|
|
277
|
+
startChild: (completionTokenArg: string) => Promise<void>
|
|
278
|
+
): Promise<TResult>; // @setup
|
|
279
|
+
declare function spawnProcessDocument(
|
|
280
|
+
documentId: string,
|
|
281
|
+
completionTokenArg: string
|
|
282
|
+
): Promise<void>; // @setup
|
|
283
|
+
|
|
284
|
+
async function startAndWaitWithRetries(
|
|
285
|
+
documentId: string,
|
|
286
|
+
maxRestarts: number
|
|
287
|
+
): Promise<{ documentId: string; summary: string }> {
|
|
288
|
+
for (let attempt = 0; attempt <= maxRestarts; attempt++) {
|
|
289
|
+
try {
|
|
290
|
+
return await startAndWait<{ documentId: string; summary: string }>(
|
|
291
|
+
`${documentId}:${attempt}`,
|
|
292
|
+
(token) => spawnProcessDocument(documentId, token)
|
|
293
|
+
);
|
|
294
|
+
} catch (error) {
|
|
295
|
+
if (attempt === maxRestarts) throw error;
|
|
345
296
|
}
|
|
346
|
-
|
|
347
|
-
if (activeRuns.size === 0) return;
|
|
348
|
-
|
|
349
|
-
iteration += 1;
|
|
350
|
-
await sleep(POLL_INTERVAL);
|
|
351
297
|
}
|
|
352
298
|
|
|
353
|
-
throw new Error("
|
|
299
|
+
throw new Error("unreachable");
|
|
354
300
|
}
|
|
355
301
|
```
|
|
356
302
|
|
|
357
303
|
## Tips
|
|
358
304
|
|
|
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.
|
|
305
|
+
- **`defineHook().resume()` must be called from a step.** The wrapped child's `finally` block calls a step that resumes the parent hook.
|
|
306
|
+
- **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()`.
|
|
307
|
+
- **Use stable hook keys** — document ID, job ID, or index — so parallel children inside one parent run don't collide on tokens.
|
|
308
|
+
- **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.
|
|
309
|
+
- **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.
|
|
310
|
+
- **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
311
|
|
|
366
312
|
## Key APIs
|
|
367
313
|
|
|
368
314
|
- [`start()`](/docs/api-reference/workflow-api/start) -- spawn a new workflow run and get its run ID
|
|
369
|
-
- [`
|
|
370
|
-
- [`
|
|
315
|
+
- [`defineHook()`](/docs/api-reference/workflow/define-hook) -- typed hook for parent/child completion handshakes
|
|
316
|
+
- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) -- resume a waiting parent from a step (called by the child wrapper)
|
|
317
|
+
- [`getWorkflowMetadata()`](/docs/api-reference/workflow/get-workflow-metadata) -- read the parent run ID for deterministic hook tokens
|
|
371
318
|
- [`"use workflow"`](/docs/foundations/workflows-and-steps) -- marks the orchestrator function
|
|
372
319
|
- [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions with full Node.js access
|