workflow 5.0.0-beta.4 → 5.0.0-beta.40
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +63 -20
- package/dist/api-workflow.d.ts +1 -1
- package/dist/api-workflow.d.ts.map +1 -1
- package/dist/api-workflow.js +1 -1
- package/dist/api.d.ts +5 -1
- package/dist/api.d.ts.map +1 -1
- package/dist/api.js +12 -2
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +6 -1
- package/dist/internal/builtins.d.ts +17 -0
- package/dist/internal/builtins.d.ts.map +1 -1
- package/dist/internal/builtins.js +65 -1
- package/dist/internal/errors.d.ts +1 -1
- package/dist/internal/errors.d.ts.map +1 -1
- package/dist/internal/errors.js +2 -2
- package/dist/nest-builder.d.ts +2 -0
- package/dist/nest-builder.d.ts.map +1 -0
- package/dist/nest-builder.js +2 -0
- package/dist/nest-vercel-builder.d.ts +2 -0
- package/dist/nest-vercel-builder.d.ts.map +1 -0
- package/dist/nest-vercel-builder.js +2 -0
- package/dist/observability.d.ts +1 -1
- package/dist/observability.js +2 -2
- package/dist/runtime.d.ts +2 -1
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +5 -2
- package/docs/ai/chat-session-modeling.mdx +7 -3
- package/docs/ai/defining-tools.mdx +2 -2
- package/docs/ai/index.mdx +30 -25
- package/docs/ai/message-queueing.mdx +10 -10
- package/docs/ai/resumable-streams.mdx +9 -1
- package/docs/api-reference/index.mdx +24 -0
- package/docs/api-reference/meta.json +8 -0
- package/docs/api-reference/vitest/index.mdx +28 -7
- package/docs/api-reference/workflow/create-hook.mdx +79 -0
- package/docs/api-reference/workflow/create-webhook.mdx +1 -0
- package/docs/api-reference/workflow/define-hook.mdx +26 -24
- package/docs/api-reference/workflow/fatal-error.mdx +29 -7
- package/docs/api-reference/workflow/fetch.mdx +8 -4
- package/docs/api-reference/workflow/index.mdx +3 -0
- package/docs/api-reference/workflow/set-attributes.mdx +61 -0
- package/docs/api-reference/workflow/sleep.mdx +1 -1
- package/docs/api-reference/workflow-ai/durable-agent.mdx +7 -45
- package/docs/api-reference/workflow-ai/index.mdx +2 -2
- package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +44 -1
- package/docs/api-reference/workflow-api/get-hook-by-token.mdx +9 -0
- package/docs/api-reference/workflow-api/get-run.mdx +25 -0
- package/docs/api-reference/workflow-api/index.mdx +6 -8
- package/docs/api-reference/workflow-api/resume-hook.mdx +60 -1
- package/docs/api-reference/workflow-api/resume-webhook.mdx +6 -4
- package/docs/api-reference/workflow-api/start.mdx +39 -6
- package/docs/api-reference/workflow-astro/index.mdx +18 -0
- package/docs/api-reference/workflow-astro/meta.json +4 -0
- package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
- package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
- package/docs/api-reference/workflow-errors/index.mdx +88 -0
- package/docs/api-reference/workflow-errors/meta.json +6 -0
- package/docs/api-reference/workflow-errors/precondition-failed-error.mdx +68 -0
- package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
- package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
- package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
- package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
- package/docs/api-reference/workflow-globals.mdx +4 -1
- package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
- package/docs/api-reference/workflow-nest/index.mdx +31 -0
- package/docs/api-reference/workflow-nest/meta.json +9 -0
- package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
- package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
- package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
- package/docs/api-reference/workflow-next/with-workflow.mdx +56 -2
- package/docs/api-reference/workflow-nitro/index.mdx +60 -0
- package/docs/api-reference/workflow-nuxt/index.mdx +48 -0
- package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
- package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
- package/docs/api-reference/workflow-observability/index.mdx +64 -0
- package/docs/api-reference/workflow-observability/meta.json +11 -0
- package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
- package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
- package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
- package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
- package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
- package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +7 -10
- package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
- package/docs/api-reference/workflow-runtime/index.mdx +43 -0
- package/docs/api-reference/workflow-runtime/meta.json +12 -0
- package/docs/api-reference/workflow-runtime/set-world.mdx +49 -0
- package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +42 -0
- package/docs/api-reference/workflow-runtime/world/analytics.mdx +138 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/index.mdx +10 -10
- package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/queue.mdx +8 -6
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +18 -5
- package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +2 -2
- package/docs/api-reference/workflow-serde/index.mdx +0 -1
- package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +1 -2
- package/docs/api-reference/workflow-serde/workflow-serialize.mdx +2 -2
- package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
- package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
- package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
- package/docs/api-reference/workflow-vite/index.mdx +18 -0
- package/docs/api-reference/workflow-vite/meta.json +4 -0
- package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
- package/docs/changelog/attributes-mvp.mdx +380 -0
- package/docs/changelog/eager-processing.mdx +269 -0
- package/docs/changelog/index.mdx +3 -1
- package/docs/changelog/lazy-event-creation.md +127 -0
- package/docs/changelog/meta.json +9 -1
- package/docs/changelog/resilient-resume.mdx +22 -0
- package/docs/changelog/resilient-start.mdx +31 -283
- package/docs/changelog/step-message-ownership.mdx +360 -0
- package/docs/changelog/turbo-mode.md +87 -0
- package/docs/comparisons/index.mdx +66 -0
- package/docs/comparisons/meta.json +11 -0
- package/docs/comparisons/workflow-sdk-vs-aws-agentcore.mdx +55 -0
- package/docs/comparisons/workflow-sdk-vs-aws-step-functions.mdx +111 -0
- package/docs/comparisons/workflow-sdk-vs-cloudflare-workflows.mdx +71 -0
- package/docs/comparisons/workflow-sdk-vs-inngest.mdx +102 -0
- package/docs/comparisons/workflow-sdk-vs-temporal.mdx +123 -0
- package/docs/comparisons/workflow-sdk-vs-trigger-dev.mdx +103 -0
- package/docs/configuration/build-and-diagnostics.mdx +70 -0
- package/docs/configuration/cli-and-web-ui.mdx +182 -0
- package/docs/configuration/framework-options.mdx +165 -0
- package/docs/configuration/index.mdx +32 -0
- package/docs/configuration/meta.json +12 -0
- package/docs/configuration/runtime-tuning.mdx +276 -0
- package/docs/configuration/worlds.mdx +275 -0
- package/docs/cookbook/advanced/child-workflows.mdx +203 -256
- package/docs/cookbook/advanced/meta.json +1 -1
- package/docs/cookbook/advanced/publishing-libraries.mdx +33 -24
- package/docs/cookbook/advanced/serializable-steps.mdx +12 -4
- package/docs/cookbook/advanced/upgrading-workflows.mdx +199 -0
- package/docs/cookbook/agent-patterns/agent-cancellation.mdx +34 -75
- package/docs/cookbook/agent-patterns/durable-agent.mdx +14 -142
- package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +11 -3
- package/docs/cookbook/common-patterns/batching.mdx +4 -0
- package/docs/cookbook/common-patterns/idempotency.mdx +40 -52
- package/docs/cookbook/common-patterns/rate-limiting.mdx +5 -1
- package/docs/cookbook/common-patterns/saga.mdx +6 -2
- package/docs/cookbook/common-patterns/scheduling.mdx +12 -0
- package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +4 -0
- package/docs/cookbook/common-patterns/timeouts.mdx +6 -1
- package/docs/cookbook/common-patterns/webhooks.mdx +4 -0
- package/docs/cookbook/common-patterns/workflow-composition.mdx +19 -16
- package/docs/cookbook/index.mdx +3 -3
- package/docs/cookbook/integrations/ai-sdk.mdx +64 -28
- package/docs/cookbook/integrations/chat-sdk.mdx +17 -0
- package/docs/cookbook/integrations/sandbox.mdx +17 -0
- package/docs/{deploying/index.mdx → deploying.mdx} +7 -7
- package/docs/errors/abort-signal-timeout-in-workflow.mdx +84 -0
- package/docs/errors/corrupted-event-log.mdx +5 -5
- package/docs/errors/deployment-mismatch.mdx +71 -0
- package/docs/errors/fetch-in-workflow.mdx +4 -0
- package/docs/errors/hook-conflict.mdx +60 -4
- package/docs/errors/index.mdx +1 -35
- package/docs/errors/node-js-module-in-workflow.mdx +4 -0
- package/docs/errors/replay-divergence.mdx +27 -0
- package/docs/errors/runtime-decryption-failed.mdx +77 -0
- package/docs/errors/serialization-failed.mdx +4 -0
- package/docs/errors/start-invalid-workflow-function.mdx +4 -0
- package/docs/errors/step-executed-multiple-times.mdx +23 -0
- package/docs/errors/step-not-registered.mdx +1 -1
- package/docs/errors/timeout-in-workflow.mdx +4 -0
- package/docs/errors/webhook-response-not-sent.mdx +4 -0
- package/docs/foundations/cancellation.mdx +459 -0
- package/docs/foundations/errors-and-retries.mdx +7 -3
- package/docs/foundations/hooks.mdx +29 -0
- package/docs/foundations/idempotency.mdx +243 -11
- package/docs/foundations/index.mdx +1 -23
- package/docs/foundations/meta.json +3 -1
- package/docs/foundations/serialization.mdx +77 -41
- package/docs/foundations/starting-workflows.mdx +79 -2
- package/docs/foundations/streaming.mdx +14 -23
- package/docs/foundations/versioning.mdx +263 -0
- package/docs/getting-started/astro.mdx +12 -2
- package/docs/getting-started/express.mdx +6 -2
- package/docs/getting-started/fastify.mdx +6 -2
- package/docs/getting-started/hono.mdx +6 -2
- package/docs/getting-started/index.mdx +16 -10
- package/docs/getting-started/meta.json +4 -1
- package/docs/getting-started/nestjs.mdx +77 -3
- package/docs/getting-started/next.mdx +12 -6
- package/docs/getting-started/nitro.mdx +28 -2
- package/docs/getting-started/nuxt.mdx +6 -2
- package/docs/getting-started/python.mdx +24 -18
- package/docs/getting-started/react-router/index.mdx +33 -0
- package/docs/getting-started/react-router/meta.json +5 -0
- package/docs/getting-started/react-router/v7.mdx +237 -0
- package/docs/getting-started/react-router/v8.mdx +232 -0
- package/docs/getting-started/sveltekit.mdx +12 -2
- package/docs/getting-started/tanstack-start.mdx +245 -0
- package/docs/getting-started/vite.mdx +6 -2
- package/docs/how-it-works/cancellation.mdx +287 -0
- package/docs/how-it-works/code-transform.mdx +21 -17
- package/docs/how-it-works/encryption.mdx +5 -5
- package/docs/how-it-works/event-sourcing.mdx +6 -6
- package/docs/how-it-works/framework-integrations.mdx +96 -337
- package/docs/how-it-works/meta.json +2 -1
- package/docs/internal/index.mdx +21 -0
- package/docs/internal/meta.json +10 -0
- package/docs/internal/nitro-native-build.mdx +38 -0
- package/docs/internal/nitro-web-ui.mdx +24 -0
- package/docs/internal/serializable-abort-controller.mdx +148 -0
- package/docs/meta.json +2 -2
- package/docs/observability/attributes.mdx +112 -0
- package/docs/observability/index.mdx +20 -1
- package/docs/observability/meta.json +1 -1
- package/docs/observability/tracing.mdx +124 -0
- package/docs/testing/index.mdx +2 -2
- package/package.json +17 -13
- package/docs/api-reference/workflow-api/world/meta.json +0 -4
- package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
- package/docs/cookbook/advanced/distributed-abort-controller.mdx +0 -318
- package/docs/deploying/building-a-world.mdx +0 -251
- package/docs/deploying/meta.json +0 -4
- package/docs/deploying/world/local-world.mdx +0 -84
- package/docs/deploying/world/meta.json +0 -4
- package/docs/deploying/world/postgres-world.mdx +0 -222
- package/docs/deploying/world/vercel-world.mdx +0 -179
- package/docs/migration-guides/index.mdx +0 -34
- package/docs/migration-guides/meta.json +0 -9
- package/docs/migration-guides/migrating-from-aws-step-functions.mdx +0 -363
- package/docs/migration-guides/migrating-from-inngest.mdx +0 -314
- package/docs/migration-guides/migrating-from-temporal.mdx +0 -318
- package/docs/migration-guides/migrating-from-trigger-dev.mdx +0 -337
|
@@ -1,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,36 @@ related:
|
|
|
11
11
|
- /docs/api-reference/workflow-ai/durable-agent
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
-
|
|
14
|
+
<CopyPrompt
|
|
15
|
+
text="Implement the durable AI SDK multi-turn pattern. Use `streamText`, `stepCountIs`, and `createUIMessageStreamResponse` from `ai`; `defineHook`, `getWritable`, and `getWorkflowMetadata` from `workflow`; and `start`/`getRun` from `workflow/api`. Put the model call in a `"use step"` function such as `runTurn(messages)` and pipe `result.toUIMessageStream()` to `getWritable<UIMessageChunk>()` with `{ preventClose: true }`. In the workflow, create one hook with `turnHook.create({ token: workflowRunId })`, loop over turns, and await the hook between user messages. Add an API route that starts a run on first message, stores/returns the run ID in `x-workflow-run-id`, resumes the hook for follow-up messages, reads from `run.getReadable({ startIndex })`, and handles stale run IDs by starting fresh. Wire the client transport to send `runId` with each request and verify first turn, follow-up turn, `/done`, and reconnect behavior."
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
[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
19
|
|
|
16
20
|
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
21
|
|
|
18
22
|
<Callout type="info">
|
|
19
|
-
For most agent use cases, prefer [`
|
|
23
|
+
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
24
|
</Callout>
|
|
21
25
|
|
|
22
26
|
## When to use streamText directly
|
|
23
27
|
|
|
24
|
-
Use [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) instead of `
|
|
28
|
+
Use [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) instead of `WorkflowAgent` when you need:
|
|
29
|
+
|
|
30
|
+
* **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()`
|
|
31
|
+
* **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
|
|
32
|
+
* **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`
|
|
25
33
|
|
|
26
|
-
|
|
27
|
-
* **Structured output** — [`Output.object()`](https://ai-sdk.dev/docs/ai-sdk-core/generating-structured-data) or `Output.array()` alongside tool calling
|
|
28
|
-
* **Step-level callbacks** — `onStepFinish` for logging, metrics, or branching logic
|
|
29
|
-
* **Provider options** — per-step model switching, reasoning budgets, or custom [provider options](https://ai-sdk.dev/docs/ai-sdk-core/provider-options)
|
|
34
|
+
`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
35
|
|
|
31
36
|
## Multi-turn pattern
|
|
32
37
|
|
|
33
38
|
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
39
|
|
|
40
|
+
<Callout type="info">
|
|
41
|
+
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.
|
|
42
|
+
</Callout>
|
|
43
|
+
|
|
35
44
|
<Tabs items={['Workflow', 'API Route', 'Client']}>
|
|
36
45
|
|
|
37
46
|
<Tab value="Workflow">
|
|
@@ -48,14 +57,15 @@ export const turnHook = defineHook({ // [!code highlight]
|
|
|
48
57
|
schema: z.object({ message: z.string() }),
|
|
49
58
|
});
|
|
50
59
|
|
|
60
|
+
// `streamText` runs tool executes inside `runTurn` (a step), so tool calls
|
|
61
|
+
// are not individually durable — the entire turn retries together. See
|
|
62
|
+
// "Tools are not individually durable" below. Make side-effectful tools idempotent.
|
|
51
63
|
async function lookupOrder({ orderId }: { orderId: string }) {
|
|
52
|
-
"use step";
|
|
53
64
|
const res = await fetch(`https://api.store.com/orders/${orderId}`);
|
|
54
65
|
return res.json();
|
|
55
66
|
}
|
|
56
67
|
|
|
57
68
|
async function processRefund({ orderId, reason }: { orderId: string; reason: string }) {
|
|
58
|
-
"use step";
|
|
59
69
|
const res = await fetch("https://api.store.com/refunds", {
|
|
60
70
|
method: "POST",
|
|
61
71
|
body: JSON.stringify({ orderId, reason }),
|
|
@@ -124,6 +134,10 @@ export async function supportWorkflow(initialMessages: ModelMessage[]) {
|
|
|
124
134
|
|
|
125
135
|
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
136
|
|
|
137
|
+
<Callout type="info">
|
|
138
|
+
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).
|
|
139
|
+
</Callout>
|
|
140
|
+
|
|
127
141
|
```typescript title="app/api/support/route.ts" lineNumbers
|
|
128
142
|
import type { UIMessage, UIMessageChunk } from "ai";
|
|
129
143
|
import { convertToModelMessages, createUIMessageStreamResponse } from "ai";
|
|
@@ -240,7 +254,7 @@ Store the `runId` in a ref and pass it in the body of every follow-up. `Workflow
|
|
|
240
254
|
"use client";
|
|
241
255
|
|
|
242
256
|
import { useChat } from "@ai-sdk/react";
|
|
243
|
-
import { WorkflowChatTransport } from "@workflow
|
|
257
|
+
import { WorkflowChatTransport } from "@ai-sdk/workflow";
|
|
244
258
|
import { useMemo, useRef, useState } from "react";
|
|
245
259
|
|
|
246
260
|
export function SupportChat() {
|
|
@@ -290,16 +304,32 @@ export function SupportChat() {
|
|
|
290
304
|
## How it works
|
|
291
305
|
|
|
292
306
|
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.
|
|
307
|
+
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).
|
|
308
|
+
3. **Hook is created once.** `turnHook.create({ token: workflowRunId })` outside the loop — calling it twice with the same token throws `HookConflictError`.
|
|
309
|
+
4. **`preventClose: true`** on `pipeTo` keeps the durable writable open so the next turn can write to it.
|
|
310
|
+
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.
|
|
311
|
+
6. **`startIndex: tailIndex + 1`** gives each follow-up response only the new chunks, avoiding replay of previous turns.
|
|
312
|
+
7. **`/done`** resumes the hook so the workflow exits cleanly, then returns a synthetic `start` + `finish` so `useChat` transitions out of "streaming".
|
|
298
313
|
|
|
299
314
|
## Pitfalls
|
|
300
315
|
|
|
301
316
|
Non-obvious correctness details worth knowing before adapting this pattern.
|
|
302
317
|
|
|
318
|
+
### Tools are not individually durable
|
|
319
|
+
|
|
320
|
+
`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.
|
|
321
|
+
|
|
322
|
+
The consequences:
|
|
323
|
+
|
|
324
|
+
- The atomic retry unit is the entire `runTurn`, not the individual tool call.
|
|
325
|
+
- If `processRefund` succeeds and then the model call (or a later tool) throws, the whole turn retries, and `processRefund` will run again.
|
|
326
|
+
- Tool calls do not appear as separate entries in the event log or observability dashboard.
|
|
327
|
+
|
|
328
|
+
**Mitigations:**
|
|
329
|
+
|
|
330
|
+
- Make side-effectful tool implementations idempotent — dedupe server-side on a stable key (e.g. `orderId`, an `Idempotency-Key` header, etc.).
|
|
331
|
+
- 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.
|
|
332
|
+
|
|
303
333
|
### Snapshot `tailIndex` *before* resuming the hook
|
|
304
334
|
|
|
305
335
|
{/* @skip-typecheck - fragment referencing variables from the surrounding multi-turn pattern */}
|
|
@@ -328,33 +358,39 @@ In `sliceUntilFinish`, use `reader.releaseLock()` in the `finally` block rather
|
|
|
328
358
|
|
|
329
359
|
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
360
|
|
|
331
|
-
|
|
361
|
+
### Make the first turn idempotent when needed
|
|
362
|
+
|
|
363
|
+
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).
|
|
364
|
+
|
|
365
|
+
## streamText vs WorkflowAgent
|
|
332
366
|
|
|
333
|
-
| | `streamText()` | `
|
|
367
|
+
| | `streamText()` (this pattern) | `WorkflowAgent` |
|
|
334
368
|
|---|---|---|
|
|
335
|
-
| **Tool loop** | AI SDK handles via `stopWhen` |
|
|
336
|
-
| **LLM call durability** | Re-executes
|
|
337
|
-
| **
|
|
338
|
-
| **
|
|
339
|
-
| **
|
|
340
|
-
| **
|
|
369
|
+
| **Tool loop** | AI SDK handles via `stopWhen` | Handles internally (AI SDK–compatible options) |
|
|
370
|
+
| **LLM call durability** | Re-executes with the parent turn | Each LLM call is a durable step |
|
|
371
|
+
| **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 |
|
|
372
|
+
| **Stop conditions** | `stopWhen`, `prepareStep` | `stopWhen`, `prepareStep` |
|
|
373
|
+
| **Structured output** | `Output.object()`, `Output.array()` | `output` (`Output.object()`, `Output.text()`) |
|
|
374
|
+
| **Step callbacks** | `onStepFinish`, `onChunk`, etc. | `onStepFinish`, `onFinish`, `onError`, `onAbort` (`onChunk` not available) |
|
|
375
|
+
| **Setup** | Manual stream piping and turn slicing | Automatic |
|
|
341
376
|
|
|
342
|
-
Use `
|
|
377
|
+
Use `WorkflowAgent` for most agent use cases. Use `streamText` when you need the raw AI SDK surface or a per-turn durability boundary.
|
|
343
378
|
|
|
344
379
|
## Key APIs
|
|
345
380
|
|
|
346
381
|
**AI SDK** ([docs](https://ai-sdk.dev/docs))
|
|
347
382
|
|
|
348
383
|
* [`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
|
|
384
|
+
* [`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
385
|
* [`stepCountIs()` / `stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/agents#stop-conditions) — bound the agent loop inside each turn
|
|
351
386
|
* [`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
387
|
* [`useChat()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) — React hook that consumes the UI message stream on the client
|
|
353
388
|
|
|
354
389
|
**Workflow SDK**
|
|
355
390
|
|
|
356
|
-
* [`"use step"`](/docs/
|
|
391
|
+
* [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) — applied to `runTurn` to make each turn a durable, retryable unit
|
|
357
392
|
* [`defineHook()`](/docs/api-reference/workflow/define-hook) — suspension point for follow-up messages
|
|
358
393
|
* [`getWritable()`](/docs/api-reference/workflow/get-writable) — resumable stream output
|
|
359
394
|
* [`getRun()`](/docs/api-reference/workflow-api/get-run) — `run.getReadable({ startIndex })` for slicing per-turn streams
|
|
360
395
|
* [`WorkflowChatTransport`](/docs/api-reference/workflow-ai/workflow-chat-transport) — passes `runId` between turns
|
|
396
|
+
* [Idempotency](/docs/foundations/idempotency) — protect duplicate-sensitive first turns and side effects
|
|
@@ -11,6 +11,10 @@ related:
|
|
|
11
11
|
- /docs/api-reference/workflow-api/get-run
|
|
12
12
|
---
|
|
13
13
|
|
|
14
|
+
<CopyPrompt
|
|
15
|
+
text="Make this Chat SDK bot durable with Workflow SDK. Install/use `workflow`. Create one exported workflow function with "use workflow" per chat thread. Store the Chat SDK thread ID, Workflow run ID, and any serialized conversation state in the project data store. Use `defineHook()` from `workflow` for incoming turns and call `resumeHook()` from `workflow/api` from the Chat SDK webhook or message handler. Put provider calls, database writes, and outbound platform messages in "use step" helper functions. Start a new run with `start(workflowFn, [initialThreadState])` when no run exists, otherwise resume the existing hook. Use `getRun(runId)` for status, cancellation, or stream reads. Verify first message, follow-up message, restart/reconnect, duplicate webhook, and failed-send retry behavior."
|
|
16
|
+
/>
|
|
17
|
+
|
|
14
18
|
[Chat SDK](https://chat-sdk.dev/) is a unified TypeScript SDK for building bots across Slack, Microsoft Teams, Google Chat, Discord, Telegram, GitHub, Linear, and WhatsApp. Write the bot once, deploy to every platform. It handles webhook verification, event normalization, subscriptions, and cross-platform features like cards and modals.
|
|
15
19
|
|
|
16
20
|
Workflow SDK complements it by making bot **sessions** durable. Each conversation thread maps to a long-running workflow run that:
|
|
@@ -20,6 +24,10 @@ Workflow SDK complements it by making bot **sessions** durable. Each conversatio
|
|
|
20
24
|
- Survives deploys, cold starts, and crashes — the session picks up from the last step on replay
|
|
21
25
|
- Receives follow-up messages via hooks, so the bot stays responsive while the workflow is still running
|
|
22
26
|
|
|
27
|
+
<Callout type="info">
|
|
28
|
+
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"`.
|
|
29
|
+
</Callout>
|
|
30
|
+
|
|
23
31
|
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
32
|
|
|
25
33
|
## How It Fits Together
|
|
@@ -172,6 +180,10 @@ export type ChatTurnPayload = {
|
|
|
172
180
|
|
|
173
181
|
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
182
|
|
|
183
|
+
<Callout type="info">
|
|
184
|
+
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).
|
|
185
|
+
</Callout>
|
|
186
|
+
|
|
175
187
|
```typescript title="lib/chat-session-handlers.ts" lineNumbers
|
|
176
188
|
import type { Message, Thread } from "chat";
|
|
177
189
|
import { getRun, resumeHook, start } from "workflow/api";
|
|
@@ -285,6 +297,10 @@ Adapter packages (`@chat-adapter/slack`, `@chat-adapter/telegram`, etc.) depend
|
|
|
285
297
|
|
|
286
298
|
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
299
|
|
|
300
|
+
### Make first-message routing atomic
|
|
301
|
+
|
|
302
|
+
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.
|
|
303
|
+
|
|
288
304
|
### Keep the hook outside the loop
|
|
289
305
|
|
|
290
306
|
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 +317,4 @@ Slack wants a 200 within 3 seconds. The webhook handler returns immediately afte
|
|
|
301
317
|
- [`getRun()`](/docs/api-reference/workflow-api/get-run) — `run.exists` before resuming, to detect stale `runId`s.
|
|
302
318
|
- [`defineHook()`](/docs/api-reference/workflow/define-hook) — per-turn suspension point inside the workflow.
|
|
303
319
|
- [`registerSingleton()`](https://chat-sdk.dev/docs/api/chat) — makes the bot resolvable from inside step functions.
|
|
320
|
+
- [Idempotency](/docs/foundations/idempotency) — protect duplicate-sensitive first messages and side effects.
|
|
@@ -10,6 +10,10 @@ related:
|
|
|
10
10
|
- /docs/cookbook/agent-patterns/durable-agent
|
|
11
11
|
---
|
|
12
12
|
|
|
13
|
+
<CopyPrompt
|
|
14
|
+
text="Implement a durable Vercel Sandbox-backed coding-agent workflow. Install the Sandbox package used by this project and `workflow`. Create an exported workflow function with "use workflow" that owns the agent session. Put sandbox creation, command execution, snapshot, refresh, and cleanup into helper functions with "use step". Persist the sandbox ID, snapshot ID, and workflow run ID in the project data store so clients can reconnect. Use `getWritable()` from `workflow` to stream agent progress and command output. Use `sleep()` to hibernate, refresh, or enforce idle timeouts. Add API routes to start a session, reconnect by run ID, and stop/cleanup. Verify first run, reconnect after reload, snapshot restore, timeout, and cleanup behavior."
|
|
15
|
+
/>
|
|
16
|
+
|
|
13
17
|
[Vercel Sandbox](https://vercel.com/docs/sandbox) provides isolated code execution environments. The `@vercel/sandbox` package has first-class support for the Workflow SDK — the `Sandbox` class is serializable, and its methods (`create`, `runCommand`, `stop`, `snapshot`) implicitly run as steps. You can use `Sandbox` directly inside a workflow function without wrapping each call in a separate `"use step"` function.
|
|
14
18
|
|
|
15
19
|
## Why Workflow + Sandbox
|
|
@@ -21,6 +25,10 @@ A sandbox alone gets you an isolated VM. A workflow around it gets you a **durab
|
|
|
21
25
|
- **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
26
|
- **Automatic cleanup.** `try/finally` in the workflow guarantees the VM is stopped on failure or destroy.
|
|
23
27
|
|
|
28
|
+
<Callout type="info">
|
|
29
|
+
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).
|
|
30
|
+
</Callout>
|
|
31
|
+
|
|
24
32
|
## Use Case: Coding Agents
|
|
25
33
|
|
|
26
34
|
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 +304,10 @@ export async function sandboxSessionWorkflow() {
|
|
|
296
304
|
|
|
297
305
|
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
306
|
|
|
307
|
+
<Callout type="info">
|
|
308
|
+
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).
|
|
309
|
+
</Callout>
|
|
310
|
+
|
|
299
311
|
```typescript title="app/api/sandbox/start/route.ts" lineNumbers
|
|
300
312
|
import { start, getRun } from "workflow/api";
|
|
301
313
|
import { sandboxSessionWorkflow } from "@/workflows/sandbox-session";
|
|
@@ -501,6 +513,10 @@ Stream closure must happen inside a `"use step"` function. Calling `writable.clo
|
|
|
501
513
|
|
|
502
514
|
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
515
|
|
|
516
|
+
### Decide whether `/start` should be idempotent
|
|
517
|
+
|
|
518
|
+
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.
|
|
519
|
+
|
|
504
520
|
### Keep the hook outside the loop
|
|
505
521
|
|
|
506
522
|
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 +530,4 @@ Each iteration's `hook.then(...)` attaches a listener to the same hook instance.
|
|
|
514
530
|
- [`sleep()`](/docs/api-reference/workflow/sleep) — durable timer that powers both idle hibernation and proactive refresh
|
|
515
531
|
- [`getRun()`](/docs/api-reference/workflow-api/get-run) — look up a run and replay its event log for reconnection
|
|
516
532
|
- [`getWritable()`](/docs/api-reference/workflow/get-writable) — resumable NDJSON event stream
|
|
533
|
+
- [Idempotency](/docs/foundations/idempotency) — choose when `/start` should reuse an existing run
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Deploying
|
|
3
|
-
icon: Rocket
|
|
4
3
|
description: Deploy workflows locally, on Vercel, or anywhere using pluggable World adapters.
|
|
5
4
|
type: overview
|
|
6
5
|
summary: Learn how to deploy workflows to different environments using World adapters.
|
|
7
6
|
related:
|
|
8
|
-
- /
|
|
9
|
-
- /
|
|
10
|
-
- /
|
|
11
|
-
- /
|
|
7
|
+
- /worlds/local
|
|
8
|
+
- /worlds/postgres
|
|
9
|
+
- /worlds/vercel
|
|
10
|
+
- /worlds/building-a-world
|
|
12
11
|
---
|
|
13
12
|
|
|
14
13
|
Workflows are designed to be highly portable. The same workflow code can run locally during development, on Vercel with zero configuration, or on any infrastructure using **Worlds** — pluggable adapters that handle storage, queuing, and communication.
|
|
@@ -42,6 +41,7 @@ The Vercel World provides:
|
|
|
42
41
|
- **Managed queuing** - Steps are processed reliably with automatic retries
|
|
43
42
|
- **Automatic scaling** - Workflows scale with your application
|
|
44
43
|
- **Built-in observability** - View workflow runs in the Vercel dashboard
|
|
44
|
+
- **Multi-region** - Runs are pinned to the region that creates them, keeping workflow data, queuing, and streaming close to your users (requires `workflow` 5.0.0-beta.33 or later)
|
|
45
45
|
|
|
46
46
|
Simply deploy your application:
|
|
47
47
|
|
|
@@ -52,7 +52,7 @@ vercel deploy
|
|
|
52
52
|
<FluidComputeCallout />
|
|
53
53
|
|
|
54
54
|
<Callout>
|
|
55
|
-
Learn more about the [Vercel World](/worlds/vercel) and its capabilities.
|
|
55
|
+
Learn more about the [Vercel World](/worlds/vercel) and its capabilities, including [multi-region](/worlds/vercel#multi-region).
|
|
56
56
|
</Callout>
|
|
57
57
|
|
|
58
58
|
## Self-Hosting & Other Providers
|
|
@@ -63,7 +63,7 @@ For self-hosting or deploying to other cloud providers, you can use community-ma
|
|
|
63
63
|
<Card title="Explore Worlds" href="/worlds">
|
|
64
64
|
Browse official and community World implementations with compatibility status and performance benchmarks.
|
|
65
65
|
</Card>
|
|
66
|
-
<Card title="Build Your Own" href="/
|
|
66
|
+
<Card title="Build Your Own" href="/worlds/building-a-world">
|
|
67
67
|
Learn how to implement a custom World for your infrastructure.
|
|
68
68
|
</Card>
|
|
69
69
|
</Cards>
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: abort-signal-timeout-in-workflow
|
|
3
|
+
description: AbortSignal.timeout() cannot be used inside workflow functions because it relies on real timers which break deterministic replay.
|
|
4
|
+
type: troubleshooting
|
|
5
|
+
summary: Use sleep() with AbortController instead of AbortSignal.timeout() in workflow functions.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /docs/foundations/workflows-and-steps
|
|
8
|
+
related:
|
|
9
|
+
- /docs/foundations/cancellation
|
|
10
|
+
- /docs/api-reference/workflow/sleep
|
|
11
|
+
- /docs/errors/timeout-in-workflow
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
<CopyPrompt
|
|
15
|
+
text="Replace `AbortSignal.timeout()` inside workflow functions. Find the failing `AbortSignal.timeout(ms)` call in a `"use workflow"` function. Import `sleep` from `workflow`. Create `const controller = new AbortController()`, pass `controller.signal` into the async work that supports cancellation, and start a deterministic timeout with `void sleep("10s").then(() => controller.abort())` or a `Promise.race` between the work and `sleep(...)`. Keep actual fetch/SDK side effects inside `"use step"` helpers when they need Node.js APIs. Treat intentional aborts as non-retryable if appropriate by throwing `FatalError`. Verify the operation succeeds before the timeout, aborts after the timeout, and replays without abort-signal-timeout-in-workflow."
|
|
16
|
+
/>
|
|
17
|
+
|
|
18
|
+
## Error
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
AbortSignal.timeout() is not supported in workflow functions.
|
|
22
|
+
Use sleep() with an AbortController instead.
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Why This Happens
|
|
26
|
+
|
|
27
|
+
`AbortSignal.timeout()` creates a signal that aborts after a real-time delay using an internal timer. Workflow functions must be [deterministic](/docs/foundations/workflows-and-steps) to support replay — they run the same code multiple times during the workflow's lifecycle, using the [event log](/docs/how-it-works/event-sourcing) to resume execution to the correct point.
|
|
28
|
+
|
|
29
|
+
Real-time timers break this determinism because:
|
|
30
|
+
- On the first execution, the timer might fire after 10 seconds
|
|
31
|
+
- On replay, the timer would fire again, but the event log may have already advanced past that point
|
|
32
|
+
- The timer's behavior depends on wall-clock time, which varies between executions
|
|
33
|
+
|
|
34
|
+
## How to Fix
|
|
35
|
+
|
|
36
|
+
Use [`sleep()`](/docs/api-reference/workflow/sleep) with an `AbortController` to create a deterministic timeout that cancels in-flight work:
|
|
37
|
+
|
|
38
|
+
**Before (incorrect):**
|
|
39
|
+
|
|
40
|
+
{/* @skip-typecheck: intentionally incorrect example */}
|
|
41
|
+
```typescript lineNumbers
|
|
42
|
+
export async function workflow() {
|
|
43
|
+
"use workflow";
|
|
44
|
+
|
|
45
|
+
// This will throw an error
|
|
46
|
+
const signal = AbortSignal.timeout(10_000); // [!code highlight]
|
|
47
|
+
const result = await fetchData(signal);
|
|
48
|
+
return result;
|
|
49
|
+
}
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
**After (correct):**
|
|
53
|
+
|
|
54
|
+
```typescript lineNumbers
|
|
55
|
+
import { sleep } from "workflow";
|
|
56
|
+
|
|
57
|
+
export async function workflow() {
|
|
58
|
+
"use workflow";
|
|
59
|
+
|
|
60
|
+
const controller = new AbortController(); // [!code highlight]
|
|
61
|
+
void sleep("10s").then(() => controller.abort()); // [!code highlight]
|
|
62
|
+
|
|
63
|
+
return await fetchData(controller.signal);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function fetchData(signal: AbortSignal) {
|
|
67
|
+
"use step";
|
|
68
|
+
const response = await fetch("https://api.example.com/data", { signal });
|
|
69
|
+
return response.json();
|
|
70
|
+
}
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
The `sleep()` + `AbortController` pattern is the durable equivalent of `AbortSignal.timeout()`. The sleep is recorded in the event log, so it replays deterministically. If `fetchData` finishes within 10 seconds you get the response; if not, the timer fires `controller.abort()`, `fetch` rejects with an `AbortError`, and the step's failure propagates to the workflow as a `FatalError` (no retries — abort is intentional cancellation).
|
|
74
|
+
|
|
75
|
+
<Callout type="info">
|
|
76
|
+
`AbortSignal.timeout()` works normally inside step functions, since steps have full Node.js runtime access and are not replayed.
|
|
77
|
+
</Callout>
|
|
78
|
+
|
|
79
|
+
## Related
|
|
80
|
+
|
|
81
|
+
- [Cancellation](/docs/foundations/cancellation) — Patterns for cancelling in-flight work
|
|
82
|
+
- [`sleep()` API Reference](/docs/api-reference/workflow/sleep) — Durable sleep primitive
|
|
83
|
+
- [Workflows and Steps](/docs/foundations/workflows-and-steps) — Why workflow functions must be deterministic
|
|
84
|
+
- [`setTimeout` in Workflow](/docs/errors/timeout-in-workflow) — Similar restriction on `setTimeout`
|
|
@@ -9,21 +9,21 @@ related:
|
|
|
9
9
|
- /docs/foundations/errors-and-retries
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
-
This error occurs when the Workflow runtime
|
|
12
|
+
This error occurs when the Workflow runtime repeatedly cannot replay events in the event log. This usually means the event log is in an invalid state, such as duplicate or orphaned events, or that a runtime determinism bug persists across retry attempts.
|
|
13
13
|
|
|
14
|
-
This is a **workflow-level fatal error**. It cannot be caught or handled inside your workflow code.
|
|
14
|
+
This is a **workflow-level fatal error**. It cannot be caught or handled inside your workflow code. The runtime first retries transient replay divergence automatically; it marks the run as failed with this error only after replay still cannot recover.
|
|
15
15
|
|
|
16
16
|
## Error Message
|
|
17
17
|
|
|
18
18
|
```
|
|
19
|
-
|
|
19
|
+
Workflow replay diverged <divergenceCount> times after <maxRecoveryReplays> recovery replays; latest divergent event was <eventId>. Last divergence: <details>
|
|
20
20
|
```
|
|
21
21
|
|
|
22
22
|
## Why This Happens
|
|
23
23
|
|
|
24
24
|
Workflows persist their progress as an ordered event log. During replay, the runtime processes each event in sequence — every event must be consumed by a matching callback (e.g., a step or sleep waiting for its result). When an event has no matching consumer, the runtime cannot advance past it, which would block all subsequent events and hang the workflow indefinitely.
|
|
25
25
|
|
|
26
|
-
Instead of silently hanging, the runtime
|
|
26
|
+
Instead of silently hanging, the runtime retries a divergent replay before failing the workflow and surfacing this terminal error.
|
|
27
27
|
|
|
28
28
|
Common scenarios that produce this error:
|
|
29
29
|
|
|
@@ -45,7 +45,7 @@ npm install workflow@latest
|
|
|
45
45
|
|
|
46
46
|
### 2. Retry the failed run
|
|
47
47
|
|
|
48
|
-
|
|
48
|
+
If this error is displayed, automatic replay recovery has already been exhausted and the run has been marked as `failed`. You can re-run it using the **Re-run** button in the Workflow Dashboard.
|
|
49
49
|
|
|
50
50
|
### 3. Report the issue
|
|
51
51
|
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: deployment-mismatch
|
|
3
|
+
description: A workflow run was delivered to a deployment other than the one it is pinned to.
|
|
4
|
+
type: troubleshooting
|
|
5
|
+
summary: Understand how Workflow recovers from a misrouted delivery, and why a run eventually fails with DEPLOYMENT_MISMATCH.
|
|
6
|
+
prerequisites:
|
|
7
|
+
- /docs/foundations/workflows-and-steps
|
|
8
|
+
related:
|
|
9
|
+
- /docs/foundations/versioning
|
|
10
|
+
- /docs/errors/runtime-decryption-failed
|
|
11
|
+
- /docs/foundations/errors-and-retries
|
|
12
|
+
---
|
|
13
|
+
|
|
14
|
+
Every run is pinned to a single deployment when it starts. When a queued workflow or step callback is delivered to a **different** deployment, Workflow does not execute it there. Instead it re-routes the message to the deployment the run is pinned to, and only if the run keeps arriving elsewhere does it fail with the `DEPLOYMENT_MISMATCH` classification.
|
|
15
|
+
|
|
16
|
+
This is an SDK/runtime signal, not an error thrown by your workflow code, and it is not catchable inside a workflow function.
|
|
17
|
+
|
|
18
|
+
## Error Message
|
|
19
|
+
|
|
20
|
+
```
|
|
21
|
+
Workflow run "wrun_..." is pinned to deployment "dpl_A", but was received by deployment "dpl_B". The runtime re-routed the message to "dpl_A" 3 times and it kept arriving elsewhere, so the run was stopped to protect against code-skew errors. Verify that the run's deployment is still available and that queue callbacks are routed to it.
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
When the queue definitively reports that the run's deployment cannot be reached — it was deleted, or aged out of its retention window — no re-route is possible and the message omits the re-routing clause. Transient or unknown publishing failures leave the current delivery unacknowledged so the queue can redeliver it; they do not fail the run or consume this recovery budget.
|
|
25
|
+
|
|
26
|
+
## Why A Run Is Pinned
|
|
27
|
+
|
|
28
|
+
A run's deployment is chosen once, at [`start()`](/docs/api-reference/workflow-api/start):
|
|
29
|
+
|
|
30
|
+
- By default it is the deployment that called `start()` — see [Versioning](/docs/foundations/versioning) for why runs are pinned this way.
|
|
31
|
+
- With `start(workflow, args, { deploymentId })` it is the id you pass, so a run can deliberately target a deployment other than the one that created it.
|
|
32
|
+
- With `deploymentId: "latest"` it is the most recent deployment for the current environment, resolved at start time.
|
|
33
|
+
|
|
34
|
+
Whichever it is, that `deploymentId` is recorded on the run, and every subsequent workflow replay and step execution must happen on that deployment. Continuing on a different one is unsafe:
|
|
35
|
+
|
|
36
|
+
1. **Code skew.** The workflow and step bundles on the receiving deployment may not match the code that produced the run's recorded history, so replay could diverge or produce incorrect results.
|
|
37
|
+
2. **Encryption.** Step inputs and other event-log payloads are encrypted with a per-run key derived from the pinned deployment's key material. A different deployment derives the wrong key and cannot decrypt them — previously the source of a confusing [runtime-decryption-failed](/docs/errors/runtime-decryption-failed) that exhausted retries with no clear cause.
|
|
38
|
+
|
|
39
|
+
So the runtime checks the pinned deployment before it executes anything, and `DEPLOYMENT_MISMATCH` names the result — instead of the mismatch surfacing later as an unrelated decryption failure.
|
|
40
|
+
|
|
41
|
+
## Automatic Recovery
|
|
42
|
+
|
|
43
|
+
A deployment that receives a run it does not own first tries to fix the delivery rather than fail the run:
|
|
44
|
+
|
|
45
|
+
1. It re-enqueues the message **explicitly addressed** to the run's own deployment. This is strictly better-addressed than the send that misrouted, which inherited the producing deployment's ambient id.
|
|
46
|
+
2. Delivery is delayed with a short exponential backoff (1s, 2s, 4s).
|
|
47
|
+
3. If the run keeps arriving at the wrong deployment, the run is failed with `DEPLOYMENT_MISMATCH` after `WORKFLOW_DEPLOYMENT_MISMATCH_MAX_RETRIES` attempts (default `3`). Set it to `0` to fail on the first misrouted delivery instead.
|
|
48
|
+
|
|
49
|
+
Nothing is executed on the wrong deployment during recovery: no workflow code, no step body, no `step_started`, and no hook resume. Whatever the delivery was carrying travels with it, so a pending step keeps its identity and a hook resume keeps its payload — they run on the deployment that can actually decrypt them.
|
|
50
|
+
|
|
51
|
+
Recovery attempts do not create events on the run, so a run that self-heals looks completely normal. They are reported on the invocation's trace span (`workflow.deployment.pinned_id`, `workflow.deployment_mismatch.retry_count`, `workflow.deployment_mismatch.recovered`) and as a runtime warning in your function logs.
|
|
52
|
+
|
|
53
|
+
## What To Do
|
|
54
|
+
|
|
55
|
+
- **Re-run from the current deployment.** Trigger the workflow again from your latest deployment (or use the **Re-run** button in the Workflow Dashboard). The new run is pinned to the current deployment.
|
|
56
|
+
- **Keep a run's deployment available** for the lifetime of that run. A run whose deployment has been deleted or has aged out cannot be resumed and must be re-run — recovery cannot help, so these fail on the first misrouted delivery. This applies to runs started with an explicit `deploymentId` too: pinning a run to an older deployment keeps it dependent on that deployment for its whole lifetime.
|
|
57
|
+
- **Report it** if the pinned deployment was still available. Include both deployment ids and the run id from the error message, plus the trace span attributes above — a run that failed this way despite a reachable target is a routing fault worth investigating rather than something to work around.
|
|
58
|
+
|
|
59
|
+
## This Error Cannot Be Caught
|
|
60
|
+
|
|
61
|
+
Like other runtime signals, `DEPLOYMENT_MISMATCH` is **not catchable** inside your workflow function — the run is failed before any workflow or step code executes on the receiving deployment. Check the run status from outside instead:
|
|
62
|
+
|
|
63
|
+
```typescript lineNumbers
|
|
64
|
+
import { getRun } from "workflow/api";
|
|
65
|
+
|
|
66
|
+
const run = getRun("wrun_abc123");
|
|
67
|
+
const status = await run.status;
|
|
68
|
+
if (status === "failed") {
|
|
69
|
+
console.error("Run failed");
|
|
70
|
+
}
|
|
71
|
+
```
|
|
@@ -9,6 +9,10 @@ related:
|
|
|
9
9
|
- /docs/api-reference/workflow/fetch
|
|
10
10
|
---
|
|
11
11
|
|
|
12
|
+
<CopyPrompt
|
|
13
|
+
text="Fix `fetch` usage inside workflow functions. Search workflow files for direct global `fetch(...)` calls and libraries such as AI SDK calls that use fetch. For simple HTTP calls inside a `"use workflow"` function, import `{ fetch }` from `workflow` and replace the global call. For SDK/client calls that need normal Node.js or provider behavior, move the call into a helper function with `"use step"` and call that step from the workflow. Keep all step inputs and outputs serializable. Verify the workflow starts and replays without the fetch-in-workflow error."
|
|
14
|
+
/>
|
|
15
|
+
|
|
12
16
|
This error occurs when you try to use `fetch()` directly in a workflow function, or when a library (like the AI SDK) tries to call `fetch()` under the hood.
|
|
13
17
|
|
|
14
18
|
## Error Message
|