workflow 5.0.0-beta.5 → 5.0.0-beta.7
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/observability.d.ts +1 -1
- package/dist/observability.js +2 -2
- package/docs/ai/index.mdx +6 -5
- package/docs/api-reference/workflow-api/start.mdx +5 -4
- package/docs/cookbook/advanced/child-workflows.mdx +13 -24
- package/docs/cookbook/agent-patterns/durable-agent.mdx +4 -0
- package/docs/cookbook/common-patterns/scheduling.mdx +4 -0
- package/docs/cookbook/common-patterns/workflow-composition.mdx +7 -14
- package/docs/cookbook/integrations/ai-sdk.mdx +4 -0
- package/docs/cookbook/integrations/chat-sdk.mdx +4 -0
- package/docs/cookbook/integrations/sandbox.mdx +4 -0
- package/docs/deploying/building-a-world.mdx +1 -1
- package/docs/deploying/world/vercel-world.mdx +2 -0
- package/docs/errors/hook-conflict.mdx +56 -4
- package/docs/foundations/index.mdx +3 -0
- package/docs/foundations/meta.json +2 -1
- package/docs/foundations/starting-workflows.mdx +5 -1
- package/docs/foundations/versioning.mdx +263 -0
- package/docs/getting-started/next.mdx +5 -3
- package/docs/how-it-works/code-transform.mdx +2 -2
- package/docs/how-it-works/event-sourcing.mdx +2 -2
- 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 +5 -10
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +8 -17
- package/package.json +11 -11
package/dist/observability.d.ts
CHANGED
package/dist/observability.js
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
*
|
|
7
7
|
* @example
|
|
8
8
|
* ```ts
|
|
9
|
-
* import { getWorld } from 'workflow/
|
|
9
|
+
* import { getWorld } from 'workflow/runtime';
|
|
10
10
|
* import { hydrateResourceIO, observabilityRevivers } from 'workflow/observability';
|
|
11
11
|
*
|
|
12
12
|
* const world = await getWorld();
|
|
@@ -17,4 +17,4 @@
|
|
|
17
17
|
*/
|
|
18
18
|
export { hydrateData, hydrateResourceIO, observabilityRevivers, } from '@workflow/core/serialization-format';
|
|
19
19
|
export { parseClassName, parseStepName, parseWorkflowName, } from '@workflow/utils';
|
|
20
|
-
//# sourceMappingURL=data:application/json;base64,
|
|
20
|
+
//# sourceMappingURL=data:application/json;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoib2JzZXJ2YWJpbGl0eS5qcyIsInNvdXJjZVJvb3QiOiIiLCJzb3VyY2VzIjpbIi4uL3NyYy9vYnNlcnZhYmlsaXR5LnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiJBQUFBOzs7Ozs7Ozs7Ozs7Ozs7O0dBZ0JHO0FBQ0gsT0FBTyxFQUNMLFdBQVcsRUFDWCxpQkFBaUIsRUFDakIscUJBQXFCLEdBRXRCLE1BQU0scUNBQXFDLENBQUM7QUFFN0MsT0FBTyxFQUNMLGNBQWMsRUFDZCxhQUFhLEVBQ2IsaUJBQWlCLEdBQ2xCLE1BQU0saUJBQWlCLENBQUMiLCJzb3VyY2VzQ29udGVudCI6WyIvKipcbiAqIE9ic2VydmFiaWxpdHkgdXRpbGl0aWVzIGZvciBoeWRyYXRpbmcgc2VyaWFsaXplZCB3b3JrZmxvdyBkYXRhLlxuICpcbiAqIFVzZSB0aGVzZSB3aGVuIGluc3BlY3Rpbmcgd29ya2Zsb3cgc3RlcCBJL08sIHJ1biBpbnB1dHMvb3V0cHV0cyxcbiAqIG9yIGV2ZW50IGRhdGEgZnJvbSB0aGUgV29ya2Zsb3cgU0RLJ3Mgd29ybGQgQVBJcy5cbiAqXG4gKiBAZXhhbXBsZVxuICogYGBgdHNcbiAqIGltcG9ydCB7IGdldFdvcmxkIH0gZnJvbSAnd29ya2Zsb3cvcnVudGltZSc7XG4gKiBpbXBvcnQgeyBoeWRyYXRlUmVzb3VyY2VJTywgb2JzZXJ2YWJpbGl0eVJldml2ZXJzIH0gZnJvbSAnd29ya2Zsb3cvb2JzZXJ2YWJpbGl0eSc7XG4gKlxuICogY29uc3Qgd29ybGQgPSBhd2FpdCBnZXRXb3JsZCgpO1xuICogY29uc3Qgc3RlcCA9IGF3YWl0IHdvcmxkLnN0ZXBzLmdldChydW5JZCwgc3RlcElkLCB7IHJlc29sdmVEYXRhOiAnYWxsJyB9KTtcbiAqIGNvbnN0IGh5ZHJhdGVkID0gaHlkcmF0ZVJlc291cmNlSU8oc3RlcCwgb2JzZXJ2YWJpbGl0eVJldml2ZXJzKTtcbiAqIC8vIGh5ZHJhdGVkLmlucHV0IGFuZCBoeWRyYXRlZC5vdXRwdXQgYXJlIG5vdyBwbGFpbiBKUyBvYmplY3RzXG4gKiBgYGBcbiAqL1xuZXhwb3J0IHtcbiAgaHlkcmF0ZURhdGEsXG4gIGh5ZHJhdGVSZXNvdXJjZUlPLFxuICBvYnNlcnZhYmlsaXR5UmV2aXZlcnMsXG4gIHR5cGUgUmV2aXZlcnMsXG59IGZyb20gJ0B3b3JrZmxvdy9jb3JlL3NlcmlhbGl6YXRpb24tZm9ybWF0JztcblxuZXhwb3J0IHtcbiAgcGFyc2VDbGFzc05hbWUsXG4gIHBhcnNlU3RlcE5hbWUsXG4gIHBhcnNlV29ya2Zsb3dOYW1lLFxufSBmcm9tICdAd29ya2Zsb3cvdXRpbHMnO1xuIl19
|
package/docs/ai/index.mdx
CHANGED
|
@@ -120,16 +120,17 @@ The core code that makes all of this happen is quite simple. Here's a breakdown
|
|
|
120
120
|
|
|
121
121
|
<Tab value="API Route">
|
|
122
122
|
|
|
123
|
-
Our API route makes a simple call to [AI SDK's `
|
|
123
|
+
Our API route makes a simple call to [AI SDK's `ToolLoopAgent` class](https://ai-sdk.dev/docs/agents/overview), which encapsulates the LLM call, tool execution loop, and stopping conditions on top of [AI SDK's `streamText` function](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text#streamtext). This is also where we pass tools to the agent.
|
|
124
124
|
|
|
125
125
|
```typescript title="app/api/chat/route.ts" lineNumbers
|
|
126
|
-
import {
|
|
127
|
-
import type {
|
|
126
|
+
import { ToolLoopAgent } from "ai";
|
|
127
|
+
import type { UIMessage } from "ai";
|
|
128
|
+
import { convertToModelMessages, createUIMessageStreamResponse } from "ai";
|
|
128
129
|
|
|
129
130
|
export async function POST(req: Request) {
|
|
130
131
|
const { messages }: { messages: UIMessage[] } = await req.json();
|
|
131
|
-
const agent = new
|
|
132
|
-
model:
|
|
132
|
+
const agent = new ToolLoopAgent({ // [!code highlight]
|
|
133
|
+
model: "bedrock/claude-4-5-haiku-20251001-v1",
|
|
133
134
|
instructions: FLIGHT_ASSISTANT_PROMPT,
|
|
134
135
|
tools: flightBookingTools,
|
|
135
136
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
title: start
|
|
3
3
|
description: Start and enqueue a new workflow run.
|
|
4
4
|
type: reference
|
|
5
|
-
summary: Use start to programmatically enqueue a new workflow run
|
|
5
|
+
summary: Use start to programmatically enqueue a new workflow run.
|
|
6
6
|
prerequisites:
|
|
7
7
|
- /docs/foundations/starting-workflows
|
|
8
8
|
---
|
|
@@ -50,7 +50,8 @@ Learn more about [`WorkflowReadableStreamOptions`](/docs/api-reference/workflow-
|
|
|
50
50
|
|
|
51
51
|
## Good to Know
|
|
52
52
|
|
|
53
|
-
* The `start()` function is used in runtime
|
|
53
|
+
* The `start()` function is used in runtime contexts to programmatically trigger workflow executions.
|
|
54
|
+
* In v5, `start()` can also be called directly from a workflow function to spawn a child run or continue work in a new run. See [Workflow Composition](/cookbook/common-patterns/workflow-composition) and [Versioning](/docs/foundations/versioning).
|
|
54
55
|
* This is different from calling workflow functions directly, which is the typical pattern in Next.js applications.
|
|
55
56
|
* The function returns immediately after enqueuing the workflow - it doesn't wait for the workflow to complete.
|
|
56
57
|
* All arguments must be [serializable](/docs/foundations/serialization).
|
|
@@ -84,7 +85,7 @@ const run = await start(myWorkflow, ["arg1", "arg2"], { // [!code highlight]
|
|
|
84
85
|
|
|
85
86
|
### Using `deploymentId: "latest"`
|
|
86
87
|
|
|
87
|
-
Set `deploymentId` to `"latest"` to automatically resolve the most recent deployment for the current environment. This is useful when you want to ensure a workflow run targets the latest deployed version of your application rather than the deployment that initiated the call.
|
|
88
|
+
Set `deploymentId` to `"latest"` to automatically resolve the most recent deployment for the current environment. This is useful when you want to ensure a workflow run targets the latest deployed version of your application rather than the deployment that initiated the call. For when to use this and how it fits with default run pinning, see [Versioning](/docs/foundations/versioning).
|
|
88
89
|
|
|
89
90
|
```typescript
|
|
90
91
|
import { start } from "workflow/api";
|
|
@@ -96,7 +97,7 @@ const run = await start(myWorkflow, ["arg1", "arg2"], { // [!code highlight]
|
|
|
96
97
|
```
|
|
97
98
|
|
|
98
99
|
<Callout type="info">
|
|
99
|
-
The `deploymentId` option is currently a Vercel-specific feature.
|
|
100
|
+
The `deploymentId` option is currently a Vercel-specific feature. Other Worlds may implement this option differently to match their own deployment runtimes, and the World spec may rename it from `deploymentId` to `version` in a future SDK version. On Vercel, `"latest"` resolves to the most recent deployment matching your current environment — the same production target for production deployments, or the same git branch for preview deployments.
|
|
100
101
|
</Callout>
|
|
101
102
|
|
|
102
103
|
<Callout type="warn">
|
|
@@ -22,7 +22,7 @@ For simpler cases where steps share a single event log, use [direct await compos
|
|
|
22
22
|
|
|
23
23
|
The core pattern has three parts:
|
|
24
24
|
|
|
25
|
-
1. A
|
|
25
|
+
1. A parent workflow that calls `start()` to spawn child workflows and records their run IDs
|
|
26
26
|
2. A **polling loop** in the parent workflow that checks child status with `getRun()`
|
|
27
27
|
3. A **step** that retrieves the child's return value once it completes
|
|
28
28
|
|
|
@@ -67,7 +67,11 @@ export async function processDocumentBatch(documentIds: string[]) {
|
|
|
67
67
|
"use workflow";
|
|
68
68
|
|
|
69
69
|
// Spawn a child workflow for each document
|
|
70
|
-
const runIds =
|
|
70
|
+
const runIds: string[] = [];
|
|
71
|
+
for (const docId of documentIds) {
|
|
72
|
+
const run = await start(processDocument, [docId]); // [!code highlight]
|
|
73
|
+
runIds.push(run.runId);
|
|
74
|
+
}
|
|
71
75
|
|
|
72
76
|
// Poll until all children complete
|
|
73
77
|
await pollUntilComplete(runIds);
|
|
@@ -77,19 +81,6 @@ export async function processDocumentBatch(documentIds: string[]) {
|
|
|
77
81
|
|
|
78
82
|
return { processed: results.length, results };
|
|
79
83
|
}
|
|
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
84
|
```
|
|
94
85
|
|
|
95
86
|
### Polling loop
|
|
@@ -163,7 +154,7 @@ async function collectResults(
|
|
|
163
154
|
|
|
164
155
|
## Fan-out pattern: chunked spawning
|
|
165
156
|
|
|
166
|
-
When spawning hundreds of children, batch the `start()` calls to avoid overwhelming the system.
|
|
157
|
+
When spawning hundreds of children, batch the `start()` calls to avoid overwhelming the system. Start one chunk at a time from the parent workflow.
|
|
167
158
|
|
|
168
159
|
```typescript
|
|
169
160
|
import { start } from "workflow/api";
|
|
@@ -179,7 +170,7 @@ export async function largeReportBatch(reportConfigs: Array<{ id: string; query:
|
|
|
179
170
|
const allRunIds: string[] = [];
|
|
180
171
|
for (let i = 0; i < reportConfigs.length; i += CHUNK_SIZE) {
|
|
181
172
|
const chunk = reportConfigs.slice(i, i + CHUNK_SIZE);
|
|
182
|
-
const runIds = await
|
|
173
|
+
const runIds = await startReportChunk(chunk); // [!code highlight]
|
|
183
174
|
allRunIds.push(...runIds);
|
|
184
175
|
}
|
|
185
176
|
|
|
@@ -190,14 +181,12 @@ export async function largeReportBatch(reportConfigs: Array<{ id: string; query:
|
|
|
190
181
|
return { total: results.length, results };
|
|
191
182
|
}
|
|
192
183
|
|
|
193
|
-
async function
|
|
184
|
+
async function startReportChunk(
|
|
194
185
|
configs: Array<{ id: string; query: string }>
|
|
195
186
|
): Promise<string[]> {
|
|
196
|
-
"use step";
|
|
197
|
-
|
|
198
187
|
const runIds: string[] = [];
|
|
199
188
|
for (const config of configs) {
|
|
200
|
-
const run = await start(generateReport, [config.id, config.query]);
|
|
189
|
+
const run = await start(generateReport, [config.id, config.query]); // [!code highlight]
|
|
201
190
|
runIds.push(run.runId);
|
|
202
191
|
}
|
|
203
192
|
return runIds;
|
|
@@ -356,12 +345,12 @@ async function pollWithRetries(
|
|
|
356
345
|
|
|
357
346
|
## Tips
|
|
358
347
|
|
|
359
|
-
- **`start()`
|
|
348
|
+
- **`start()` can be called directly from a workflow function in v5.** It records a step-backed boundary in the parent event log and returns a serializable `Run`.
|
|
360
349
|
- **`getRun()` must also be called from a step.** The polling loop lives in the workflow, but the actual status check is a step.
|
|
361
350
|
- **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.**
|
|
351
|
+
- **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.
|
|
363
352
|
- **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.
|
|
353
|
+
- **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
354
|
|
|
366
355
|
## Key APIs
|
|
367
356
|
|
|
@@ -14,6 +14,10 @@ Use this pattern to make any AI SDK agent durable. The agent becomes a workflow,
|
|
|
14
14
|
- Long-running agent sessions where losing progress is unacceptable
|
|
15
15
|
- Agents that need per-step observability in the workflow event log
|
|
16
16
|
|
|
17
|
+
<Callout type="info">
|
|
18
|
+
A durable agent run stays on the deployment that started it. For multi-turn agents that should pick up newer code between turns, see [Versioning](/docs/foundations/versioning) for patterns that start the next turn or next session run with `deploymentId: "latest"`.
|
|
19
|
+
</Callout>
|
|
20
|
+
|
|
17
21
|
## Pattern
|
|
18
22
|
|
|
19
23
|
Replace `Agent` with `DurableAgent`, wrap the function in `"use workflow"`, mark each tool with `"use step"`, and stream output through `getWritable()`.
|
|
@@ -7,6 +7,10 @@ summary: Schedule future actions with durable sleep that survives cold starts, a
|
|
|
7
7
|
|
|
8
8
|
Workflow's `sleep()` is durable — it survives cold starts, restarts, and deployments. Combined with `defineHook()` and `Promise.race()`, it becomes the foundation for interruptible scheduled workflows like drip campaigns, reminders, and timed sequences.
|
|
9
9
|
|
|
10
|
+
<Callout type="info">
|
|
11
|
+
Scheduled workflows are still pinned to the deployment that started them. If you are building recurring or indefinitely running schedules that should adopt newer code over time, see [Versioning](/docs/foundations/versioning) for the explicit `deploymentId: "latest"` continuation pattern.
|
|
12
|
+
</Callout>
|
|
13
|
+
|
|
10
14
|
## When to use this
|
|
11
15
|
|
|
12
16
|
- Sending emails on a schedule (drip campaigns, onboarding sequences, reminders)
|
|
@@ -53,7 +53,7 @@ The parent waits for the child to finish before continuing. Both functions share
|
|
|
53
53
|
|
|
54
54
|
### Background spawn via `start()`
|
|
55
55
|
|
|
56
|
-
To run a child workflow independently without blocking the parent, call [`start()`](/docs/api-reference/workflow-api/start) from
|
|
56
|
+
To run a child workflow independently without blocking the parent, call [`start()`](/docs/api-reference/workflow-api/start) from the parent workflow. This launches the child as a separate workflow run with its own `runId`.
|
|
57
57
|
|
|
58
58
|
```typescript lineNumbers
|
|
59
59
|
import { start } from "workflow/api";
|
|
@@ -62,37 +62,30 @@ declare function generateReport(reportId: string): Promise<void>; // @setup
|
|
|
62
62
|
declare function fulfillOrder(orderId: string): Promise<{ id: string }>; // @setup
|
|
63
63
|
declare function sendConfirmation(orderId: string): Promise<void>; // @setup
|
|
64
64
|
|
|
65
|
-
async function triggerReportGeneration(reportId: string) {
|
|
66
|
-
"use step"; // [!code highlight]
|
|
67
|
-
|
|
68
|
-
const run = await start(generateReport, [reportId]); // [!code highlight]
|
|
69
|
-
return run.runId;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
65
|
export async function processOrder(orderId: string) {
|
|
73
66
|
"use workflow";
|
|
74
67
|
|
|
75
68
|
const order = await fulfillOrder(orderId);
|
|
76
69
|
|
|
77
|
-
const
|
|
70
|
+
const reportRun = await start(generateReport, [orderId]); // [!code highlight]
|
|
78
71
|
|
|
79
72
|
await sendConfirmation(orderId);
|
|
80
73
|
|
|
81
|
-
return { orderId, reportRunId };
|
|
74
|
+
return { orderId, reportRunId: reportRun.runId };
|
|
82
75
|
}
|
|
83
76
|
```
|
|
84
77
|
|
|
85
78
|
The parent continues immediately after `start()` returns. The child runs independently and can be monitored separately using the returned `runId` (e.g., via [`getRun()`](/docs/api-reference/workflow-api/get-run)).
|
|
86
79
|
|
|
87
80
|
<Callout type="info">
|
|
88
|
-
If you want the child workflow to run on the latest deployment rather than the current one, pass [`deploymentId: "latest"`](/docs/api-reference/workflow-api/start#using-deploymentid-latest) in the `start()` options. This is currently a Vercel-specific feature. Be aware that the child workflow's function name, file path, argument types, and return type must remain compatible across deployments — renaming the function or changing its location will change the workflow ID, and modifying expected inputs or outputs can cause serialization failures.
|
|
81
|
+
If you want the child workflow to run on the latest deployment rather than the current one, pass [`deploymentId: "latest"`](/docs/api-reference/workflow-api/start#using-deploymentid-latest) in the `start()` options. See [Versioning](/docs/foundations/versioning) for the full model. This is currently a Vercel-specific feature, and other Worlds may map the concept to their own deployment runtimes. Be aware that the child workflow's function name, file path, argument types, and return type must remain compatible across deployments — renaming the function or changing its location will change the workflow ID, and modifying expected inputs or outputs can cause serialization failures.
|
|
89
82
|
</Callout>
|
|
90
83
|
|
|
91
84
|
## How it works
|
|
92
85
|
|
|
93
86
|
1. **Direct await flattens.** When a workflow function awaits another workflow function, the child's `"use workflow"` directive is treated as inline — the child's steps emit into the parent's event log and share the parent's run ID.
|
|
94
87
|
2. **`start()` mints a new run.** The child gets its own `runId`, its own event log, and its own retry boundary. The parent only sees the `runId` returned by `start()`.
|
|
95
|
-
3. **`start()`
|
|
88
|
+
3. **`start()` can run inside workflows.** In v5, `start()` is step-backed, so it can be called directly from a workflow function and still records a deterministic step boundary in the event log.
|
|
96
89
|
|
|
97
90
|
## Choosing between the two modes
|
|
98
91
|
|
|
@@ -106,9 +99,9 @@ If you want the child workflow to run on the latest deployment rather than the c
|
|
|
106
99
|
|
|
107
100
|
## Adapting to your use case
|
|
108
101
|
|
|
109
|
-
- **Spawn many children at once** — call `start()` in a loop
|
|
102
|
+
- **Spawn many children at once** — call `start()` in a loop from the workflow. For more advanced fan-out (chunking, polling, partial-failure handling), graduate to the [Child Workflows](/cookbook/advanced/child-workflows) recipe.
|
|
110
103
|
- **Wait for a background child to finish** — combine `start()` with `getRun()` polling. The [Child Workflows](/cookbook/advanced/child-workflows) page covers the full polling loop.
|
|
111
|
-
- **Pass results back from background children** —
|
|
104
|
+
- **Pass results back from background children** — `start()` returns the `runId`; later, a poll step uses `getRun(runId).returnValue` to fetch the final result.
|
|
112
105
|
|
|
113
106
|
## Key APIs
|
|
114
107
|
|
|
@@ -32,6 +32,10 @@ Use [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text)
|
|
|
32
32
|
|
|
33
33
|
One workflow run = one full conversation. The workflow suspends between turns on a hook and resumes when the next user message arrives. Conversation state, tool history, and intermediate computation all live inside the run.
|
|
34
34
|
|
|
35
|
+
<Callout type="info">
|
|
36
|
+
Because the conversation is one workflow run, it stays on the deployment that started it. If each turn should run on the latest deployment while preserving selected state or streams, see [Versioning](/docs/foundations/versioning) for the child-run continuation pattern.
|
|
37
|
+
</Callout>
|
|
38
|
+
|
|
35
39
|
<Tabs items={['Workflow', 'API Route', 'Client']}>
|
|
36
40
|
|
|
37
41
|
<Tab value="Workflow">
|
|
@@ -20,6 +20,10 @@ Workflow SDK complements it by making bot **sessions** durable. Each conversatio
|
|
|
20
20
|
- Survives deploys, cold starts, and crashes — the session picks up from the last step on replay
|
|
21
21
|
- Receives follow-up messages via hooks, so the bot stays responsive while the workflow is still running
|
|
22
22
|
|
|
23
|
+
<Callout type="info">
|
|
24
|
+
One thread mapped to one workflow run also means the thread stays on the deployment that started it. For channels where each message should use newer code, see [Versioning](/docs/foundations/versioning) for explicit child-run and handoff patterns using `deploymentId: "latest"`.
|
|
25
|
+
</Callout>
|
|
26
|
+
|
|
23
27
|
The rest of this page covers the integration pattern. For a full Slack + Next.js + Redis walkthrough, see the [Durable chat sessions guide](https://chat-sdk.dev/docs/guides/durable-chat-sessions-nextjs) on chat-sdk.dev.
|
|
24
28
|
|
|
25
29
|
## How It Fits Together
|
|
@@ -21,6 +21,10 @@ A sandbox alone gets you an isolated VM. A workflow around it gets you a **durab
|
|
|
21
21
|
- **Beyond the 5-hour hard cap.** Every Vercel Sandbox has a maximum lifetime. The workflow tracks that deadline and proactively snapshots + recreates *before* the cap, so the logical session outlives any one VM. Effectively unbounded session duration on top of time-bounded infrastructure.
|
|
22
22
|
- **Automatic cleanup.** `try/finally` in the workflow guarantees the VM is stopped on failure or destroy.
|
|
23
23
|
|
|
24
|
+
<Callout type="info">
|
|
25
|
+
An effectively unbounded sandbox session is still one workflow run, so it stays on the deployment that started it. If the controller or agent code should upgrade over time, use an explicit version boundary and pass the serialized state or stream handles forward. See [Versioning](/docs/foundations/versioning).
|
|
26
|
+
</Callout>
|
|
27
|
+
|
|
24
28
|
## Use Case: Coding Agents
|
|
25
29
|
|
|
26
30
|
This is the pattern [Open Agents](https://open-agents.dev/) uses to spawn coding agents that run "infinitely in the cloud." Each agent session gets its own sandbox — full filesystem, network, and runtime access — and the durable workflow keeps the agent loop resumable across restarts, auto-hibernates when the user walks away, and reconnects instantly when they return.
|
|
@@ -93,7 +93,7 @@ interface Storage {
|
|
|
93
93
|
|
|
94
94
|
**Run Creation:** For `run_created` events, the `runId` parameter may be a client-provided string or `null`. When `null`, your World generates and returns a new `runId`.
|
|
95
95
|
|
|
96
|
-
**Hook Tokens:** Hook tokens must be unique. If a `hook_created` event conflicts with an existing token, return a `hook_conflict` event instead.
|
|
96
|
+
**Hook Tokens:** Hook tokens must be unique. If a `hook_created` event conflicts with an existing token, return a `hook_conflict` event instead and include the active hook owner's run ID as `eventData.conflictingRunId`.
|
|
97
97
|
|
|
98
98
|
**Automatic Hook Disposal:** When a workflow reaches a terminal state (`completed`, `failed`, or `cancelled`), automatically dispose of all associated hooks to release tokens for reuse.
|
|
99
99
|
|
|
@@ -139,6 +139,8 @@ On Vercel, workflow runs are pegged to the deployment that started them. This me
|
|
|
139
139
|
|
|
140
140
|
This ensures long-running workflows complete reliably without being affected by subsequent deployments.
|
|
141
141
|
|
|
142
|
+
For the full model, including rerunning on latest and explicit upgrade boundaries, see [Versioning](/docs/foundations/versioning).
|
|
143
|
+
|
|
142
144
|
## Security
|
|
143
145
|
|
|
144
146
|
### Consumer function security
|
|
@@ -73,9 +73,9 @@ export async function processPayment() {
|
|
|
73
73
|
}
|
|
74
74
|
```
|
|
75
75
|
|
|
76
|
-
## Handling Hook Conflicts
|
|
76
|
+
## Handling Hook Conflicts
|
|
77
77
|
|
|
78
|
-
When a hook conflict occurs, awaiting the hook will throw a `HookConflictError`.
|
|
78
|
+
When a hook conflict occurs, awaiting the hook will throw a `HookConflictError`. The error exposes the token that conflicted and, for current worlds, the run ID that currently owns it. `conflictingRunId` remains optional for compatibility with older persisted events and world implementations, so guard it before delegating:
|
|
79
79
|
|
|
80
80
|
```typescript lineNumbers
|
|
81
81
|
import { createHook } from "workflow";
|
|
@@ -93,14 +93,64 @@ export async function processPayment(orderId: string) {
|
|
|
93
93
|
if (HookConflictError.is(error)) { // [!code highlight]
|
|
94
94
|
// Another workflow is already processing this order
|
|
95
95
|
console.log(`Conflicting token: ${error.token}`);
|
|
96
|
-
|
|
96
|
+
if (error.conflictingRunId) {
|
|
97
|
+
console.log(`Active run: ${error.conflictingRunId}`);
|
|
98
|
+
}
|
|
99
|
+
return {
|
|
100
|
+
success: false,
|
|
101
|
+
reason: "duplicate-processing",
|
|
102
|
+
token: error.token,
|
|
103
|
+
runId: error.conflictingRunId
|
|
104
|
+
};
|
|
97
105
|
}
|
|
98
106
|
throw error; // Re-throw other errors
|
|
99
107
|
}
|
|
100
108
|
}
|
|
101
109
|
```
|
|
102
110
|
|
|
103
|
-
This pattern is useful when you want to detect
|
|
111
|
+
This pattern is useful when you want to detect duplicate processing inside the workflow. Runtime APIs such as `resumeHook()` and `getRun()` must be called outside workflow functions, for example from an API route or in a step.
|
|
112
|
+
|
|
113
|
+
### Delegate to the Active Run
|
|
114
|
+
|
|
115
|
+
In idempotency flows, a conflict means another active run already owns the hook token. You can return the duplicate-processing payload from the workflow, resume the active hook to deliver the payload to the existing run, then use `getRun(result.runId)` to wait for, stream, or cancel the active run:
|
|
116
|
+
|
|
117
|
+
```typescript lineNumbers
|
|
118
|
+
import { getRun, resumeHook, start } from "workflow/api";
|
|
119
|
+
import { processPayment } from "@/workflows/process-payment";
|
|
120
|
+
|
|
121
|
+
type ProcessPaymentResult =
|
|
122
|
+
| { success: true; payment: unknown }
|
|
123
|
+
| {
|
|
124
|
+
success: false;
|
|
125
|
+
reason: "duplicate-processing";
|
|
126
|
+
token: string;
|
|
127
|
+
runId?: string;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
export async function POST(request: Request) {
|
|
131
|
+
const { orderId, payment } = await request.json();
|
|
132
|
+
const run = await start(processPayment, [orderId]);
|
|
133
|
+
const result = (await run.returnValue) as ProcessPaymentResult;
|
|
134
|
+
|
|
135
|
+
if (
|
|
136
|
+
result.success === false &&
|
|
137
|
+
result.reason === "duplicate-processing" &&
|
|
138
|
+
result.runId
|
|
139
|
+
) {
|
|
140
|
+
await resumeHook(result.token, payment); // [!code highlight]
|
|
141
|
+
const activeRun = getRun(result.runId); // [!code highlight]
|
|
142
|
+
|
|
143
|
+
return Response.json({
|
|
144
|
+
delegatedToRunId: activeRun.runId,
|
|
145
|
+
result: await activeRun.returnValue
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return Response.json(result);
|
|
150
|
+
}
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
If the caller needs live output instead of the final result, return `activeRun.getReadable()` from the same branch. If the duplicate request should replace the active work, call `await activeRun.cancel()` after inspecting the run.
|
|
104
154
|
|
|
105
155
|
## When Hook Tokens Are Released
|
|
106
156
|
|
|
@@ -122,4 +172,6 @@ After a workflow completes, its hook tokens become available for reuse by other
|
|
|
122
172
|
## Related
|
|
123
173
|
|
|
124
174
|
- [Hooks](/docs/foundations/hooks) - Learn more about using hooks in workflows
|
|
175
|
+
- [getRun](/docs/api-reference/workflow-api/get-run) - Retrieve or control the active run
|
|
176
|
+
- [resumeHook](/docs/api-reference/workflow-api/resume-hook) - Deliver data to the active hook
|
|
125
177
|
- [createWebhook](/docs/api-reference/workflow/create-webhook) - Alternative for fixed webhook URLs
|
|
@@ -32,4 +32,7 @@ Workflow programming can be a slight shift from how you traditionally write real
|
|
|
32
32
|
<Card href="/docs/foundations/idempotency" title="Idempotency">
|
|
33
33
|
Prevent duplicate side effects when retrying operations.
|
|
34
34
|
</Card>
|
|
35
|
+
<Card href="/docs/foundations/versioning" title="Versioning">
|
|
36
|
+
Understand how runs stay pinned to deployments and when to opt in to newer code.
|
|
37
|
+
</Card>
|
|
35
38
|
</Cards>
|
|
@@ -13,7 +13,7 @@ Once you've defined your workflow functions, you need to trigger them to begin e
|
|
|
13
13
|
|
|
14
14
|
## The `start()` Function
|
|
15
15
|
|
|
16
|
-
The [`start()`](/docs/api-reference/workflow-api/start) function is used to programmatically trigger workflow executions from runtime contexts like API routes, Server Actions, or any server-side code.
|
|
16
|
+
The [`start()`](/docs/api-reference/workflow-api/start) function is used to programmatically trigger workflow executions from runtime contexts like API routes, Server Actions, or any server-side code. In v5, you can also call `start()` from inside a workflow function when you want to spawn a child run or continue work in a new run.
|
|
17
17
|
|
|
18
18
|
```typescript lineNumbers
|
|
19
19
|
import { start } from "workflow/api";
|
|
@@ -41,6 +41,10 @@ export async function POST(request: Request) {
|
|
|
41
41
|
|
|
42
42
|
**Learn more**: [`start()` API Reference](/docs/api-reference/workflow-api/start)
|
|
43
43
|
|
|
44
|
+
<Callout type="info">
|
|
45
|
+
For parent-child workflow patterns, see [Workflow Composition](/cookbook/common-patterns/workflow-composition). For long-lived workflows that intentionally hand off to newer deployments with `deploymentId: "latest"`, see [Versioning](/docs/foundations/versioning).
|
|
46
|
+
</Callout>
|
|
47
|
+
|
|
44
48
|
## The `Run` Object
|
|
45
49
|
|
|
46
50
|
When you call `start()`, it returns a [`Run`](/docs/api-reference/workflow-api/start#returns) object that provides access to the workflow's status and results.
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Versioning
|
|
3
|
+
description: Understand how workflow runs are pinned to deployments, how to recover runs after a fix, and how to opt in to newer code explicitly.
|
|
4
|
+
type: guide
|
|
5
|
+
summary: Keep in-flight runs stable by default, then choose explicit upgrade boundaries when you need them.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /docs/foundations/starting-workflows
|
|
8
|
+
related:
|
|
9
|
+
- /docs/api-reference/workflow-api/start
|
|
10
|
+
- /docs/foundations/cancellation
|
|
11
|
+
- /cookbook/common-patterns/workflow-composition
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Workflow runs are pinned to the deployment that starts them. When a run begins, Workflow SDK records the deployment for that run and continues executing the run on that same copy of your code.
|
|
15
|
+
|
|
16
|
+
That default is intentional. Durable workflows can pause for minutes, days, or months. If the code underneath a paused run changed every time you deployed, an in-flight run could resume into a different function body, different step names, or different input types than the ones it started with. That can make type safety fragile and can break long-running work in hard-to-debug ways.
|
|
17
|
+
|
|
18
|
+
With Workflow SDK, you can keep shipping. New runs use new deployments, while existing runs keep the version they already understand.
|
|
19
|
+
|
|
20
|
+
## Default behavior
|
|
21
|
+
|
|
22
|
+
Start a workflow normally:
|
|
23
|
+
|
|
24
|
+
```typescript title="app/api/orders/route.ts" lineNumbers
|
|
25
|
+
import { start } from "workflow/api";
|
|
26
|
+
import { fulfillOrder } from "@/workflows/fulfill-order";
|
|
27
|
+
|
|
28
|
+
export async function POST(request: Request) {
|
|
29
|
+
const { orderId } = await request.json();
|
|
30
|
+
|
|
31
|
+
const run = await start(fulfillOrder, [orderId]); // [!code highlight]
|
|
32
|
+
|
|
33
|
+
return Response.json({ runId: run.runId });
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
The run is tied to the deployment that handled this request. If you deploy a new version while the workflow is [sleeping](/docs/api-reference/workflow/sleep), [waiting on a hook](/docs/foundations/hooks), [retrying a step](/docs/foundations/errors-and-retries), or processing later queue messages, that existing run still resumes on the original deployment.
|
|
38
|
+
|
|
39
|
+
```typescript title="workflows/fulfill-order.ts" lineNumbers
|
|
40
|
+
import { sleep } from "workflow";
|
|
41
|
+
|
|
42
|
+
export async function fulfillOrder(orderId: string) {
|
|
43
|
+
"use workflow";
|
|
44
|
+
|
|
45
|
+
await reserveInventory(orderId);
|
|
46
|
+
await sleep("2d");
|
|
47
|
+
await chargeCustomer(orderId);
|
|
48
|
+
await shipOrder(orderId);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function reserveInventory(orderId: string) {
|
|
52
|
+
"use step";
|
|
53
|
+
// ...
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function chargeCustomer(orderId: string) {
|
|
57
|
+
"use step";
|
|
58
|
+
// ...
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function shipOrder(orderId: string) {
|
|
62
|
+
"use step";
|
|
63
|
+
// ...
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
If you deploy a change to `chargeCustomer()` while a run is in the two-day sleep, the existing run does not suddenly resume into the new implementation. It continues on the deployment it started on. The next order starts on the latest deployment and uses the new code from the beginning.
|
|
68
|
+
|
|
69
|
+
## Fixing in-flight runs
|
|
70
|
+
|
|
71
|
+
Sometimes you deploy because the old code had a bug. The safest fix is usually explicit:
|
|
72
|
+
|
|
73
|
+
1. Deploy the fixed code.
|
|
74
|
+
2. Find the affected runs in [observability](/docs/observability) or with the CLI.
|
|
75
|
+
3. Cancel the old runs if they are still running.
|
|
76
|
+
4. Rerun them on the latest deployment with the same inputs.
|
|
77
|
+
|
|
78
|
+
This keeps the version boundary visible. The old run ends as cancelled or failed, and the replacement run starts fresh on the fixed deployment. This is a good fit for one-off, ad-hoc upgrades where you explicitly opt in to moving affected runs onto a new version.
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
# Inspect affected runs and copy the exact workflowName value.
|
|
82
|
+
npx workflow inspect runs \
|
|
83
|
+
--backend vercel \
|
|
84
|
+
--status running
|
|
85
|
+
|
|
86
|
+
# Cancel one run.
|
|
87
|
+
npx workflow cancel <run-id> \
|
|
88
|
+
--backend vercel
|
|
89
|
+
|
|
90
|
+
# Or bulk-cancel matching running runs.
|
|
91
|
+
npx workflow cancel \
|
|
92
|
+
--status running \
|
|
93
|
+
--workflowName "workflow//./workflows/fulfill-order//fulfillOrder" \
|
|
94
|
+
--backend vercel
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The `--workflowName` filter expects the generated workflow ID, not only the exported function's short name. Use the `workflowName` value from `workflow inspect runs`, and use [`parseWorkflowName()`](/docs/api-reference/workflow-api/world/observability) when you need display-friendly names.
|
|
98
|
+
|
|
99
|
+
In the [observability UI](/docs/observability), use **Rerun on latest** to enqueue the workflow again with the same inputs against the latest deployment.
|
|
100
|
+
|
|
101
|
+
If you are writing your own recovery route, call `start()` with the same arguments and `deploymentId: "latest"`:
|
|
102
|
+
|
|
103
|
+
```typescript title="app/api/orders/rerun/route.ts" lineNumbers
|
|
104
|
+
import { start } from "workflow/api";
|
|
105
|
+
import { fulfillOrder } from "@/workflows/fulfill-order";
|
|
106
|
+
|
|
107
|
+
export async function POST(request: Request) {
|
|
108
|
+
const { orderId } = await request.json();
|
|
109
|
+
|
|
110
|
+
const run = await start(fulfillOrder, [orderId], {
|
|
111
|
+
deploymentId: "latest", // [!code highlight]
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
return Response.json({ runId: run.runId });
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
<Callout type="warn">
|
|
119
|
+
`deploymentId: "latest"` is currently a Vercel-specific feature. Other Worlds may implement this option differently to match their own deployment runtimes, and the World spec may rename it from `deploymentId` to `version` in a future SDK version. On Vercel, `"latest"` resolves to the most recent deployment matching your current environment. Because the caller and target deployment can be different, keep the [workflow function name and file path](/docs/errors/workflow-not-registered), arguments, and return value backward-compatible across the deployments you plan to bridge.
|
|
120
|
+
</Callout>
|
|
121
|
+
|
|
122
|
+
## Self upgrading workflows
|
|
123
|
+
|
|
124
|
+
Some workflows are expected to run for a very long time. Scheduled loops, recurring jobs, agents, and chat sessions often should not stay on one deployment forever.
|
|
125
|
+
|
|
126
|
+
Model those as a sequence of runs. Each run does a bounded piece of work, then starts the next run on the latest deployment and exits. This is similar to `continueAsNew` in other durable execution systems, but in Workflow SDK it is just [explicit recursion through `start()`](/cookbook/common-patterns/workflow-composition).
|
|
127
|
+
|
|
128
|
+
```typescript title="workflows/daily-digest.ts" lineNumbers
|
|
129
|
+
import { sleep } from "workflow";
|
|
130
|
+
import { start } from "workflow/api";
|
|
131
|
+
|
|
132
|
+
type DigestState = {
|
|
133
|
+
userId: string;
|
|
134
|
+
lastSentAt?: string;
|
|
135
|
+
};
|
|
136
|
+
|
|
137
|
+
export async function dailyDigest(state: DigestState) {
|
|
138
|
+
"use workflow";
|
|
139
|
+
|
|
140
|
+
const sentAt = await sendDigest(state.userId);
|
|
141
|
+
await sleep("1d");
|
|
142
|
+
|
|
143
|
+
const run = await start(
|
|
144
|
+
dailyDigest,
|
|
145
|
+
[{ ...state, lastSentAt: sentAt }],
|
|
146
|
+
{
|
|
147
|
+
deploymentId: "latest", // [!code highlight]
|
|
148
|
+
}
|
|
149
|
+
);
|
|
150
|
+
|
|
151
|
+
return { continuedAs: run.runId };
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
async function sendDigest(userId: string) {
|
|
155
|
+
"use step";
|
|
156
|
+
// ...
|
|
157
|
+
return new Date().toISOString();
|
|
158
|
+
}
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
This pattern gives every run a clear lifecycle:
|
|
162
|
+
|
|
163
|
+
- The current run stays on its original deployment.
|
|
164
|
+
- The next run starts on the latest deployment.
|
|
165
|
+
- The [serialized `state`](/docs/foundations/serialization) is the migration boundary between versions.
|
|
166
|
+
- Observability can link parent and child runs when a workflow starts another run.
|
|
167
|
+
|
|
168
|
+
## Carrying context forward
|
|
169
|
+
|
|
170
|
+
Anything that is [serializable by Workflow SDK](/docs/foundations/serialization) can be passed from one run to the next as an argument. That includes plain state objects, `ReadableStream`, `WritableStream`, `AbortSignal`, and other supported serialized values.
|
|
171
|
+
|
|
172
|
+
For example, a long export can register its [output stream](/docs/foundations/streaming) once, write progress from each run, and pass the same stream plus updated state into the next run:
|
|
173
|
+
|
|
174
|
+
```typescript title="workflows/export-report.ts" lineNumbers
|
|
175
|
+
import { getWritable } from "workflow";
|
|
176
|
+
import { start } from "workflow/api";
|
|
177
|
+
|
|
178
|
+
type ExportState = {
|
|
179
|
+
exportId: string;
|
|
180
|
+
page: number;
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
export async function exportReport(
|
|
184
|
+
state: ExportState,
|
|
185
|
+
progress?: WritableStream<string>
|
|
186
|
+
) {
|
|
187
|
+
"use workflow";
|
|
188
|
+
|
|
189
|
+
// Register the stream once. Continuation runs receive this same stream
|
|
190
|
+
// as an argument and keep writing to it.
|
|
191
|
+
const stream =
|
|
192
|
+
progress !== undefined ? progress : getWritable<string>();
|
|
193
|
+
|
|
194
|
+
const hasMore = await exportPage(state, stream);
|
|
195
|
+
|
|
196
|
+
if (!hasMore) {
|
|
197
|
+
await writeProgress(stream, { type: "done", totalPages: state.page });
|
|
198
|
+
return { totalPages: state.page };
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
const run = await start(exportReport, [
|
|
202
|
+
{ ...state, page: state.page + 1 },
|
|
203
|
+
stream,
|
|
204
|
+
], {
|
|
205
|
+
deploymentId: "latest", // [!code highlight]
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
return { continuedAs: run.runId };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function exportPage(
|
|
212
|
+
state: ExportState,
|
|
213
|
+
stream: WritableStream<string>
|
|
214
|
+
) {
|
|
215
|
+
"use step";
|
|
216
|
+
|
|
217
|
+
// Do work for this version boundary.
|
|
218
|
+
const hasMore = state.page < 10;
|
|
219
|
+
const writer = stream.getWriter();
|
|
220
|
+
|
|
221
|
+
try {
|
|
222
|
+
await writer.write(
|
|
223
|
+
JSON.stringify({ type: "page", page: state.page }) + "\n"
|
|
224
|
+
);
|
|
225
|
+
return hasMore;
|
|
226
|
+
} finally {
|
|
227
|
+
writer.releaseLock();
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
async function writeProgress(
|
|
232
|
+
stream: WritableStream<string>,
|
|
233
|
+
event: { type: "done"; totalPages: number }
|
|
234
|
+
) {
|
|
235
|
+
"use step";
|
|
236
|
+
|
|
237
|
+
const writer = stream.getWriter();
|
|
238
|
+
try {
|
|
239
|
+
await writer.write(JSON.stringify(event) + "\n");
|
|
240
|
+
} finally {
|
|
241
|
+
writer.releaseLock();
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
```typescript title="app/api/export/route.ts" lineNumbers
|
|
247
|
+
import { start } from "workflow/api";
|
|
248
|
+
import { exportReport } from "@/workflows/export-report";
|
|
249
|
+
|
|
250
|
+
export async function POST(request: Request) {
|
|
251
|
+
const { exportId } = await request.json();
|
|
252
|
+
|
|
253
|
+
const run = await start(exportReport, [{ exportId, page: 1 }]);
|
|
254
|
+
|
|
255
|
+
// Linked continuation runs keep writing to the stream registered by
|
|
256
|
+
// the parent run, because that stream is passed forward as an argument.
|
|
257
|
+
return new Response(run.readable, {
|
|
258
|
+
headers: { "Content-Type": "application/jsonl" },
|
|
259
|
+
});
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Each run still has one clear version boundary: the current run stays on its original deployment, the next run starts on the latest deployment, and only the explicit state and stream handle are carried forward.
|
|
@@ -75,9 +75,9 @@ To enable helpful hints in your IDE, setup the workflow plugin in `tsconfig.json
|
|
|
75
75
|
</Accordion>
|
|
76
76
|
|
|
77
77
|
<Accordion type="single" collapsible>
|
|
78
|
-
<AccordionItem value="
|
|
78
|
+
<AccordionItem value="configure-proxy-handler" className="[&_h3]:my-0">
|
|
79
79
|
<AccordionTrigger className="text-sm">
|
|
80
|
-
|
|
80
|
+
<h3 id="configure-proxy-handler">Configure Proxy Handler (if applicable)</h3>
|
|
81
81
|
</AccordionTrigger>
|
|
82
82
|
<AccordionContent className="[&_p]:my-2">
|
|
83
83
|
|
|
@@ -85,7 +85,9 @@ If your Next.js app has a [proxy handler](https://nextjs.org/docs/app/api-refere
|
|
|
85
85
|
(formerly known as "middleware"), you'll need to update the matcher pattern to exclude Workflow's
|
|
86
86
|
internal paths to prevent the proxy handler from running on them.
|
|
87
87
|
|
|
88
|
-
|
|
88
|
+
If you see `[local world] Queue operation failed` with `Cannot perform ArrayBuffer.prototype.slice on a detached ArrayBuffer`, your proxy matcher is still intercepting Workflow's internal `POST /.well-known/workflow/v1/flow` request. This is especially easy to miss in Next.js 16, where `proxy.ts` replaced `middleware.ts`.
|
|
89
|
+
|
|
90
|
+
Add `.well-known/workflow/*` to your matcher exclusion list:
|
|
89
91
|
|
|
90
92
|
```typescript title="proxy.ts" lineNumbers
|
|
91
93
|
import { NextResponse } from "next/server";
|
|
@@ -193,7 +193,7 @@ handleUserSignup.workflowId = "workflow//workflows/user.js//handleUserSignup"; /
|
|
|
193
193
|
- The `workflowId` property is added (same as workflow mode)
|
|
194
194
|
- Step functions are not transformed in client mode
|
|
195
195
|
|
|
196
|
-
**Why this transformation?** Workflow functions cannot be called directly—they must be started using [`start()`](/docs/api-reference/workflow-api/start). The error prevents accidental direct execution while the `workflowId` property allows the `start()` function to identify which workflow to launch.
|
|
196
|
+
**Why this transformation?** Workflow functions cannot be called directly from application code—they must be started using [`start()`](/docs/api-reference/workflow-api/start). The error prevents accidental direct execution while the `workflowId` property allows the `start()` function to identify which workflow to launch.
|
|
197
197
|
|
|
198
198
|
The IDs are generated exactly like in workflow mode to ensure they can be directly referenced at runtime.
|
|
199
199
|
|
|
@@ -321,7 +321,7 @@ The compiler generates stable IDs for workflows and steps based on file paths an
|
|
|
321
321
|
- **Portable**: Works across different runtimes and deployments
|
|
322
322
|
|
|
323
323
|
<Callout type="info">
|
|
324
|
-
Although IDs can change when files are moved or functions are renamed, Workflow SDK
|
|
324
|
+
Although IDs can change when files are moved or functions are renamed, Workflow SDK functions assume [atomic versioning](/docs/foundations/versioning) in the World. This means changing IDs won't break old workflows from running, but will prevent runs from being upgraded and will cause your workflow/step names to change in observability across deployments.
|
|
325
325
|
</Callout>
|
|
326
326
|
|
|
327
327
|
## Framework Integration
|
|
@@ -127,7 +127,7 @@ flowchart TD
|
|
|
127
127
|
|
|
128
128
|
Unlike other entities, hooks don't have a `status` field—the states above are conceptual. An "active" hook is one that exists in storage, while "disposed" means the hook has been deleted. When a `hook_disposed` event is created, the hook record is removed rather than updated.
|
|
129
129
|
|
|
130
|
-
While a hook is active, its token is reserved and cannot be used by other workflows. If a workflow attempts to create a hook with a token that is already in use by another active hook, a `hook_conflict` event is recorded instead of `hook_created`. This causes the hook's promise to reject with a `HookConflictError`, which you can detect with `HookConflictError.is(error)`. See the [hook-conflict error](/docs/errors/hook-conflict) documentation for more details.
|
|
130
|
+
While a hook is active, its token is reserved and cannot be used by other workflows. If a workflow attempts to create a hook with a token that is already in use by another active hook, a `hook_conflict` event is recorded instead of `hook_created`. Current worlds include the token and the run ID that currently owns it, though older persisted events or world implementations may only include the token. This causes the hook's promise to reject with a `HookConflictError`, which you can detect with `HookConflictError.is(error)`. See the [hook-conflict error](/docs/errors/hook-conflict) documentation for more details.
|
|
131
131
|
|
|
132
132
|
When a hook is disposed (either explicitly or when its workflow completes), the token is released and can be claimed by future workflows. Hooks are automatically disposed when a workflow reaches a terminal state (`completed`, `failed`, or `cancelled`). The `hook_disposed` event is only needed for explicit disposal before workflow completion.
|
|
133
133
|
|
|
@@ -188,7 +188,7 @@ Events are categorized by the entity type they affect. Each event contains metad
|
|
|
188
188
|
| Event | Description |
|
|
189
189
|
|-------|-------------|
|
|
190
190
|
| `hook_created` | Creates a new hook in `active` state. Contains the hook token and optional metadata. |
|
|
191
|
-
| `hook_conflict` | Records that hook creation failed because the token is already in use by another active hook. The hook is not created, and awaiting the hook will reject with a `HookConflictError`. |
|
|
191
|
+
| `hook_conflict` | Records that hook creation failed because the token is already in use by another active hook. Contains the token and, for current worlds, the active hook owner's run ID. The hook is not created, and awaiting the hook will reject with a `HookConflictError`. |
|
|
192
192
|
| `hook_received` | Records that a payload was delivered to the hook. The hook remains `active` and can receive more payloads. |
|
|
193
193
|
| `hook_disposed` | Deletes the hook from storage (conceptually transitioning to `disposed` state). The token is released for reuse by future workflows. |
|
|
194
194
|
|
|
@@ -54,11 +54,11 @@ The migration replaces declarative configuration with idiomatic TypeScript and c
|
|
|
54
54
|
| Choice state | `if` / `else` / `switch` | Native TypeScript control flow. |
|
|
55
55
|
| Wait state | `sleep()` | Import `sleep` from `workflow`. |
|
|
56
56
|
| Parallel state | `Promise.all()` | Standard concurrency primitives. |
|
|
57
|
-
| Map state | Inline sequential → `for` loop; bounded parallel (`MaxConcurrency: N`) → batched `Promise.all` or a concurrency limiter like `p-limit`; Distributed Map / large fan-out →
|
|
57
|
+
| Map state | Inline sequential → `for` loop; bounded parallel (`MaxConcurrency: N`) → batched `Promise.all` or a concurrency limiter like `p-limit`; Distributed Map / large fan-out → `start()` per item, then step-wrapped `getRun()` to collect. | Match the concurrency mode of the original Map. |
|
|
58
58
|
| Retry / Catch | Step retries, `RetryableError`, `FatalError` | Retry logic moves to step boundaries. |
|
|
59
59
|
| `Catch` to a compensation state | `try`/`catch` in the workflow function, calling compensation steps in reverse order (push/pop a rollback stack) | See [`/docs/foundations/errors-and-retries`](/docs/foundations/errors-and-retries) for the SAGA pattern. |
|
|
60
60
|
| `.waitForTaskToken` | `createHook()` or `createWebhook()` | Hooks for typed signals; webhooks for HTTP. |
|
|
61
|
-
| Child state machine (`StartExecution`) | `"use step"` around `
|
|
61
|
+
| Child state machine (`StartExecution`) | `start()` plus a `"use step"` wrapper around `getRun()` | Return the `Run` object, await its result from another step. |
|
|
62
62
|
| Execution event history | Workflow event log | Same durable replay model. |
|
|
63
63
|
| Progress via DynamoDB / SNS for client polling | `getWritable()` + named streams | Stream durable updates; clients read from the stream. |
|
|
64
64
|
|
|
@@ -213,21 +213,16 @@ return { refundId, status: 'rejected' };
|
|
|
213
213
|
|
|
214
214
|
## Spawn a child workflow
|
|
215
215
|
|
|
216
|
-
In ASL, a parent machine calls `StartExecution` (usually via `.sync` or `.waitForTaskToken`) to launch a child. In
|
|
216
|
+
In ASL, a parent machine calls `StartExecution` (usually via `.sync` or `.waitForTaskToken`) to launch a child. In v5, call `start()` directly from the workflow to launch a child. Wrap `getRun()` and `returnValue` access in a `"use step"` function when you need to await the child result.
|
|
217
217
|
|
|
218
218
|
### Parent starts a child
|
|
219
219
|
|
|
220
220
|
```typescript title="workflow/workflows/parent.ts"
|
|
221
221
|
import { start } from 'workflow/api';
|
|
222
222
|
|
|
223
|
-
async function spawnChild(item: string) {
|
|
224
|
-
'use step'; // [!code highlight]
|
|
225
|
-
return await start(childWorkflow, [item]);
|
|
226
|
-
}
|
|
227
|
-
|
|
228
223
|
export async function parentWorkflow(item: string) {
|
|
229
224
|
'use workflow';
|
|
230
|
-
const run = await
|
|
225
|
+
const run = await start(childWorkflow, [item]); // [!code highlight]
|
|
231
226
|
return { childRunId: run.runId };
|
|
232
227
|
}
|
|
233
228
|
```
|
|
@@ -338,7 +333,7 @@ Delete the ASL JSON, per-task Lambda deployments, IAM roles, and callback queues
|
|
|
338
333
|
## Features without a 1:1 equivalent
|
|
339
334
|
|
|
340
335
|
- **Express workflows.** At-least-once semantics and 5-minute duration make them a poor fit for the SDK's durable replay model. Consider keeping them on Step Functions or migrating to a queue consumer.
|
|
341
|
-
- **Distributed Map state.** Up to 10,000 concurrent child executions with S3 item sources has no 1:1 analog; fan out with
|
|
336
|
+
- **Distributed Map state.** Up to 10,000 concurrent child executions with S3 item sources has no 1:1 analog; fan out with `start()` per item, then `Promise.all` with `p-limit` to bound concurrency.
|
|
342
337
|
- **Optimized AWS service integrations (`arn:aws:states:::dynamodb:*`, `eventbridge:*`, `bedrock:*`, `ecs:runTask.sync`, etc.).** These become regular SDK calls inside `'use step'` functions — credentials, retries, and polling move into the step.
|
|
343
338
|
- **Per-state IAM roles.** ASL lets each state run under its own IAM role. In the SDK, all steps share the deployment's credentials; scope secrets and roles at deployment time.
|
|
344
339
|
- **CloudWatch alarms / X-Ray cross-service traces / CloudWatch Logs retention.** The SDK event log + observability UI replaces orchestrator state transitions, not AWS-wide observability. Keep alarms and traces for other resources.
|
|
@@ -351,8 +346,8 @@ Delete the ASL JSON, per-task Lambda deployments, IAM roles, and callback queues
|
|
|
351
346
|
- Replace Choice states with `if`/`else`/`switch`.
|
|
352
347
|
- Replace Wait states with `sleep()` from `workflow`.
|
|
353
348
|
- Replace Parallel states with `Promise.all()`.
|
|
354
|
-
- Replace Map states based on their concurrency mode: inline sequential → `for` loop; bounded parallel (`MaxConcurrency: N`) → batched `Promise.all` or a concurrency limiter like `p-limit`; Distributed Map / large fan-out →
|
|
355
|
-
- Replace `StartExecution` child machines with `"use step"`
|
|
349
|
+
- Replace Map states based on their concurrency mode: inline sequential → `for` loop; bounded parallel (`MaxConcurrency: N`) → batched `Promise.all` or a concurrency limiter like `p-limit`; Distributed Map / large fan-out → `start()` per item, then step-wrapped `getRun()` to collect.
|
|
350
|
+
- Replace `StartExecution` child machines with direct `start()` calls and a `"use step"` wrapper around `getRun()` when collecting results.
|
|
356
351
|
- Replace `.waitForTaskToken` with `createHook()` (internal callers) or `createWebhook()` (HTTP callers).
|
|
357
352
|
- Move Retry/Catch to step boundaries using `maxRetries`, `RetryableError`, and `FatalError`.
|
|
358
353
|
- Use `getStepMetadata().stepId` as the idempotency key for external side effects.
|
|
@@ -51,10 +51,10 @@ Inngest's event-bus model is loosely coupled — publishers don't know consumers
|
|
|
51
51
|
| `step.run()` | `"use step"` function | Standalone async function with Node.js access. |
|
|
52
52
|
| `step.sleep()` / `step.sleepUntil()` | `sleep()` | `sleep('5m')` for a duration; `sleep(date)` for sleep-until. |
|
|
53
53
|
| `step.waitForEvent()` | `createHook()` or `createWebhook()` | Hooks for typed signals, webhooks for HTTP. |
|
|
54
|
-
| `step.invoke()` | `"use step"`
|
|
54
|
+
| `step.invoke()` | `start()` plus a `"use step"` wrapper around `getRun()` | Spawn a child run, pass `runId` forward, and collect from a step when needed. |
|
|
55
55
|
| `inngest.send()` / event triggers | `start()` from your app boundary | Start workflows directly. |
|
|
56
56
|
| Retry configuration (`retries`) | `RetryableError`, `FatalError`, `maxRetries` | Retry logic lives at the step level. |
|
|
57
|
-
| `step.sendEvent()` | `
|
|
57
|
+
| `step.sendEvent()` | `start()` from the workflow or app boundary | Fan out explicitly, not through an event bus. |
|
|
58
58
|
| Realtime / `step.realtime.publish()` | `getWritable()` / `getWritable({ namespace })` | Named streams are the canonical way for clients to read workflow status. No database or `getRun()` polling required. |
|
|
59
59
|
|
|
60
60
|
## Translate your first workflow
|
|
@@ -169,20 +169,10 @@ Event matching disappears. A hook's token encodes the routing (for example, `ref
|
|
|
169
169
|
|
|
170
170
|
## Spawn a child workflow
|
|
171
171
|
|
|
172
|
-
`step.invoke()` splits into
|
|
172
|
+
`step.invoke()` splits into spawn and collect. In v5, call `start()` directly from the workflow to spawn the child. Wrap `getRun()` and `returnValue` access in a `"use step"` function when you need to collect the result. Returning the `Run` object from `start()` lets observability deep-link into the child run.
|
|
173
173
|
|
|
174
174
|
You can return either the full `Run` object (enables deep-linking) or just `run.runId` (simpler).
|
|
175
175
|
|
|
176
|
-
{/* @skip-typecheck: snippet without imports */}
|
|
177
|
-
```typescript title="workflow/workflows/parent.ts"
|
|
178
|
-
async function spawnChild(item: string) {
|
|
179
|
-
'use step';
|
|
180
|
-
return start(childWorkflow, [item]); // [!code highlight]
|
|
181
|
-
}
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
Await the result in a second step, then orchestrate both from the parent:
|
|
185
|
-
|
|
186
176
|
{/* @skip-typecheck: snippet without imports */}
|
|
187
177
|
```typescript title="workflow/workflows/parent.ts"
|
|
188
178
|
async function collectResult(runId: string) {
|
|
@@ -193,7 +183,7 @@ async function collectResult(runId: string) {
|
|
|
193
183
|
|
|
194
184
|
export async function parentWorkflow(item: string) {
|
|
195
185
|
'use workflow';
|
|
196
|
-
const child = await
|
|
186
|
+
const child = await start(childWorkflow, [item]); // [!code highlight]
|
|
197
187
|
return await collectResult(child.runId);
|
|
198
188
|
}
|
|
199
189
|
```
|
|
@@ -266,7 +256,7 @@ See [Errors and retries](/docs/foundations/errors-and-retries) for full retry do
|
|
|
266
256
|
|
|
267
257
|
- `step.waitForEvent(...)` → `createHook({ token })` + `await hook`. Resume it from an API route with `resumeHook(token, payload)`.
|
|
268
258
|
- `step.sleep(...)` → `sleep("5m")` from `workflow`.
|
|
269
|
-
- `step.invoke(child, { data })` →
|
|
259
|
+
- `step.invoke(child, { data })` → call `start(child, [data])` from the workflow, and optionally read its return value from a step with `getRun(run.runId).returnValue`.
|
|
270
260
|
|
|
271
261
|
### Step 5: Start runs from the app
|
|
272
262
|
|
|
@@ -301,8 +291,8 @@ Remove the `inngest` client, the `serve()` route, event schemas, and the Inngest
|
|
|
301
291
|
- Swap `step.sleep()` / `step.sleepUntil()` for `sleep()` from `workflow`.
|
|
302
292
|
- Swap `step.waitForEvent()` for `createHook()` (internal) or `createWebhook()` (HTTP).
|
|
303
293
|
- Model `waitForEvent` timeouts as `Promise.race()` between the hook and `sleep()`.
|
|
304
|
-
- Replace `step.invoke()` with `"use step"`
|
|
305
|
-
- Replace `step.sendEvent()` fan-out with `start()`
|
|
294
|
+
- Replace `step.invoke()` with direct `start()` calls and a `"use step"` wrapper around `getRun()` when collecting results.
|
|
295
|
+
- Replace `step.sendEvent()` fan-out with explicit `start()` calls.
|
|
306
296
|
- Remove the Inngest client, `serve()` handler, and event definitions.
|
|
307
297
|
- Push retry configuration down to step boundaries via `maxRetries`, `RetryableError`, and `FatalError`.
|
|
308
298
|
- Use `getStepMetadata().stepId` as the idempotency key for external side effects.
|
|
@@ -50,7 +50,7 @@ Migration removes infrastructure and collapses indirection. Business logic stays
|
|
|
50
50
|
| Signal | `createHook()` or `createWebhook()` | Use hooks for typed resume signals; webhooks for HTTP callbacks. |
|
|
51
51
|
| Query | `getWritable({ namespace: 'status' })` stream | Durably stream status updates from the workflow. Clients read from the stream instead of polling a database. |
|
|
52
52
|
| Update | `createHook()` + `resumeHook()` (one-way) | Temporal Updates return a value to the caller; hooks do not. If the Update returns data, either write the result to a named stream via `getWritable()` and have the caller read from it, or keep an HTTP read route that fetches the workflow's current state. |
|
|
53
|
-
| Child Workflow | `"use step"`
|
|
53
|
+
| Child Workflow | `start()` plus a `"use step"` wrapper around `getRun()` | Spawn a child run and return the `Run` object so observability can deep-link into child runs. |
|
|
54
54
|
| Activity retry policy | Step retries, `RetryableError`, `FatalError`, `maxRetries` | Retries live at the step boundary. |
|
|
55
55
|
| Event History | Workflow event log / run timeline | Same durable replay; built-in observability UI replaces Temporal Web. Search attributes and visibility APIs have no direct equivalent — filter by run status and timestamps instead. |
|
|
56
56
|
|
|
@@ -173,19 +173,14 @@ Temporal Queries expose in-memory workflow state on demand. In the Workflow SDK,
|
|
|
173
173
|
|
|
174
174
|
### Minimal translation
|
|
175
175
|
|
|
176
|
-
`start()`
|
|
176
|
+
In v5, call `start()` directly from the workflow to spawn a child run. Wrap `getRun()` and `returnValue` access in a `"use step"` function when you need to await the child result. Return the `Run` object (not a plain `runId` string) so workflow observability can deep-link into child runs.
|
|
177
177
|
|
|
178
178
|
```typescript title="workflow/workflows/parent.ts"
|
|
179
179
|
import { start } from 'workflow/api';
|
|
180
180
|
|
|
181
|
-
async function spawnChild(item: string) {
|
|
182
|
-
'use step'; // [!code highlight]
|
|
183
|
-
return start(childWorkflow, [item]); // [!code highlight]
|
|
184
|
-
}
|
|
185
|
-
|
|
186
181
|
export async function parentWorkflow(item: string) {
|
|
187
182
|
'use workflow';
|
|
188
|
-
const child = await
|
|
183
|
+
const child = await start(childWorkflow, [item]); // [!code highlight]
|
|
189
184
|
return { childRunId: child.runId };
|
|
190
185
|
}
|
|
191
186
|
```
|
|
@@ -204,7 +199,7 @@ async function collectResult(runId: string) {
|
|
|
204
199
|
}
|
|
205
200
|
```
|
|
206
201
|
|
|
207
|
-
Call
|
|
202
|
+
Call `start()` and then `collectResult()` from the parent in sequence: `const result = await collectResult(child.runId)`. To fan out, call `start()` inside a loop, then `Promise.all` the `collectResult` calls.
|
|
208
203
|
|
|
209
204
|
<Callout type="warn">
|
|
210
205
|
Activity retry policy moves to the step boundary. Use `maxRetries`, `RetryableError`, and `FatalError` on each step instead of a single workflow-wide retry block.
|
|
@@ -308,7 +303,7 @@ Remove the Worker process, `@temporalio/*` dependencies, and the Temporal Server
|
|
|
308
303
|
- Convert each Activity into a `"use step"` function.
|
|
309
304
|
- Remove Worker and Task Queue code. Start workflows from the app with `start()`.
|
|
310
305
|
- Replace Signals with `createHook()` or `createWebhook()` for HTTP callers.
|
|
311
|
-
-
|
|
306
|
+
- Use `start()` directly for child workflows, and wrap `getRun()` in a `"use step"` function when collecting results. Return the `Run` object from `start()` so observability can deep-link into child runs.
|
|
312
307
|
- Set retry policy per step with `maxRetries`, `RetryableError`, and `FatalError`.
|
|
313
308
|
- Use `getStepMetadata().stepId` as the idempotency key for external side effects.
|
|
314
309
|
- Stream status and progress from steps with `getWritable({ namespace: 'status' })`, and have clients read from the stream instead of polling.
|
|
@@ -48,7 +48,7 @@ Migration collapses the task abstraction into plain async functions. Business lo
|
|
|
48
48
|
| `logger` / `metadata.set` | `console` + `getWritable({ namespace: 'status' })` | Logs flow through the run timeline. Status writes go on a named stream. |
|
|
49
49
|
| `wait.for({ seconds \| minutes \| hours \| days })` / `wait.until({ date })` | `sleep()` | Import from `workflow`. |
|
|
50
50
|
| `wait.forToken({ timeout })` | `createHook()` + `Promise.race` with `sleep()` | Hooks carry a typed token. |
|
|
51
|
-
| `tasks.trigger()` / `triggerAndWait()` | `start()` and `getRun(runId).returnValue` |
|
|
51
|
+
| `tasks.trigger()` / `triggerAndWait()` | `start()` and `getRun(runId).returnValue` | Call `start()` directly; wrap `getRun()` collection in a `"use step"` function. |
|
|
52
52
|
| `batch.triggerAndWait()` | `Promise.all(runIds.map(collectResult))` | Fan out via standard concurrency. |
|
|
53
53
|
| `AbortTaskRunError` | `FatalError` | Stops retries immediately. |
|
|
54
54
|
| `retry.onThrow` / `retry.fetch` | `RetryableError`, `FatalError`, `maxRetries` | Retry count lives on the step via `myStep.maxRetries = N` (default 3). Control delay between attempts by throwing `new RetryableError(msg, { retryAfter: '5s' })` — there is no built-in exponential helper; compute the delay yourself based on `getStepMetadata().attempt` if you need one. |
|
|
@@ -180,23 +180,14 @@ A hook is an inbound write channel. The caller that knows the token resumes the
|
|
|
180
180
|
|
|
181
181
|
## Spawn a child workflow
|
|
182
182
|
|
|
183
|
-
`triggerAndWait()` splits into
|
|
183
|
+
`triggerAndWait()` splits into spawn and collect. In v5, call `start()` directly from the workflow to spawn the child. Wrap `getRun()` and `returnValue` access in a `"use step"` function when you need to collect the result.
|
|
184
184
|
|
|
185
185
|
You can return either the full `Run` object (enables deep-linking) or just `run.runId` (simpler). The runtime serializes `Run` to its `runId` in the event log either way.
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
import { start } from 'workflow/api';
|
|
189
|
-
|
|
190
|
-
async function spawnChild(item: string) {
|
|
191
|
-
'use step';
|
|
192
|
-
return start(childWorkflow, [item]); // [!code highlight]
|
|
193
|
-
}
|
|
194
|
-
```
|
|
195
|
-
|
|
196
|
-
Await the result in a second step, then orchestrate both from the parent:
|
|
187
|
+
Await the result in a step, then orchestrate both from the parent:
|
|
197
188
|
|
|
198
189
|
```typescript title="workflow/workflows/parent.ts"
|
|
199
|
-
import { getRun } from 'workflow/api';
|
|
190
|
+
import { getRun, start } from 'workflow/api';
|
|
200
191
|
|
|
201
192
|
async function collectResult(runId: string) {
|
|
202
193
|
'use step';
|
|
@@ -206,12 +197,12 @@ async function collectResult(runId: string) {
|
|
|
206
197
|
|
|
207
198
|
export async function parentWorkflow(item: string) {
|
|
208
199
|
'use workflow';
|
|
209
|
-
const child = await
|
|
200
|
+
const child = await start(childWorkflow, [item]); // [!code highlight]
|
|
210
201
|
return await collectResult(child.runId);
|
|
211
202
|
}
|
|
212
203
|
```
|
|
213
204
|
|
|
214
|
-
To fan out, call `
|
|
205
|
+
To fan out, call `start()` inside a loop, then `Promise.all` the `collectResult` calls. That replaces `batch.triggerAndWait()`.
|
|
215
206
|
|
|
216
207
|
`Promise.all` rejects on first failure; use `Promise.allSettled` if you need batch-mode error tolerance similar to trigger.dev's `{ ok, output, error }` per-run result.
|
|
217
208
|
|
|
@@ -273,7 +264,7 @@ async function loadOrder(id: string) {
|
|
|
273
264
|
- `wait.for({ seconds | minutes | hours | days })` / `wait.until({ date })` → `sleep('5m')` or `sleep(date)` from `workflow`.
|
|
274
265
|
- `wait.forToken(token)` → `createHook({ token })` + `await`. Complete it with `resumeHook(token, payload)` from an API route.
|
|
275
266
|
- `wait.forToken({ timeout })` → `Promise.race([hook, sleep(timeout)])`.
|
|
276
|
-
- `triggerAndWait(payload)` →
|
|
267
|
+
- `triggerAndWait(payload)` → call `start(child, [payload])` from the workflow and return the `Run` object, then read the result with a step that calls `getRun(runId).returnValue`.
|
|
277
268
|
|
|
278
269
|
### Step 5: Start runs from the app
|
|
279
270
|
|
|
@@ -324,7 +315,7 @@ Throw `new RetryableError(msg, { retryAfter: '5s' })` to control delay between a
|
|
|
324
315
|
- Swap `wait.for` / `wait.until` for `sleep()` from `workflow`.
|
|
325
316
|
- Swap `wait.forToken` for `createHook()` (internal) or `createWebhook()` (HTTP).
|
|
326
317
|
- Model `wait.forToken` timeouts as `Promise.race()` between the hook and `sleep()`.
|
|
327
|
-
- Replace `triggerAndWait()` with `"use step"`
|
|
318
|
+
- Replace `triggerAndWait()` with direct `start()` calls and a `"use step"` wrapper around `getRun()` when collecting results.
|
|
328
319
|
- Replace `batch.triggerAndWait()` with `Promise.all` over the collected child `Run` handles.
|
|
329
320
|
- Move `schemaTask` validation to the call site; pass typed arguments into the workflow.
|
|
330
321
|
- Replace `AbortTaskRunError` with `FatalError`; model retries per step with `RetryableError` and `maxRetries`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "workflow",
|
|
3
|
-
"version": "5.0.0-beta.
|
|
3
|
+
"version": "5.0.0-beta.7",
|
|
4
4
|
"description": "Workflow SDK - Build durable, resilient, and observable workflows",
|
|
5
5
|
"main": "dist/typescript-plugin.cjs",
|
|
6
6
|
"type": "module",
|
|
@@ -57,18 +57,18 @@
|
|
|
57
57
|
},
|
|
58
58
|
"dependencies": {
|
|
59
59
|
"ms": "2.1.3",
|
|
60
|
-
"@workflow/astro": "5.0.0-beta.
|
|
61
|
-
"@workflow/cli": "5.0.0-beta.
|
|
62
|
-
"@workflow/core": "5.0.0-beta.
|
|
63
|
-
"@workflow/errors": "5.0.0-beta.
|
|
60
|
+
"@workflow/astro": "5.0.0-beta.7",
|
|
61
|
+
"@workflow/cli": "5.0.0-beta.7",
|
|
62
|
+
"@workflow/core": "5.0.0-beta.7",
|
|
63
|
+
"@workflow/errors": "5.0.0-beta.4",
|
|
64
64
|
"@workflow/typescript-plugin": "5.0.0-beta.3",
|
|
65
65
|
"@workflow/utils": "5.0.0-beta.2",
|
|
66
|
-
"@workflow/next": "5.0.0-beta.
|
|
67
|
-
"@workflow/nest": "5.0.0-beta.
|
|
68
|
-
"@workflow/nitro": "5.0.0-beta.
|
|
69
|
-
"@workflow/nuxt": "5.0.0-beta.
|
|
70
|
-
"@workflow/sveltekit": "5.0.0-beta.
|
|
71
|
-
"@workflow/rollup": "5.0.0-beta.
|
|
66
|
+
"@workflow/next": "5.0.0-beta.7",
|
|
67
|
+
"@workflow/nest": "5.0.0-beta.7",
|
|
68
|
+
"@workflow/nitro": "5.0.0-beta.7",
|
|
69
|
+
"@workflow/nuxt": "5.0.0-beta.7",
|
|
70
|
+
"@workflow/sveltekit": "5.0.0-beta.7",
|
|
71
|
+
"@workflow/rollup": "5.0.0-beta.7"
|
|
72
72
|
},
|
|
73
73
|
"devDependencies": {
|
|
74
74
|
"@types/ms": "2.1.0",
|