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.
Files changed (227) hide show
  1. package/README.md +63 -20
  2. package/dist/api-workflow.d.ts +1 -1
  3. package/dist/api-workflow.d.ts.map +1 -1
  4. package/dist/api-workflow.js +1 -1
  5. package/dist/api.d.ts +5 -1
  6. package/dist/api.d.ts.map +1 -1
  7. package/dist/api.js +12 -2
  8. package/dist/index.d.ts +1 -0
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +6 -1
  11. package/dist/internal/builtins.d.ts +17 -0
  12. package/dist/internal/builtins.d.ts.map +1 -1
  13. package/dist/internal/builtins.js +65 -1
  14. package/dist/internal/errors.d.ts +1 -1
  15. package/dist/internal/errors.d.ts.map +1 -1
  16. package/dist/internal/errors.js +2 -2
  17. package/dist/nest-builder.d.ts +2 -0
  18. package/dist/nest-builder.d.ts.map +1 -0
  19. package/dist/nest-builder.js +2 -0
  20. package/dist/nest-vercel-builder.d.ts +2 -0
  21. package/dist/nest-vercel-builder.d.ts.map +1 -0
  22. package/dist/nest-vercel-builder.js +2 -0
  23. package/dist/observability.d.ts +1 -1
  24. package/dist/observability.js +2 -2
  25. package/dist/runtime.d.ts +2 -1
  26. package/dist/runtime.d.ts.map +1 -1
  27. package/dist/runtime.js +5 -2
  28. package/docs/ai/chat-session-modeling.mdx +7 -3
  29. package/docs/ai/defining-tools.mdx +2 -2
  30. package/docs/ai/index.mdx +30 -25
  31. package/docs/ai/message-queueing.mdx +10 -10
  32. package/docs/ai/resumable-streams.mdx +9 -1
  33. package/docs/api-reference/index.mdx +24 -0
  34. package/docs/api-reference/meta.json +8 -0
  35. package/docs/api-reference/vitest/index.mdx +28 -7
  36. package/docs/api-reference/workflow/create-hook.mdx +79 -0
  37. package/docs/api-reference/workflow/create-webhook.mdx +1 -0
  38. package/docs/api-reference/workflow/define-hook.mdx +26 -24
  39. package/docs/api-reference/workflow/fatal-error.mdx +29 -7
  40. package/docs/api-reference/workflow/fetch.mdx +8 -4
  41. package/docs/api-reference/workflow/index.mdx +3 -0
  42. package/docs/api-reference/workflow/set-attributes.mdx +61 -0
  43. package/docs/api-reference/workflow/sleep.mdx +1 -1
  44. package/docs/api-reference/workflow-ai/durable-agent.mdx +7 -45
  45. package/docs/api-reference/workflow-ai/index.mdx +2 -2
  46. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +44 -1
  47. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +9 -0
  48. package/docs/api-reference/workflow-api/get-run.mdx +25 -0
  49. package/docs/api-reference/workflow-api/index.mdx +6 -8
  50. package/docs/api-reference/workflow-api/resume-hook.mdx +60 -1
  51. package/docs/api-reference/workflow-api/resume-webhook.mdx +6 -4
  52. package/docs/api-reference/workflow-api/start.mdx +39 -6
  53. package/docs/api-reference/workflow-astro/index.mdx +18 -0
  54. package/docs/api-reference/workflow-astro/meta.json +4 -0
  55. package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
  56. package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
  57. package/docs/api-reference/workflow-errors/index.mdx +88 -0
  58. package/docs/api-reference/workflow-errors/meta.json +6 -0
  59. package/docs/api-reference/workflow-errors/precondition-failed-error.mdx +68 -0
  60. package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
  61. package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
  62. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
  63. package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
  64. package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
  65. package/docs/api-reference/workflow-globals.mdx +4 -1
  66. package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
  67. package/docs/api-reference/workflow-nest/index.mdx +31 -0
  68. package/docs/api-reference/workflow-nest/meta.json +9 -0
  69. package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
  70. package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
  71. package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
  72. package/docs/api-reference/workflow-next/with-workflow.mdx +56 -2
  73. package/docs/api-reference/workflow-nitro/index.mdx +60 -0
  74. package/docs/api-reference/workflow-nuxt/index.mdx +48 -0
  75. package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
  76. package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
  77. package/docs/api-reference/workflow-observability/index.mdx +64 -0
  78. package/docs/api-reference/workflow-observability/meta.json +11 -0
  79. package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
  80. package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
  81. package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
  82. package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
  83. package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
  84. package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
  85. package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +7 -10
  86. package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
  87. package/docs/api-reference/workflow-runtime/index.mdx +43 -0
  88. package/docs/api-reference/workflow-runtime/meta.json +12 -0
  89. package/docs/api-reference/workflow-runtime/set-world.mdx +49 -0
  90. package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +42 -0
  91. package/docs/api-reference/workflow-runtime/world/analytics.mdx +138 -0
  92. package/docs/api-reference/{workflow-api → workflow-runtime}/world/index.mdx +10 -10
  93. package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
  94. package/docs/api-reference/{workflow-api → workflow-runtime}/world/queue.mdx +8 -6
  95. package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +18 -5
  96. package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +2 -2
  97. package/docs/api-reference/workflow-serde/index.mdx +0 -1
  98. package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +1 -2
  99. package/docs/api-reference/workflow-serde/workflow-serialize.mdx +2 -2
  100. package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
  101. package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
  102. package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
  103. package/docs/api-reference/workflow-vite/index.mdx +18 -0
  104. package/docs/api-reference/workflow-vite/meta.json +4 -0
  105. package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
  106. package/docs/changelog/attributes-mvp.mdx +380 -0
  107. package/docs/changelog/eager-processing.mdx +269 -0
  108. package/docs/changelog/index.mdx +3 -1
  109. package/docs/changelog/lazy-event-creation.md +127 -0
  110. package/docs/changelog/meta.json +9 -1
  111. package/docs/changelog/resilient-resume.mdx +22 -0
  112. package/docs/changelog/resilient-start.mdx +31 -283
  113. package/docs/changelog/step-message-ownership.mdx +360 -0
  114. package/docs/changelog/turbo-mode.md +87 -0
  115. package/docs/comparisons/index.mdx +66 -0
  116. package/docs/comparisons/meta.json +11 -0
  117. package/docs/comparisons/workflow-sdk-vs-aws-agentcore.mdx +55 -0
  118. package/docs/comparisons/workflow-sdk-vs-aws-step-functions.mdx +111 -0
  119. package/docs/comparisons/workflow-sdk-vs-cloudflare-workflows.mdx +71 -0
  120. package/docs/comparisons/workflow-sdk-vs-inngest.mdx +102 -0
  121. package/docs/comparisons/workflow-sdk-vs-temporal.mdx +123 -0
  122. package/docs/comparisons/workflow-sdk-vs-trigger-dev.mdx +103 -0
  123. package/docs/configuration/build-and-diagnostics.mdx +70 -0
  124. package/docs/configuration/cli-and-web-ui.mdx +182 -0
  125. package/docs/configuration/framework-options.mdx +165 -0
  126. package/docs/configuration/index.mdx +32 -0
  127. package/docs/configuration/meta.json +12 -0
  128. package/docs/configuration/runtime-tuning.mdx +276 -0
  129. package/docs/configuration/worlds.mdx +275 -0
  130. package/docs/cookbook/advanced/child-workflows.mdx +203 -256
  131. package/docs/cookbook/advanced/meta.json +1 -1
  132. package/docs/cookbook/advanced/publishing-libraries.mdx +33 -24
  133. package/docs/cookbook/advanced/serializable-steps.mdx +12 -4
  134. package/docs/cookbook/advanced/upgrading-workflows.mdx +199 -0
  135. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +34 -75
  136. package/docs/cookbook/agent-patterns/durable-agent.mdx +14 -142
  137. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +11 -3
  138. package/docs/cookbook/common-patterns/batching.mdx +4 -0
  139. package/docs/cookbook/common-patterns/idempotency.mdx +40 -52
  140. package/docs/cookbook/common-patterns/rate-limiting.mdx +5 -1
  141. package/docs/cookbook/common-patterns/saga.mdx +6 -2
  142. package/docs/cookbook/common-patterns/scheduling.mdx +12 -0
  143. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +4 -0
  144. package/docs/cookbook/common-patterns/timeouts.mdx +6 -1
  145. package/docs/cookbook/common-patterns/webhooks.mdx +4 -0
  146. package/docs/cookbook/common-patterns/workflow-composition.mdx +19 -16
  147. package/docs/cookbook/index.mdx +3 -3
  148. package/docs/cookbook/integrations/ai-sdk.mdx +64 -28
  149. package/docs/cookbook/integrations/chat-sdk.mdx +17 -0
  150. package/docs/cookbook/integrations/sandbox.mdx +17 -0
  151. package/docs/{deploying/index.mdx → deploying.mdx} +7 -7
  152. package/docs/errors/abort-signal-timeout-in-workflow.mdx +84 -0
  153. package/docs/errors/corrupted-event-log.mdx +5 -5
  154. package/docs/errors/deployment-mismatch.mdx +71 -0
  155. package/docs/errors/fetch-in-workflow.mdx +4 -0
  156. package/docs/errors/hook-conflict.mdx +60 -4
  157. package/docs/errors/index.mdx +1 -35
  158. package/docs/errors/node-js-module-in-workflow.mdx +4 -0
  159. package/docs/errors/replay-divergence.mdx +27 -0
  160. package/docs/errors/runtime-decryption-failed.mdx +77 -0
  161. package/docs/errors/serialization-failed.mdx +4 -0
  162. package/docs/errors/start-invalid-workflow-function.mdx +4 -0
  163. package/docs/errors/step-executed-multiple-times.mdx +23 -0
  164. package/docs/errors/step-not-registered.mdx +1 -1
  165. package/docs/errors/timeout-in-workflow.mdx +4 -0
  166. package/docs/errors/webhook-response-not-sent.mdx +4 -0
  167. package/docs/foundations/cancellation.mdx +459 -0
  168. package/docs/foundations/errors-and-retries.mdx +7 -3
  169. package/docs/foundations/hooks.mdx +29 -0
  170. package/docs/foundations/idempotency.mdx +243 -11
  171. package/docs/foundations/index.mdx +1 -23
  172. package/docs/foundations/meta.json +3 -1
  173. package/docs/foundations/serialization.mdx +77 -41
  174. package/docs/foundations/starting-workflows.mdx +79 -2
  175. package/docs/foundations/streaming.mdx +14 -23
  176. package/docs/foundations/versioning.mdx +263 -0
  177. package/docs/getting-started/astro.mdx +12 -2
  178. package/docs/getting-started/express.mdx +6 -2
  179. package/docs/getting-started/fastify.mdx +6 -2
  180. package/docs/getting-started/hono.mdx +6 -2
  181. package/docs/getting-started/index.mdx +16 -10
  182. package/docs/getting-started/meta.json +4 -1
  183. package/docs/getting-started/nestjs.mdx +77 -3
  184. package/docs/getting-started/next.mdx +12 -6
  185. package/docs/getting-started/nitro.mdx +28 -2
  186. package/docs/getting-started/nuxt.mdx +6 -2
  187. package/docs/getting-started/python.mdx +24 -18
  188. package/docs/getting-started/react-router/index.mdx +33 -0
  189. package/docs/getting-started/react-router/meta.json +5 -0
  190. package/docs/getting-started/react-router/v7.mdx +237 -0
  191. package/docs/getting-started/react-router/v8.mdx +232 -0
  192. package/docs/getting-started/sveltekit.mdx +12 -2
  193. package/docs/getting-started/tanstack-start.mdx +245 -0
  194. package/docs/getting-started/vite.mdx +6 -2
  195. package/docs/how-it-works/cancellation.mdx +287 -0
  196. package/docs/how-it-works/code-transform.mdx +21 -17
  197. package/docs/how-it-works/encryption.mdx +5 -5
  198. package/docs/how-it-works/event-sourcing.mdx +6 -6
  199. package/docs/how-it-works/framework-integrations.mdx +96 -337
  200. package/docs/how-it-works/meta.json +2 -1
  201. package/docs/internal/index.mdx +21 -0
  202. package/docs/internal/meta.json +10 -0
  203. package/docs/internal/nitro-native-build.mdx +38 -0
  204. package/docs/internal/nitro-web-ui.mdx +24 -0
  205. package/docs/internal/serializable-abort-controller.mdx +148 -0
  206. package/docs/meta.json +2 -2
  207. package/docs/observability/attributes.mdx +112 -0
  208. package/docs/observability/index.mdx +20 -1
  209. package/docs/observability/meta.json +1 -1
  210. package/docs/observability/tracing.mdx +124 -0
  211. package/docs/testing/index.mdx +2 -2
  212. package/package.json +17 -13
  213. package/docs/api-reference/workflow-api/world/meta.json +0 -4
  214. package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
  215. package/docs/cookbook/advanced/distributed-abort-controller.mdx +0 -318
  216. package/docs/deploying/building-a-world.mdx +0 -251
  217. package/docs/deploying/meta.json +0 -4
  218. package/docs/deploying/world/local-world.mdx +0 -84
  219. package/docs/deploying/world/meta.json +0 -4
  220. package/docs/deploying/world/postgres-world.mdx +0 -222
  221. package/docs/deploying/world/vercel-world.mdx +0 -179
  222. package/docs/migration-guides/index.mdx +0 -34
  223. package/docs/migration-guides/meta.json +0 -9
  224. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +0 -363
  225. package/docs/migration-guides/migrating-from-inngest.mdx +0 -314
  226. package/docs/migration-guides/migrating-from-temporal.mdx +0 -318
  227. 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 for lower-level control over model calls and tool execution.
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 for full control over model options, stop conditions, and output schemas while tools remain durable steps.
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
- [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 those calls durable: the model request, the tool loop, and the multi-turn conversation all survive restarts and timeouts.
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 `&quot;use step&quot;` 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 [`DurableAgent`](/cookbook/agent-patterns/durable-agent) which wraps [`streamText`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) and manages the tool loop automatically. This page covers using `streamText()` directly when you need lower-level control.
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 `DurableAgent` when you need:
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
- * **Custom stop conditions** [`stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/agents#stop-conditions), [`prepareStep`](https://ai-sdk.dev/docs/ai-sdk-core/agents#prepare-step), or [`onStepFinish`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text#on-step-finish) callbacks
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/ai";
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. **Hook is created once.** `turnHook.create({ token: workflowRunId })` outside the loop calling it twice with the same token throws `HookConflictError`.
294
- 3. **`preventClose: true`** on `pipeTo` keeps the durable writable open so the next turn can write to it.
295
- 4. **`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.
296
- 5. **`startIndex: tailIndex + 1`** gives each follow-up response only the new chunks, avoiding replay of previous turns.
297
- 6. **`/done`** resumes the hook so the workflow exits cleanly, then returns a synthetic `start` + `finish` so `useChat` transitions out of "streaming".
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
- ## streamText vs DurableAgent
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()` | `DurableAgent` |
367
+ | | `streamText()` (this pattern) | `WorkflowAgent` |
334
368
  |---|---|---|
335
- | **Tool loop** | AI SDK handles via `stopWhen` | DurableAgent handles internally |
336
- | **LLM call durability** | Re-executes on replay | Each LLM call is a durable step |
337
- | **Stop conditions** | `stopWhen`, `prepareStep` | `prepareStep` only |
338
- | **Structured output** | `Output.object()`, `Output.array()` | Not available |
339
- | **Step callbacks** | `onStepFinish`, `onChunk` | Not available |
340
- | **Setup** | Manual stream piping | Automatic |
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 `DurableAgent` for most agent use cases. Use `streamText` when you need the additional control.
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 wrap `"use step"` functions so each tool call is replayed from the log, not re-executed
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/api-reference/workflow/use-step) — makes tool executions durable
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 &quot;use workflow&quot; 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 &quot;use step&quot; 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 &quot;use workflow&quot; that owns the agent session. Put sandbox creation, command execution, snapshot, refresh, and cleanup into helper functions with &quot;use step&quot;. 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
- - /docs/deploying/world/local-world
9
- - /docs/deploying/world/postgres-world
10
- - /docs/deploying/world/vercel-world
11
- - /docs/deploying/building-a-world
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="/docs/deploying/building-a-world">
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 `&quot;use workflow&quot;` 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(&quot;10s&quot;).then(() => controller.abort())` or a `Promise.race` between the work and `sleep(...)`. Keep actual fetch/SDK side effects inside `&quot;use step&quot;` 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 encounters an event in the event log that no registered consumer can process. This means the event log is in an invalid state typically due to duplicate or orphaned events.
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. A corrupted event log immediately fails the entire run without executing any more user code. The run must be retried from outside the workflow.
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
- Unconsumed event in event log: eventType=<type>, correlationId=<id>, eventId=<id>. This indicates a corrupted or invalid event log.
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 raises a `WorkflowRuntimeError` to fail the workflow fast and surface the problem.
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
- Since this is a fatal error, the run is automatically marked as `failed`. You can re-run it using the **Re-run** button in the Workflow Dashboard.
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 `&quot;use workflow&quot;` 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 `&quot;use step&quot;` 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