workflow 5.0.0-beta.3 → 5.0.0-beta.31
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api-workflow.d.ts +1 -1
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +1 -1
- package/dist/api.d.ts +5 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +12 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/internal/builtins.d.ts +17 -0
- package/dist/internal/builtins.d.ts.map +1 -1
- package/dist/internal/builtins.js +65 -1
- package/dist/observability.d.ts +1 -1
- package/dist/observability.js +2 -2
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +5 -2
- package/docs/ai/chat-session-modeling.mdx +7 -3
- package/docs/ai/defining-tools.mdx +2 -2
- package/docs/ai/index.mdx +28 -23
- package/docs/ai/message-queueing.mdx +10 -10
- package/docs/ai/resumable-streams.mdx +9 -1
- package/docs/api-reference/index.mdx +24 -0
- package/docs/api-reference/meta.json +8 -0
- package/docs/api-reference/vitest/index.mdx +28 -7
- package/docs/api-reference/workflow/create-hook.mdx +38 -0
- package/docs/api-reference/workflow/create-webhook.mdx +1 -0
- package/docs/api-reference/workflow/fetch.mdx +5 -0
- package/docs/api-reference/workflow/index.mdx +3 -0
- package/docs/api-reference/workflow/set-attributes.mdx +65 -0
- package/docs/api-reference/workflow-ai/durable-agent.mdx +7 -45
- package/docs/api-reference/workflow-ai/index.mdx +2 -2
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +44 -1
- package/docs/api-reference/workflow-api/get-hook-by-token.mdx +7 -0
- package/docs/api-reference/workflow-api/get-run.mdx +25 -0
- package/docs/api-reference/workflow-api/index.mdx +6 -8
- package/docs/api-reference/workflow-api/resume-hook.mdx +57 -0
- package/docs/api-reference/workflow-api/start.mdx +13 -5
- package/docs/api-reference/workflow-astro/index.mdx +18 -0
- package/docs/api-reference/workflow-astro/meta.json +4 -0
- package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
- package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
- package/docs/api-reference/workflow-errors/index.mdx +85 -0
- package/docs/api-reference/workflow-errors/meta.json +5 -0
- package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
- package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
- package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
- package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
- package/docs/api-reference/workflow-nest/index.mdx +31 -0
- package/docs/api-reference/workflow-nest/meta.json +9 -0
- package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
- package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
- package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
- package/docs/api-reference/workflow-next/with-workflow.mdx +56 -2
- package/docs/api-reference/workflow-nitro/index.mdx +60 -0
- package/docs/api-reference/workflow-nuxt/index.mdx +48 -0
- package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
- package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
- package/docs/api-reference/workflow-observability/index.mdx +64 -0
- package/docs/api-reference/workflow-observability/meta.json +11 -0
- package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
- package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
- package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
- package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
- package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
- package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +7 -10
- package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
- package/docs/api-reference/workflow-runtime/index.mdx +43 -0
- package/docs/api-reference/workflow-runtime/meta.json +12 -0
- package/docs/api-reference/workflow-runtime/set-world.mdx +49 -0
- package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +42 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/index.mdx +5 -8
- package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/queue.mdx +4 -2
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +11 -4
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +2 -2
- package/docs/api-reference/workflow-serde/index.mdx +0 -1
- package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +1 -2
- package/docs/api-reference/workflow-serde/workflow-serialize.mdx +1 -2
- package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
- package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
- package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
- package/docs/api-reference/workflow-vite/index.mdx +18 -0
- package/docs/api-reference/workflow-vite/meta.json +4 -0
- package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
- package/docs/changelog/attributes-mvp.mdx +380 -0
- package/docs/changelog/eager-processing.mdx +269 -0
- package/docs/changelog/index.mdx +2 -1
- package/docs/changelog/lazy-event-creation.md +127 -0
- package/docs/changelog/meta.json +8 -1
- package/docs/changelog/resilient-start.mdx +31 -283
- package/docs/changelog/step-message-ownership.mdx +360 -0
- package/docs/changelog/turbo-mode.md +87 -0
- package/docs/configuration/build-and-diagnostics.mdx +51 -0
- package/docs/configuration/cli-and-web-ui.mdx +154 -0
- package/docs/configuration/framework-options.mdx +165 -0
- package/docs/configuration/index.mdx +32 -0
- package/docs/configuration/meta.json +12 -0
- package/docs/configuration/runtime-tuning.mdx +156 -0
- package/docs/configuration/worlds.mdx +228 -0
- package/docs/cookbook/advanced/child-workflows.mdx +199 -256
- package/docs/cookbook/advanced/meta.json +1 -1
- package/docs/cookbook/advanced/publishing-libraries.mdx +13 -12
- package/docs/cookbook/advanced/serializable-steps.mdx +8 -4
- package/docs/cookbook/advanced/upgrading-workflows.mdx +195 -0
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +31 -76
- package/docs/cookbook/agent-patterns/durable-agent.mdx +11 -143
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +7 -3
- package/docs/cookbook/common-patterns/idempotency.mdx +36 -52
- package/docs/cookbook/common-patterns/rate-limiting.mdx +1 -1
- package/docs/cookbook/common-patterns/saga.mdx +2 -2
- package/docs/cookbook/common-patterns/scheduling.mdx +8 -0
- package/docs/cookbook/common-patterns/timeouts.mdx +2 -1
- package/docs/cookbook/common-patterns/workflow-composition.mdx +15 -16
- package/docs/cookbook/index.mdx +3 -3
- package/docs/cookbook/integrations/ai-sdk.mdx +60 -28
- package/docs/cookbook/integrations/chat-sdk.mdx +13 -0
- package/docs/cookbook/integrations/sandbox.mdx +13 -0
- package/docs/deploying/building-a-world.mdx +1 -1
- package/docs/deploying/index.mdx +1 -0
- package/docs/deploying/world/local-world.mdx +23 -6
- package/docs/deploying/world/postgres-world.mdx +79 -13
- package/docs/deploying/world/vercel-world.mdx +47 -12
- package/docs/errors/abort-signal-timeout-in-workflow.mdx +80 -0
- package/docs/errors/corrupted-event-log.mdx +5 -5
- package/docs/errors/hook-conflict.mdx +56 -4
- package/docs/errors/index.mdx +1 -35
- package/docs/errors/replay-divergence.mdx +27 -0
- package/docs/errors/runtime-decryption-failed.mdx +77 -0
- package/docs/errors/step-executed-multiple-times.mdx +23 -0
- package/docs/errors/step-not-registered.mdx +1 -1
- package/docs/foundations/cancellation.mdx +459 -0
- package/docs/foundations/errors-and-retries.mdx +7 -3
- package/docs/foundations/hooks.mdx +29 -0
- package/docs/foundations/idempotency.mdx +236 -11
- package/docs/foundations/index.mdx +1 -23
- package/docs/foundations/meta.json +3 -1
- package/docs/foundations/serialization.mdx +77 -41
- package/docs/foundations/starting-workflows.mdx +5 -1
- package/docs/foundations/streaming.mdx +14 -23
- package/docs/foundations/versioning.mdx +263 -0
- package/docs/getting-started/astro.mdx +6 -0
- package/docs/getting-started/index.mdx +6 -7
- package/docs/getting-started/meta.json +1 -0
- package/docs/getting-started/nestjs.mdx +9 -0
- package/docs/getting-started/next.mdx +5 -3
- package/docs/getting-started/nitro.mdx +22 -0
- package/docs/getting-started/sveltekit.mdx +6 -0
- package/docs/getting-started/tanstack-start.mdx +241 -0
- package/docs/how-it-works/cancellation.mdx +287 -0
- package/docs/how-it-works/code-transform.mdx +2 -2
- package/docs/how-it-works/encryption.mdx +2 -2
- package/docs/how-it-works/event-sourcing.mdx +2 -2
- package/docs/how-it-works/meta.json +2 -1
- package/docs/internal/index.mdx +21 -0
- package/docs/internal/meta.json +10 -0
- package/docs/internal/nitro-native-build.mdx +38 -0
- package/docs/internal/nitro-web-ui.mdx +24 -0
- package/docs/internal/serializable-abort-controller.mdx +148 -0
- package/docs/meta.json +1 -1
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +7 -12
- package/docs/migration-guides/migrating-from-inngest.mdx +7 -17
- package/docs/migration-guides/migrating-from-temporal.mdx +6 -11
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +8 -17
- package/docs/observability/attributes.mdx +81 -0
- package/docs/observability/index.mdx +18 -1
- package/docs/observability/meta.json +1 -1
- package/docs/observability/tracing.mdx +119 -0
- package/docs/testing/index.mdx +2 -2
- package/package.json +14 -13
- package/docs/api-reference/workflow-api/world/meta.json +0 -4
- package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
- package/docs/cookbook/advanced/distributed-abort-controller.mdx +0 -318
|
@@ -1,45 +1,25 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Idempotency
|
|
3
|
-
description:
|
|
3
|
+
description: Make step retries safe and coordinate duplicate workflow starts with hook tokens.
|
|
4
4
|
type: guide
|
|
5
|
-
summary: Use step IDs
|
|
5
|
+
summary: Use step IDs for retry-safe external calls, and use deterministic hook tokens when duplicate requests must route to one active workflow.
|
|
6
6
|
---
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Use idempotency when a retry or duplicate request should not repeat the underlying work. In Workflow, there are two common patterns: use the step ID for retry-safe external calls, and use hook tokens to coordinate duplicate workflow starts.
|
|
9
9
|
|
|
10
10
|
## When to use this
|
|
11
11
|
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
- Creating records in external systems where duplicates are harmful
|
|
15
|
-
- Any step that has side effects in systems you don't control
|
|
12
|
+
- A step charges a payment, sends an email, enqueues work, or creates an external record.
|
|
13
|
+
- A route may receive duplicate requests that should map to one active workflow run.
|
|
16
14
|
|
|
17
|
-
##
|
|
15
|
+
## Step idempotency
|
|
18
16
|
|
|
19
17
|
Every step has a unique, deterministic `stepId` available via `getStepMetadata()`. Pass this as the idempotency key to external APIs:
|
|
20
18
|
|
|
21
19
|
```typescript
|
|
22
20
|
import { getStepMetadata } from "workflow";
|
|
23
21
|
|
|
24
|
-
|
|
25
|
-
declare function sendReceipt(customerId: string, chargeId: string): Promise<void>; // @setup
|
|
26
|
-
|
|
27
|
-
export async function chargeCustomer(customerId: string, amount: number) {
|
|
28
|
-
"use workflow";
|
|
29
|
-
|
|
30
|
-
const charge = await createCharge(customerId, amount);
|
|
31
|
-
await sendReceipt(customerId, charge.id);
|
|
32
|
-
|
|
33
|
-
return { customerId, chargeId: charge.id, status: "completed" };
|
|
34
|
-
}
|
|
35
|
-
```
|
|
36
|
-
|
|
37
|
-
### Step function with idempotency key
|
|
38
|
-
|
|
39
|
-
```typescript
|
|
40
|
-
import { getStepMetadata } from "workflow";
|
|
41
|
-
|
|
42
|
-
async function createCharge(
|
|
22
|
+
export async function createCharge(
|
|
43
23
|
customerId: string,
|
|
44
24
|
amount: number
|
|
45
25
|
): Promise<{ id: string }> {
|
|
@@ -69,39 +49,43 @@ async function createCharge(
|
|
|
69
49
|
|
|
70
50
|
return charge.json();
|
|
71
51
|
}
|
|
72
|
-
|
|
73
|
-
async function sendReceipt(customerId: string, chargeId: string): Promise<void> {
|
|
74
|
-
"use step";
|
|
75
|
-
|
|
76
|
-
const { stepId } = getStepMetadata();
|
|
77
|
-
|
|
78
|
-
await fetch("https://api.example.com/receipts", {
|
|
79
|
-
method: "POST",
|
|
80
|
-
headers: { "Idempotency-Key": stepId },
|
|
81
|
-
body: JSON.stringify({ customerId, chargeId }),
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
52
|
```
|
|
85
53
|
|
|
86
|
-
|
|
54
|
+
See [Step Idempotency](/docs/foundations/idempotency#step-idempotency) for why `stepId` is stable across retries and how to think about external API conflicts.
|
|
87
55
|
|
|
88
|
-
|
|
56
|
+
## Run idempotency
|
|
89
57
|
|
|
90
|
-
-
|
|
91
|
-
- **Don't use check-then-act patterns** like "read a flag, then write if not set" -- another run could read the same flag between your read and write.
|
|
58
|
+
For duplicate workflow-start requests, derive a hook token from your domain key. You can avoid obvious duplicate starts by checking whether an active hook already owns that token before calling `start()`:
|
|
92
59
|
|
|
93
|
-
|
|
60
|
+
```typescript
|
|
61
|
+
import { getHookByToken, start } from "workflow/api";
|
|
62
|
+
import { HookNotFoundError } from "workflow/errors";
|
|
63
|
+
import { processOrder } from "./workflows/process-order";
|
|
64
|
+
|
|
65
|
+
export async function POST(request: Request) {
|
|
66
|
+
const { orderId } = await request.json();
|
|
67
|
+
const token = `order:${orderId}`;
|
|
68
|
+
|
|
69
|
+
try {
|
|
70
|
+
const hook = await getHookByToken(token); // [!code highlight]
|
|
71
|
+
return Response.json({ runId: hook.runId, reused: true });
|
|
72
|
+
} catch (error) {
|
|
73
|
+
if (!HookNotFoundError.is(error)) throw error;
|
|
74
|
+
}
|
|
94
75
|
|
|
95
|
-
|
|
76
|
+
const run = await start(processOrder, [orderId]); // [!code highlight]
|
|
77
|
+
return Response.json({ runId: run.runId, reused: false });
|
|
78
|
+
}
|
|
79
|
+
```
|
|
96
80
|
|
|
97
|
-
-
|
|
98
|
-
- **Always provide idempotency keys for non-idempotent external calls.** Even if you think a step won't be retried, cold-start replay will re-execute it.
|
|
99
|
-
- **Handle 409/conflict as success.** If an external API returns "already processed," treat that as a successful result, not an error.
|
|
100
|
-
- **Make your own APIs idempotent** where possible. Accept an idempotency key and return the cached result on duplicate requests.
|
|
81
|
+
The workflow should create the deterministic hook and check `await hook.getConflict()` before duplicate-sensitive work — awaiting `getConflict()` suspends the workflow to commit the hook registration and resolves with the conflicting run when another active run already owns the token (or `null` once the hook is registered). See [Run idempotency](/docs/foundations/idempotency#run-idempotency) for the full pattern, including how to steer an active run with `resumeHook()` and how to handle the current race between `start()` and hook registration.
|
|
101
82
|
|
|
102
83
|
## Key APIs
|
|
103
84
|
|
|
104
|
-
- [`"use workflow"`](/docs/
|
|
105
|
-
- [`"use step"`](/docs/
|
|
106
|
-
- [`getStepMetadata()`](/docs/api-reference/
|
|
85
|
+
- [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions) -- declares the orchestrator function
|
|
86
|
+
- [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) -- declares step functions with full Node.js access
|
|
87
|
+
- [`getStepMetadata()`](/docs/api-reference/workflow/get-step-metadata) -- provides the deterministic `stepId` for idempotency keys
|
|
88
|
+
- [`createHook()`](/docs/api-reference/workflow/create-hook) -- creates a hook with an optional deterministic token
|
|
89
|
+
- [`getHookByToken()`](/docs/api-reference/workflow-api/get-hook-by-token) -- finds the active hook for a token
|
|
90
|
+
- [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) -- resumes the active hook when the duplicate request carries data
|
|
107
91
|
- [`start()`](/docs/api-reference/workflow-api/start) -- starts a new workflow run
|
|
@@ -224,5 +224,5 @@ export async function downloadWithRetry(url: string) {
|
|
|
224
224
|
- [`"use step"`](/docs/foundations/workflows-and-steps) -- marks functions that run with full Node.js access
|
|
225
225
|
- [`RetryableError`](/docs/api-reference/workflow/retryable-error) -- signals the runtime to retry after a delay
|
|
226
226
|
- [`FatalError`](/docs/api-reference/workflow/fatal-error) -- signals a permanent failure, skipping retries
|
|
227
|
-
- [`getStepMetadata()`](/docs/api-reference/
|
|
227
|
+
- [`getStepMetadata()`](/docs/api-reference/workflow/get-step-metadata) -- provides the current attempt number and step ID
|
|
228
228
|
- [`sleep()`](/docs/api-reference/workflow/sleep) -- durable pause for circuit breaker cooldowns
|
|
@@ -241,7 +241,7 @@ export async function subscriptionUpgradeSaga(accountId: string, seats: number)
|
|
|
241
241
|
|
|
242
242
|
## Key APIs
|
|
243
243
|
|
|
244
|
-
- [`"use workflow"`](/docs/
|
|
245
|
-
- [`"use step"`](/docs/
|
|
244
|
+
- [`"use workflow"`](/docs/foundations/workflows-and-steps#workflow-functions) -- declares the orchestrator function
|
|
245
|
+
- [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) -- declares step functions with full Node.js access
|
|
246
246
|
- [`FatalError`](/docs/api-reference/workflow/fatal-error) -- non-retryable error that triggers compensation
|
|
247
247
|
- [`getWritable()`](/docs/api-reference/workflow/get-writable) -- streams data from workflows for real-time UI updates
|
|
@@ -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)
|
|
@@ -102,6 +106,10 @@ export async function POST(req: Request) {
|
|
|
102
106
|
3. **Race** — `Promise.race([sleep(...), hook])` blocks until either the timer fires or the hook is resumed, whichever comes first.
|
|
103
107
|
4. **Fresh hooks per window** — after a sleep completes normally, the previous hook instance is consumed. A new `.create()` call registers a fresh hook for the next sleep window, reusing the same token.
|
|
104
108
|
|
|
109
|
+
<Callout type="info">
|
|
110
|
+
Deterministic hook tokens can also serve as the idempotency point for scheduled runs. If duplicate schedule starts would send duplicate campaigns or reminders, create a hook with a token derived from the campaign key near the beginning of the workflow and route retries through that hook. If two scheduled starts race, the duplicate run can detect the conflict early with `await hook.getConflict()`, which resolves with the active owner so the duplicate can defer to it. See [Idempotency](/docs/foundations/idempotency).
|
|
111
|
+
</Callout>
|
|
112
|
+
|
|
105
113
|
## Adapting to your use case
|
|
106
114
|
|
|
107
115
|
- **Change durations** — replace `"2d"` with any duration string (`"1h"`, `"7d"`, `"30m"`) or a `Date` object for absolute times.
|
|
@@ -80,7 +80,7 @@ export async function waitForApproval(requestId: string) {
|
|
|
80
80
|
4. **Throw to fail the workflow** — inside a workflow function, throwing an `Error` exits the run with that error. Use `FatalError` inside steps; throw plain errors inside workflows.
|
|
81
81
|
|
|
82
82
|
<Callout type="warn">
|
|
83
|
-
**The losing operation keeps running.** `Promise.race` doesn't cancel — when the sleep wins, the underlying step (or model call, or HTTP request) continues to completion in the background. This is fine for idempotent reads but matters when the operation has side effects or costs money.
|
|
83
|
+
**The losing operation keeps running.** `Promise.race` doesn't cancel — when the sleep wins, the underlying step (or model call, or HTTP request) continues to completion in the background. This is fine for idempotent reads but matters when the operation has side effects or costs money. Pass an `AbortSignal` into the step to cancel it cooperatively, and use idempotency keys for non-idempotent side effects. See the [Cancellation Guide](/docs/foundations/cancellation) and [Idempotency](/docs/foundations/idempotency) for patterns.
|
|
84
84
|
</Callout>
|
|
85
85
|
|
|
86
86
|
## Adapting to your use case
|
|
@@ -96,4 +96,5 @@ export async function waitForApproval(requestId: string) {
|
|
|
96
96
|
- [`sleep()`](/docs/api-reference/workflow/sleep) — durable wait (survives restarts, zero compute cost)
|
|
97
97
|
- [`createWebhook()`](/docs/api-reference/workflow/create-webhook) — create a webhook URL the workflow can race against
|
|
98
98
|
- [`defineHook()`](/docs/api-reference/workflow/define-hook) — typed hook for in-process cancellation
|
|
99
|
+
- [Idempotency](/docs/foundations/idempotency) — protect side effects that may keep running after a timeout
|
|
99
100
|
- [`Promise.race()`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/race) — race operations against deadlines
|
|
@@ -5,11 +5,12 @@ type: guide
|
|
|
5
5
|
summary: Compose workflows two ways — direct await flattens the child into the parent's event log, while background spawn via start() runs the child as an independent run.
|
|
6
6
|
related:
|
|
7
7
|
- /cookbook/advanced/child-workflows
|
|
8
|
+
- /cookbook/common-patterns/idempotency
|
|
8
9
|
- /docs/api-reference/workflow-api/start
|
|
9
10
|
- /docs/api-reference/workflow-api/get-run
|
|
10
11
|
---
|
|
11
12
|
|
|
12
|
-
Workflows can call other workflows. Choose between two composition modes depending on whether the parent needs the child's result inline (direct await) or wants to fire the child off as an independent run (background spawn). For massive fan-out with
|
|
13
|
+
Workflows can call other workflows. Choose between two composition modes depending on whether the parent needs the child's result inline (direct await) or wants to fire the child off as an independent run (background spawn). For massive fan-out with hook-based waiting and partial-failure handling, see [Child Workflows](/cookbook/advanced/child-workflows).
|
|
13
14
|
|
|
14
15
|
## When to use this
|
|
15
16
|
|
|
@@ -53,7 +54,7 @@ The parent waits for the child to finish before continuing. Both functions share
|
|
|
53
54
|
|
|
54
55
|
### Background spawn via `start()`
|
|
55
56
|
|
|
56
|
-
To run a child workflow independently without blocking the parent, call [`start()`](/docs/api-reference/workflow-api/start) from
|
|
57
|
+
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
58
|
|
|
58
59
|
```typescript lineNumbers
|
|
59
60
|
import { start } from "workflow/api";
|
|
@@ -62,37 +63,34 @@ declare function generateReport(reportId: string): Promise<void>; // @setup
|
|
|
62
63
|
declare function fulfillOrder(orderId: string): Promise<{ id: string }>; // @setup
|
|
63
64
|
declare function sendConfirmation(orderId: string): Promise<void>; // @setup
|
|
64
65
|
|
|
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
66
|
export async function processOrder(orderId: string) {
|
|
73
67
|
"use workflow";
|
|
74
68
|
|
|
75
69
|
const order = await fulfillOrder(orderId);
|
|
76
70
|
|
|
77
|
-
const
|
|
71
|
+
const reportRun = await start(generateReport, [orderId]); // [!code highlight]
|
|
78
72
|
|
|
79
73
|
await sendConfirmation(orderId);
|
|
80
74
|
|
|
81
|
-
return { orderId, reportRunId };
|
|
75
|
+
return { orderId, reportRunId: reportRun.runId };
|
|
82
76
|
}
|
|
83
77
|
```
|
|
84
78
|
|
|
85
79
|
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
80
|
|
|
87
81
|
<Callout type="info">
|
|
88
|
-
|
|
82
|
+
Each background spawn creates a separate run. If duplicate requests must route to one active child workflow, have the child create a deterministic hook token from the business key and use that hook as the idempotency point. If concurrent starts race, the losing child can detect the conflict early with `await hook.getConflict()`, which resolves with the active owner so the child can point callers at it. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
|
|
83
|
+
</Callout>
|
|
84
|
+
|
|
85
|
+
<Callout type="info">
|
|
86
|
+
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
87
|
</Callout>
|
|
90
88
|
|
|
91
89
|
## How it works
|
|
92
90
|
|
|
93
91
|
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
92
|
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()`
|
|
93
|
+
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
94
|
|
|
97
95
|
## Choosing between the two modes
|
|
98
96
|
|
|
@@ -106,9 +104,9 @@ If you want the child workflow to run on the latest deployment rather than the c
|
|
|
106
104
|
|
|
107
105
|
## Adapting to your use case
|
|
108
106
|
|
|
109
|
-
- **Spawn many children at once** — call `start()` in a loop
|
|
110
|
-
- **Wait for a background child to finish** — combine `start()` with
|
|
111
|
-
- **Pass results back from background children** — the
|
|
107
|
+
- **Spawn many children at once** — call `start()` in a loop from the workflow. For more advanced fan-out (chunking, hook-based waiting, partial-failure handling), graduate to the [Child Workflows](/cookbook/advanced/child-workflows) recipe.
|
|
108
|
+
- **Wait for a background child to finish** — combine `start()` with a completion hook the child resumes when done. The [Child Workflows](/cookbook/advanced/child-workflows) page covers the recommended `startAndWait()` pattern.
|
|
109
|
+
- **Pass results back from background children** — the wrapped child resumes the parent's hook in `finally` with `{ status, value | error }`; the parent awaits the hook instead of polling `getRun().status`.
|
|
112
110
|
|
|
113
111
|
## Key APIs
|
|
114
112
|
|
|
@@ -116,3 +114,4 @@ If you want the child workflow to run on the latest deployment rather than the c
|
|
|
116
114
|
- [`"use step"`](/docs/foundations/workflows-and-steps) — marks functions with full Node.js access
|
|
117
115
|
- [`start()`](/docs/api-reference/workflow-api/start) — spawn a child workflow as a separate run
|
|
118
116
|
- [`getRun()`](/docs/api-reference/workflow-api/get-run) — retrieve a workflow run's status and return value
|
|
117
|
+
- [Idempotency](/docs/foundations/idempotency) — deduplicate step side effects and workflow starts
|
package/docs/cookbook/index.mdx
CHANGED
|
@@ -8,7 +8,7 @@ A curated collection of workflow patterns with clean, copy-paste code examples f
|
|
|
8
8
|
|
|
9
9
|
## Agent Patterns
|
|
10
10
|
|
|
11
|
-
- [**
|
|
11
|
+
- [**WorkflowAgent**](/cookbook/agent-patterns/durable-agent) — Build durable, resumable AI agents with AI SDK's WorkflowAgent
|
|
12
12
|
- [**Human-in-the-Loop**](/cookbook/agent-patterns/human-in-the-loop) — Pause an agent for human approval, then resume based on the decision
|
|
13
13
|
- [**Agent Cancellation**](/cookbook/agent-patterns/agent-cancellation) — Stop a running agent immediately via `run.cancel()` or gracefully via a hook + `Promise.race`
|
|
14
14
|
|
|
@@ -21,7 +21,7 @@ A curated collection of workflow patterns with clean, copy-paste code examples f
|
|
|
21
21
|
- [**Rate Limiting**](/cookbook/common-patterns/rate-limiting) — Handle 429 responses and transient failures with RetryableError and backoff
|
|
22
22
|
- [**Scheduling**](/cookbook/common-patterns/scheduling) — Use durable sleep to schedule actions minutes, hours, or weeks ahead
|
|
23
23
|
- [**Timeouts**](/cookbook/common-patterns/timeouts) — Add deadlines to slow steps, hooks, and webhooks by racing them against a durable sleep
|
|
24
|
-
- [**Idempotency**](/cookbook/common-patterns/idempotency) — Ensure side effects
|
|
24
|
+
- [**Idempotency**](/cookbook/common-patterns/idempotency) — Ensure side effects and duplicate starts are safe to retry
|
|
25
25
|
- [**Webhooks**](/cookbook/common-patterns/webhooks) — Receive HTTP callbacks from external services and process them durably
|
|
26
26
|
|
|
27
27
|
## Integrations
|
|
@@ -33,6 +33,6 @@ A curated collection of workflow patterns with clean, copy-paste code examples f
|
|
|
33
33
|
## Advanced
|
|
34
34
|
|
|
35
35
|
- [**Child Workflows**](/cookbook/advanced/child-workflows) — Spawn and orchestrate child workflows from a parent
|
|
36
|
-
- [**
|
|
36
|
+
- [**Upgrading Workflows**](/cookbook/advanced/upgrading-workflows) — Identify a clean upgrade point in a long-running workflow and spawn a fresh run on the latest deployment carrying state forward
|
|
37
37
|
- [**Serializable Steps**](/cookbook/advanced/serializable-steps) — Wrap non-serializable third-party objects so they cross the workflow boundary
|
|
38
38
|
- [**Publishing Libraries**](/cookbook/advanced/publishing-libraries) — Ship npm packages that export reusable workflow functions
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: AI SDK
|
|
3
|
-
description: Use AI SDK's streamText directly inside durable workflows
|
|
3
|
+
description: Use AI SDK's streamText directly inside durable workflows when you need the raw AI SDK API or a per-turn durability boundary.
|
|
4
4
|
type: guide
|
|
5
|
-
summary: Use streamText() inside a workflow
|
|
5
|
+
summary: Use streamText() inside a workflow when the durability boundary is an entire user turn, or when you need AI SDK APIs not exposed by WorkflowAgent. Individual tool calls and LLM calls inside a turn are not separately durable.
|
|
6
6
|
related:
|
|
7
7
|
- /docs/ai
|
|
8
8
|
- /docs/ai/chat-session-modeling
|
|
@@ -11,27 +11,32 @@ related:
|
|
|
11
11
|
- /docs/api-reference/workflow-ai/durable-agent
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
[AI SDK](https://ai-sdk.dev/) is Vercel's framework-agnostic TypeScript toolkit for building AI-powered apps and agents — unified provider access, streaming, tool calling, structured output, and UI hooks. Workflow SDK complements it by making
|
|
14
|
+
[AI SDK](https://ai-sdk.dev/) is Vercel's framework-agnostic TypeScript toolkit for building AI-powered apps and agents — unified provider access, streaming, tool calling, structured output, and UI hooks. Workflow SDK complements it by making the multi-turn loop durable: the conversation state, hooks, and per-turn responses survive restarts and timeouts. Note that in this pattern the durability boundary is the entire turn — individual tool calls inside a turn are **not** durable on their own (see [Pitfalls](#tools-are-not-individually-durable) below).
|
|
15
15
|
|
|
16
16
|
For the full AI SDK reference (providers, `streamText`, `generateObject`, `useChat`, tool calling, etc.) see the [AI SDK docs](https://ai-sdk.dev/docs). This page covers the Workflow-specific integration points.
|
|
17
17
|
|
|
18
18
|
<Callout type="info">
|
|
19
|
-
For most agent use cases, prefer [`
|
|
19
|
+
For most agent use cases, prefer AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent), which implements the same agent loop as [`streamText`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text), manages tool calling automatically, and runs tools at workflow scope — each tool can be marked `"use step"` for per-call durability and retries, or stay at workflow level to use primitives like `sleep()` and hooks. Use this page's raw `streamText()` pattern when you want the exact AI SDK API (for example `toUIMessageStream()`, `onChunk`, or `generateText`), or when the durability boundary should be an entire user turn in one step — accepting that tool calls inside that turn are not individually durable.
|
|
20
20
|
</Callout>
|
|
21
21
|
|
|
22
22
|
## When to use streamText directly
|
|
23
23
|
|
|
24
|
-
Use [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) instead of `
|
|
24
|
+
Use [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) instead of `WorkflowAgent` when you need:
|
|
25
25
|
|
|
26
|
-
* **
|
|
27
|
-
* **
|
|
28
|
-
* **
|
|
29
|
-
|
|
26
|
+
* **The raw AI SDK API** — `streamText().toUIMessageStream()`, `onChunk`, `smoothStream`, or other options that map directly to the [`streamText`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) return value rather than `WorkflowAgent.stream()`
|
|
27
|
+
* **Per-turn durability** — wrap the entire agent response (model + tools) in a single `"use step"` function so one user turn is the atomic retry unit; useful when you want all tool calls inside a turn to re-execute together
|
|
28
|
+
* **Custom multi-turn orchestration** — manual hook loops, per-turn stream slicing (`sliceUntilFinish`), or other workflow patterns shown below that don't map cleanly to `WorkflowAgent`
|
|
29
|
+
|
|
30
|
+
`WorkflowAgent` already supports `stopWhen`, `prepareStep`, lifecycle callbacks, structured output (`output`), per-step model switching, and [provider options](https://ai-sdk.dev/docs/ai-sdk-core/provider-options). See the [`WorkflowAgent` docs](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent).
|
|
30
31
|
|
|
31
32
|
## Multi-turn pattern
|
|
32
33
|
|
|
33
34
|
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
35
|
|
|
36
|
+
<Callout type="info">
|
|
37
|
+
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.
|
|
38
|
+
</Callout>
|
|
39
|
+
|
|
35
40
|
<Tabs items={['Workflow', 'API Route', 'Client']}>
|
|
36
41
|
|
|
37
42
|
<Tab value="Workflow">
|
|
@@ -48,14 +53,15 @@ export const turnHook = defineHook({ // [!code highlight]
|
|
|
48
53
|
schema: z.object({ message: z.string() }),
|
|
49
54
|
});
|
|
50
55
|
|
|
56
|
+
// `streamText` runs tool executes inside `runTurn` (a step), so tool calls
|
|
57
|
+
// are not individually durable — the entire turn retries together. See
|
|
58
|
+
// "Tools are not individually durable" below. Make side-effectful tools idempotent.
|
|
51
59
|
async function lookupOrder({ orderId }: { orderId: string }) {
|
|
52
|
-
"use step";
|
|
53
60
|
const res = await fetch(`https://api.store.com/orders/${orderId}`);
|
|
54
61
|
return res.json();
|
|
55
62
|
}
|
|
56
63
|
|
|
57
64
|
async function processRefund({ orderId, reason }: { orderId: string; reason: string }) {
|
|
58
|
-
"use step";
|
|
59
65
|
const res = await fetch("https://api.store.com/refunds", {
|
|
60
66
|
method: "POST",
|
|
61
67
|
body: JSON.stringify({ orderId, reason }),
|
|
@@ -124,6 +130,10 @@ export async function supportWorkflow(initialMessages: ModelMessage[]) {
|
|
|
124
130
|
|
|
125
131
|
One endpoint handles first turn, follow-ups, and the `/done` exit. The client sends `runId` in the body to distinguish first vs follow-up.
|
|
126
132
|
|
|
133
|
+
<Callout type="info">
|
|
134
|
+
The first turn calls `start()` and then returns the `runId`. If your client or platform can retry that first request before it receives and stores the `runId`, use an atomic conversation or request key before starting the workflow. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
|
|
135
|
+
</Callout>
|
|
136
|
+
|
|
127
137
|
```typescript title="app/api/support/route.ts" lineNumbers
|
|
128
138
|
import type { UIMessage, UIMessageChunk } from "ai";
|
|
129
139
|
import { convertToModelMessages, createUIMessageStreamResponse } from "ai";
|
|
@@ -240,7 +250,7 @@ Store the `runId` in a ref and pass it in the body of every follow-up. `Workflow
|
|
|
240
250
|
"use client";
|
|
241
251
|
|
|
242
252
|
import { useChat } from "@ai-sdk/react";
|
|
243
|
-
import { WorkflowChatTransport } from "@workflow
|
|
253
|
+
import { WorkflowChatTransport } from "@ai-sdk/workflow";
|
|
244
254
|
import { useMemo, useRef, useState } from "react";
|
|
245
255
|
|
|
246
256
|
export function SupportChat() {
|
|
@@ -290,16 +300,32 @@ export function SupportChat() {
|
|
|
290
300
|
## How it works
|
|
291
301
|
|
|
292
302
|
1. **One workflow = one conversation.** The workflow loops on a hook, keeping `allMessages`, tool history, and state alive across turns.
|
|
293
|
-
2.
|
|
294
|
-
3.
|
|
295
|
-
4. **`
|
|
296
|
-
5. **`
|
|
297
|
-
6.
|
|
303
|
+
2. **`runTurn` is the durability boundary.** Each turn is one step. The model request and all tool calls inside it run as plain inline functions within that step. If anything throws mid-turn, the whole `runTurn` retries — individual tool calls are not separately durable. See [Pitfalls](#tools-are-not-individually-durable).
|
|
304
|
+
3. **Hook is created once.** `turnHook.create({ token: workflowRunId })` outside the loop — calling it twice with the same token throws `HookConflictError`.
|
|
305
|
+
4. **`preventClose: true`** on `pipeTo` keeps the durable writable open so the next turn can write to it.
|
|
306
|
+
5. **`sliceUntilFinish`** in the API reads chunks until `type === "finish"`, then closes the HTTP response. The source reader is released — not cancelled — so the workflow stream keeps flowing.
|
|
307
|
+
6. **`startIndex: tailIndex + 1`** gives each follow-up response only the new chunks, avoiding replay of previous turns.
|
|
308
|
+
7. **`/done`** resumes the hook so the workflow exits cleanly, then returns a synthetic `start` + `finish` so `useChat` transitions out of "streaming".
|
|
298
309
|
|
|
299
310
|
## Pitfalls
|
|
300
311
|
|
|
301
312
|
Non-obvious correctness details worth knowing before adapting this pattern.
|
|
302
313
|
|
|
314
|
+
### Tools are not individually durable
|
|
315
|
+
|
|
316
|
+
`streamText()` is invoked from inside `runTurn` (a `"use step"` function), and the AI SDK calls each tool by directly invoking its `execute` function in that same step. Even if a tool body has its own `"use step"` directive, that directive is a [no-op when called from another step](/docs/foundations/workflows-and-steps#step-functions) — the function just runs inline.
|
|
317
|
+
|
|
318
|
+
The consequences:
|
|
319
|
+
|
|
320
|
+
- The atomic retry unit is the entire `runTurn`, not the individual tool call.
|
|
321
|
+
- If `processRefund` succeeds and then the model call (or a later tool) throws, the whole turn retries, and `processRefund` will run again.
|
|
322
|
+
- Tool calls do not appear as separate entries in the event log or observability dashboard.
|
|
323
|
+
|
|
324
|
+
**Mitigations:**
|
|
325
|
+
|
|
326
|
+
- Make side-effectful tool implementations idempotent — dedupe server-side on a stable key (e.g. `orderId`, an `Idempotency-Key` header, etc.).
|
|
327
|
+
- Or use AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent), which runs tools at workflow scope — each tool can be marked `"use step"` to become its own durable, retryable step, or stay at workflow level to use primitives like `sleep()` and hooks.
|
|
328
|
+
|
|
303
329
|
### Snapshot `tailIndex` *before* resuming the hook
|
|
304
330
|
|
|
305
331
|
{/* @skip-typecheck - fragment referencing variables from the surrounding multi-turn pattern */}
|
|
@@ -328,33 +354,39 @@ In `sliceUntilFinish`, use `reader.releaseLock()` in the `finally` block rather
|
|
|
328
354
|
|
|
329
355
|
Clients can send a `runId` from a long-gone workflow (localStorage, back button, server restart). Wrap the follow-up path in a try/catch for `not found` / `expired` and fall through to the first-turn code path to start a fresh workflow.
|
|
330
356
|
|
|
331
|
-
|
|
357
|
+
### Make the first turn idempotent when needed
|
|
358
|
+
|
|
359
|
+
This example stores the `runId` after the first response. For strict one-session-per-thread behavior, use a deterministic hook token derived from the thread ID or conversation ID and route retries through the active hook. See [Idempotency](/docs/foundations/idempotency).
|
|
360
|
+
|
|
361
|
+
## streamText vs WorkflowAgent
|
|
332
362
|
|
|
333
|
-
| | `streamText()` | `
|
|
363
|
+
| | `streamText()` (this pattern) | `WorkflowAgent` |
|
|
334
364
|
|---|---|---|
|
|
335
|
-
| **Tool loop** | AI SDK handles via `stopWhen` |
|
|
336
|
-
| **LLM call durability** | Re-executes
|
|
337
|
-
| **
|
|
338
|
-
| **
|
|
339
|
-
| **
|
|
340
|
-
| **
|
|
365
|
+
| **Tool loop** | AI SDK handles via `stopWhen` | Handles internally (AI SDK–compatible options) |
|
|
366
|
+
| **LLM call durability** | Re-executes with the parent turn | Each LLM call is a durable step |
|
|
367
|
+
| **Tool call durability** | Not individually durable — re-executes with the parent turn | Per tool — mark `"use step"` for a durable, retryable step, or keep at workflow level for `sleep()` / hooks |
|
|
368
|
+
| **Stop conditions** | `stopWhen`, `prepareStep` | `stopWhen`, `prepareStep` |
|
|
369
|
+
| **Structured output** | `Output.object()`, `Output.array()` | `output` (`Output.object()`, `Output.text()`) |
|
|
370
|
+
| **Step callbacks** | `onStepFinish`, `onChunk`, etc. | `onStepFinish`, `onFinish`, `onError`, `onAbort` (`onChunk` not available) |
|
|
371
|
+
| **Setup** | Manual stream piping and turn slicing | Automatic |
|
|
341
372
|
|
|
342
|
-
Use `
|
|
373
|
+
Use `WorkflowAgent` for most agent use cases. Use `streamText` when you need the raw AI SDK surface or a per-turn durability boundary.
|
|
343
374
|
|
|
344
375
|
## Key APIs
|
|
345
376
|
|
|
346
377
|
**AI SDK** ([docs](https://ai-sdk.dev/docs))
|
|
347
378
|
|
|
348
379
|
* [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) — core streaming function; `toUIMessageStream()` pipes into the durable writable
|
|
349
|
-
* [`tool()` / tool calling](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling) — tools
|
|
380
|
+
* [`tool()` / tool calling](https://ai-sdk.dev/docs/ai-sdk-core/tools-and-tool-calling) — tools are plain async functions invoked by `streamText` inside the turn step; they are **not** individually durable in this pattern (see [Pitfalls](#tools-are-not-individually-durable))
|
|
350
381
|
* [`stepCountIs()` / `stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/agents#stop-conditions) — bound the agent loop inside each turn
|
|
351
382
|
* [`convertToModelMessages()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/convert-to-model-messages) / [`createUIMessageStreamResponse()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/create-ui-message-stream-response) — UI ↔ model message conversion at the API boundary
|
|
352
383
|
* [`useChat()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) — React hook that consumes the UI message stream on the client
|
|
353
384
|
|
|
354
385
|
**Workflow SDK**
|
|
355
386
|
|
|
356
|
-
* [`"use step"`](/docs/
|
|
387
|
+
* [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) — applied to `runTurn` to make each turn a durable, retryable unit
|
|
357
388
|
* [`defineHook()`](/docs/api-reference/workflow/define-hook) — suspension point for follow-up messages
|
|
358
389
|
* [`getWritable()`](/docs/api-reference/workflow/get-writable) — resumable stream output
|
|
359
390
|
* [`getRun()`](/docs/api-reference/workflow-api/get-run) — `run.getReadable({ startIndex })` for slicing per-turn streams
|
|
360
391
|
* [`WorkflowChatTransport`](/docs/api-reference/workflow-ai/workflow-chat-transport) — passes `runId` between turns
|
|
392
|
+
* [Idempotency](/docs/foundations/idempotency) — protect duplicate-sensitive first turns and side effects
|
|
@@ -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
|
|
@@ -172,6 +176,10 @@ export type ChatTurnPayload = {
|
|
|
172
176
|
|
|
173
177
|
Handlers live outside the workflow file so adapter dependencies don't leak in. They decide whether to start a new workflow or resume an existing one, then store the `runId` in thread state:
|
|
174
178
|
|
|
179
|
+
<Callout type="info">
|
|
180
|
+
If the platform can deliver the same first message concurrently, use a deterministic hook token derived from the thread ID so duplicate handlers route to the active chat session hook. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
|
|
181
|
+
</Callout>
|
|
182
|
+
|
|
175
183
|
```typescript title="lib/chat-session-handlers.ts" lineNumbers
|
|
176
184
|
import type { Message, Thread } from "chat";
|
|
177
185
|
import { getRun, resumeHook, start } from "workflow/api";
|
|
@@ -285,6 +293,10 @@ Adapter packages (`@chat-adapter/slack`, `@chat-adapter/telegram`, etc.) depend
|
|
|
285
293
|
|
|
286
294
|
A workflow run ends but its `runId` is still cached in thread state. The next message calls `resumeHook` on a dead run and throws `not found` / `expired`. Gate on `getRun(runId).exists` before resuming, or catch the error and fall through to `startSession`. Either way the user's message must not be dropped.
|
|
287
295
|
|
|
296
|
+
### Make first-message routing atomic
|
|
297
|
+
|
|
298
|
+
Thread state is also the idempotency boundary for starting sessions. Back it with a state adapter or database operation that can atomically claim the thread before `startSession()` runs when duplicate sessions would be harmful.
|
|
299
|
+
|
|
288
300
|
### Keep the hook outside the loop
|
|
289
301
|
|
|
290
302
|
One `chatTurnHook.create({ token: workflowRunId })` per workflow run, reused every iteration. Creating a new hook with the same token throws `HookConflictError`. This is the same rule as the [AI SDK](/docs/cookbook/integrations/ai-sdk) and [Sandbox](/docs/cookbook/integrations/sandbox) session patterns.
|
|
@@ -301,3 +313,4 @@ Slack wants a 200 within 3 seconds. The webhook handler returns immediately afte
|
|
|
301
313
|
- [`getRun()`](/docs/api-reference/workflow-api/get-run) — `run.exists` before resuming, to detect stale `runId`s.
|
|
302
314
|
- [`defineHook()`](/docs/api-reference/workflow/define-hook) — per-turn suspension point inside the workflow.
|
|
303
315
|
- [`registerSingleton()`](https://chat-sdk.dev/docs/api/chat) — makes the bot resolvable from inside step functions.
|
|
316
|
+
- [Idempotency](/docs/foundations/idempotency) — protect duplicate-sensitive first messages and side effects.
|
|
@@ -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.
|
|
@@ -296,6 +300,10 @@ export async function sandboxSessionWorkflow() {
|
|
|
296
300
|
|
|
297
301
|
Two endpoints. `/start` accepts an optional `{ runId }` — if the run still exists, it replays the event log from index 0 so a returning client fully rehydrates. `/command` resumes the hook and returns immediately; command output lands on the `/start` stream.
|
|
298
302
|
|
|
303
|
+
<Callout type="info">
|
|
304
|
+
This example starts a fresh sandbox session when no `runId` is provided. If your product needs one sandbox session per user, project, or task, use a deterministic hook token derived from that session key and route retries through the active hook. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
|
|
305
|
+
</Callout>
|
|
306
|
+
|
|
299
307
|
```typescript title="app/api/sandbox/start/route.ts" lineNumbers
|
|
300
308
|
import { start, getRun } from "workflow/api";
|
|
301
309
|
import { sandboxSessionWorkflow } from "@/workflows/sandbox-session";
|
|
@@ -501,6 +509,10 @@ Stream closure must happen inside a `"use step"` function. Calling `writable.clo
|
|
|
501
509
|
|
|
502
510
|
Clients can hold `runId`s from long-gone workflow runs (localStorage, back button, server restart). Gate the reconnect path on `run.exists` and fall through to starting fresh. On `hook.resume`, catch `not found` / `expired` and return 410 so the client clears its state.
|
|
503
511
|
|
|
512
|
+
### Decide whether `/start` should be idempotent
|
|
513
|
+
|
|
514
|
+
The sample treats a missing or stale `runId` as a request for a new session. For one-session-per-resource behavior, use a durable resource key, such as `projectId` or `taskId`, to claim or retrieve the run before starting a new one.
|
|
515
|
+
|
|
504
516
|
### Keep the hook outside the loop
|
|
505
517
|
|
|
506
518
|
Each iteration's `hook.then(...)` attaches a listener to the same hook instance. Creating a new hook per iteration with the same token throws `HookConflictError`. One hook, one token (`workflowRunId`), reused every iteration.
|
|
@@ -514,3 +526,4 @@ Each iteration's `hook.then(...)` attaches a listener to the same hook instance.
|
|
|
514
526
|
- [`sleep()`](/docs/api-reference/workflow/sleep) — durable timer that powers both idle hibernation and proactive refresh
|
|
515
527
|
- [`getRun()`](/docs/api-reference/workflow-api/get-run) — look up a run and replay its event log for reconnection
|
|
516
528
|
- [`getWritable()`](/docs/api-reference/workflow/get-writable) — resumable NDJSON event stream
|
|
529
|
+
- [Idempotency](/docs/foundations/idempotency) — choose when `/start` should reuse an existing run
|
|
@@ -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
|
|
package/docs/deploying/index.mdx
CHANGED
|
@@ -4,6 +4,7 @@ icon: Rocket
|
|
|
4
4
|
description: Deploy workflows locally, on Vercel, or anywhere using pluggable World adapters.
|
|
5
5
|
type: overview
|
|
6
6
|
summary: Learn how to deploy workflows to different environments using World adapters.
|
|
7
|
+
manualCards: true
|
|
7
8
|
related:
|
|
8
9
|
- /docs/deploying/world/local-world
|
|
9
10
|
- /docs/deploying/world/postgres-world
|