workflow 5.0.0-beta.43 → 5.0.0-beta.44

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 (182) hide show
  1. package/README.md +6 -4
  2. package/dist/internal/builtins.d.ts +4 -4
  3. package/dist/internal/builtins.js +6 -6
  4. package/docs/ai/chat-session-modeling.mdx +23 -24
  5. package/docs/ai/defining-tools.mdx +5 -6
  6. package/docs/ai/human-in-the-loop.mdx +11 -11
  7. package/docs/ai/index.mdx +20 -20
  8. package/docs/ai/message-queueing.mdx +6 -6
  9. package/docs/ai/meta.json +1 -0
  10. package/docs/ai/resumable-streams.mdx +28 -28
  11. package/docs/ai/sleep-and-delays.mdx +9 -9
  12. package/docs/ai/streaming-updates-from-tools.mdx +4 -4
  13. package/docs/api-reference/vitest/index.mdx +8 -8
  14. package/docs/api-reference/workflow/create-hook.mdx +15 -15
  15. package/docs/api-reference/workflow/create-webhook.mdx +15 -15
  16. package/docs/api-reference/workflow/define-hook.mdx +10 -10
  17. package/docs/api-reference/workflow/fatal-error.mdx +2 -2
  18. package/docs/api-reference/workflow/fetch.mdx +7 -7
  19. package/docs/api-reference/workflow/get-step-metadata.mdx +2 -2
  20. package/docs/api-reference/workflow/get-workflow-metadata.mdx +3 -3
  21. package/docs/api-reference/workflow/get-writable.mdx +7 -7
  22. package/docs/api-reference/workflow/index.mdx +1 -1
  23. package/docs/api-reference/workflow/retryable-error.mdx +1 -1
  24. package/docs/api-reference/workflow/set-attributes.mdx +2 -2
  25. package/docs/api-reference/workflow/sleep.mdx +3 -3
  26. package/docs/api-reference/workflow-ai/durable-agent.mdx +42 -42
  27. package/docs/api-reference/workflow-ai/index.mdx +3 -3
  28. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +28 -28
  29. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +11 -11
  30. package/docs/api-reference/workflow-api/get-run.mdx +10 -10
  31. package/docs/api-reference/workflow-api/index.mdx +2 -4
  32. package/docs/api-reference/workflow-api/resume-hook.mdx +14 -14
  33. package/docs/api-reference/workflow-api/resume-webhook.mdx +3 -3
  34. package/docs/api-reference/workflow-api/start.mdx +16 -15
  35. package/docs/api-reference/workflow-astro/workflow.mdx +3 -3
  36. package/docs/api-reference/workflow-errors/entity-conflict-error.mdx +4 -4
  37. package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +4 -4
  38. package/docs/api-reference/workflow-errors/hook-not-found-error.mdx +8 -8
  39. package/docs/api-reference/workflow-errors/index.mdx +6 -6
  40. package/docs/api-reference/workflow-errors/precondition-failed-error.mdx +9 -9
  41. package/docs/api-reference/workflow-errors/run-expired-error.mdx +2 -2
  42. package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +4 -4
  43. package/docs/api-reference/workflow-errors/step-not-registered-error.mdx +5 -5
  44. package/docs/api-reference/workflow-errors/throttle-error.mdx +2 -2
  45. package/docs/api-reference/workflow-errors/too-early-error.mdx +2 -2
  46. package/docs/api-reference/workflow-errors/workflow-error.mdx +4 -4
  47. package/docs/api-reference/workflow-errors/workflow-not-registered-error.mdx +5 -6
  48. package/docs/api-reference/workflow-errors/workflow-run-cancelled-error.mdx +6 -6
  49. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +5 -5
  50. package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +4 -4
  51. package/docs/api-reference/workflow-errors/workflow-run-not-found-error.mdx +4 -4
  52. package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +2 -2
  53. package/docs/api-reference/workflow-errors/workflow-world-error.mdx +8 -8
  54. package/docs/api-reference/workflow-globals.mdx +12 -11
  55. package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +2 -2
  56. package/docs/api-reference/workflow-nest/nest-local-builder.mdx +4 -4
  57. package/docs/api-reference/workflow-nest/workflow-controller.mdx +1 -1
  58. package/docs/api-reference/workflow-nest/workflow-module.mdx +4 -4
  59. package/docs/api-reference/workflow-next/with-workflow.mdx +16 -16
  60. package/docs/api-reference/workflow-nitro/index.mdx +6 -6
  61. package/docs/api-reference/workflow-nuxt/index.mdx +4 -4
  62. package/docs/api-reference/workflow-observability/hydrate-data.mdx +5 -5
  63. package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +4 -4
  64. package/docs/api-reference/workflow-observability/index.mdx +6 -8
  65. package/docs/api-reference/workflow-observability/observability-revivers.mdx +2 -2
  66. package/docs/api-reference/workflow-observability/parse-class-name.mdx +3 -3
  67. package/docs/api-reference/workflow-observability/parse-step-name.mdx +4 -4
  68. package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +4 -4
  69. package/docs/api-reference/workflow-runtime/create-world.mdx +8 -8
  70. package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +6 -6
  71. package/docs/api-reference/workflow-runtime/get-world.mdx +4 -4
  72. package/docs/api-reference/workflow-runtime/health-check.mdx +1 -1
  73. package/docs/api-reference/workflow-runtime/index.mdx +2 -4
  74. package/docs/api-reference/workflow-runtime/set-world.mdx +15 -13
  75. package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +6 -5
  76. package/docs/api-reference/workflow-runtime/world/analytics.mdx +9 -9
  77. package/docs/api-reference/workflow-runtime/world/index.mdx +5 -3
  78. package/docs/api-reference/workflow-runtime/world/queue.mdx +11 -11
  79. package/docs/api-reference/workflow-runtime/world/storage.mdx +62 -28
  80. package/docs/api-reference/workflow-runtime/world/streams.mdx +7 -7
  81. package/docs/api-reference/workflow-serde/index.mdx +1 -1
  82. package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +2 -2
  83. package/docs/api-reference/workflow-serde/workflow-serialize.mdx +7 -7
  84. package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +3 -3
  85. package/docs/api-reference/workflow-vite/workflow.mdx +5 -5
  86. package/docs/changelog/attributes-mvp.mdx +39 -39
  87. package/docs/changelog/batched-event-writes.mdx +12 -12
  88. package/docs/changelog/eager-processing.mdx +63 -63
  89. package/docs/changelog/index.mdx +3 -3
  90. package/docs/changelog/lazy-event-creation.md +27 -27
  91. package/docs/changelog/resilient-resume.mdx +5 -5
  92. package/docs/changelog/resilient-start.mdx +14 -14
  93. package/docs/changelog/step-message-ownership.mdx +47 -47
  94. package/docs/changelog/turbo-mode.md +20 -20
  95. package/docs/comparisons/index.mdx +13 -13
  96. package/docs/comparisons/workflow-sdk-vs-aws-agentcore.mdx +15 -15
  97. package/docs/comparisons/workflow-sdk-vs-aws-step-functions.mdx +12 -12
  98. package/docs/comparisons/workflow-sdk-vs-cloudflare-workflows.mdx +11 -11
  99. package/docs/comparisons/workflow-sdk-vs-inngest.mdx +19 -19
  100. package/docs/comparisons/workflow-sdk-vs-temporal.mdx +23 -23
  101. package/docs/comparisons/workflow-sdk-vs-trigger-dev.mdx +18 -17
  102. package/docs/configuration/build-and-diagnostics.mdx +5 -5
  103. package/docs/configuration/cli-and-web-ui.mdx +4 -4
  104. package/docs/configuration/runtime-tuning.mdx +86 -23
  105. package/docs/configuration/worlds.mdx +28 -14
  106. package/docs/cookbook/advanced/child-workflows.mdx +25 -25
  107. package/docs/cookbook/advanced/publishing-libraries.mdx +40 -40
  108. package/docs/cookbook/advanced/serializable-steps.mdx +21 -21
  109. package/docs/cookbook/advanced/upgrading-workflows.mdx +31 -31
  110. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +20 -20
  111. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +22 -22
  112. package/docs/cookbook/common-patterns/batching.mdx +14 -14
  113. package/docs/cookbook/common-patterns/idempotency.mdx +9 -9
  114. package/docs/cookbook/common-patterns/rate-limiting.mdx +3 -3
  115. package/docs/cookbook/common-patterns/saga.mdx +19 -19
  116. package/docs/cookbook/common-patterns/scheduling.mdx +23 -23
  117. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +26 -26
  118. package/docs/cookbook/common-patterns/timeouts.mdx +23 -23
  119. package/docs/cookbook/common-patterns/webhooks.mdx +6 -6
  120. package/docs/cookbook/common-patterns/workflow-composition.mdx +19 -19
  121. package/docs/cookbook/index.mdx +22 -22
  122. package/docs/cookbook/integrations/ai-sdk.mdx +43 -41
  123. package/docs/cookbook/integrations/chat-sdk.mdx +34 -34
  124. package/docs/cookbook/integrations/sandbox.mdx +46 -46
  125. package/docs/deploying.mdx +15 -15
  126. package/docs/errors/abort-signal-timeout-in-workflow.mdx +12 -12
  127. package/docs/errors/corrupted-event-log.mdx +11 -11
  128. package/docs/errors/deployment-mismatch.mdx +14 -14
  129. package/docs/errors/fetch-in-workflow.mdx +8 -8
  130. package/docs/errors/hook-conflict.mdx +11 -11
  131. package/docs/errors/index.mdx +1 -1
  132. package/docs/errors/node-js-module-in-workflow.mdx +5 -5
  133. package/docs/errors/replay-divergence.mdx +2 -2
  134. package/docs/errors/runtime-decryption-failed.mdx +12 -12
  135. package/docs/errors/serialization-failed.mdx +40 -12
  136. package/docs/errors/start-invalid-workflow-function.mdx +5 -5
  137. package/docs/errors/step-executed-multiple-times.mdx +2 -2
  138. package/docs/errors/step-not-registered.mdx +5 -5
  139. package/docs/errors/timeout-in-workflow.mdx +8 -8
  140. package/docs/errors/webhook-invalid-respond-with-value.mdx +18 -18
  141. package/docs/errors/webhook-response-not-sent.mdx +16 -16
  142. package/docs/errors/workflow-not-registered.mdx +5 -5
  143. package/docs/foundations/cancellation.mdx +31 -31
  144. package/docs/foundations/errors-and-retries.mdx +42 -11
  145. package/docs/foundations/hooks.mdx +35 -35
  146. package/docs/foundations/idempotency.mdx +9 -9
  147. package/docs/foundations/serialization.mdx +21 -22
  148. package/docs/foundations/starting-workflows.mdx +36 -37
  149. package/docs/foundations/streaming.mdx +46 -41
  150. package/docs/foundations/versioning.mdx +3 -3
  151. package/docs/foundations/workflows-and-steps.mdx +9 -9
  152. package/docs/getting-started/astro.mdx +16 -16
  153. package/docs/getting-started/express.mdx +8 -8
  154. package/docs/getting-started/fastify.mdx +8 -8
  155. package/docs/getting-started/hono.mdx +8 -8
  156. package/docs/getting-started/nestjs.mdx +18 -17
  157. package/docs/getting-started/next.mdx +11 -11
  158. package/docs/getting-started/nitro.mdx +16 -16
  159. package/docs/getting-started/nuxt.mdx +8 -8
  160. package/docs/getting-started/python.mdx +4 -4
  161. package/docs/getting-started/react-router/v7.mdx +1 -1
  162. package/docs/getting-started/react-router/v8.mdx +1 -1
  163. package/docs/getting-started/sveltekit.mdx +14 -14
  164. package/docs/getting-started/tanstack-start.mdx +12 -12
  165. package/docs/getting-started/vite.mdx +8 -8
  166. package/docs/how-it-works/cancellation.mdx +62 -62
  167. package/docs/how-it-works/code-transform.mdx +66 -54
  168. package/docs/how-it-works/encryption.mdx +25 -21
  169. package/docs/how-it-works/event-sourcing.mdx +53 -35
  170. package/docs/how-it-works/framework-integrations.mdx +12 -12
  171. package/docs/how-it-works/understanding-directives.mdx +21 -21
  172. package/docs/internal/index.mdx +6 -6
  173. package/docs/internal/nitro-native-build.mdx +2 -2
  174. package/docs/internal/nitro-web-ui.mdx +4 -4
  175. package/docs/internal/serializable-abort-controller.mdx +7 -7
  176. package/docs/observability/attributes.mdx +3 -3
  177. package/docs/observability/index.mdx +14 -10
  178. package/docs/observability/tracing.mdx +10 -10
  179. package/docs/testing/index.mdx +33 -33
  180. package/docs/testing/server-based.mdx +10 -10
  181. package/docs/whats-new.mdx +185 -0
  182. package/package.json +12 -12
@@ -2,7 +2,7 @@
2
2
  title: Webhooks & External Callbacks
3
3
  description: Receive HTTP callbacks from external services, process them durably, and respond inline.
4
4
  type: guide
5
- summary: Create webhook endpoints that your workflow can await, process incoming requests in steps, and respond to the caller — all within durable workflow context.
5
+ summary: Create webhook endpoints that your workflow can await, process incoming requests in steps, and respond to the caller, all within durable workflow context.
6
6
  ---
7
7
 
8
8
  <CopyPrompt
@@ -17,7 +17,7 @@ Use webhooks when external services push events to your application via HTTP cal
17
17
  - Waiting for third-party verification or processing results
18
18
  - Any integration where an external system calls you back asynchronously
19
19
 
20
- ## Pattern: Processing webhook events
20
+ ## Pattern: processing webhook events
21
21
 
22
22
  Create a webhook with manual response control, then iterate over incoming requests:
23
23
 
@@ -77,7 +77,7 @@ async function processEvent(
77
77
  }
78
78
  ```
79
79
 
80
- ## Pattern: Async request-reply with timeout
80
+ ## Pattern: async request-reply with timeout
81
81
 
82
82
  Submit a request to an external service, pass it your webhook URL, then race the callback against a deadline:
83
83
 
@@ -128,7 +128,7 @@ async function processCallback(
128
128
  }
129
129
  ```
130
130
 
131
- ## Pattern: Large payload by reference
131
+ ## Pattern: large payload by reference
132
132
 
133
133
  When payloads are too large to serialize into the event log, pass a lightweight reference (a "claim check") instead. Use a hook to signal when the data is ready:
134
134
 
@@ -143,7 +143,7 @@ export async function importLargeFile(importId: string) {
143
143
  // Suspend until the external system signals the blob is uploaded
144
144
  const { blobToken } = await blobReady.create({ token: `upload:${importId}` }); // [!code highlight]
145
145
 
146
- // Process by reference -- the full payload never enters the event log
146
+ // Process by reference. The full payload never enters the event log.
147
147
  await processBlob(blobToken);
148
148
 
149
149
  return { importId, blobToken, status: "indexed" };
@@ -177,7 +177,7 @@ export async function POST(request: Request) {
177
177
  - **`for await` on a webhook** lets you process multiple events from the same URL. Use `break` to stop listening after a terminal event.
178
178
  - **Webhooks auto-generate URLs** at `/.well-known/workflow/v1/webhook/:token`. Pass this URL to external services.
179
179
  - **Race webhooks against `sleep()`** for deadlines. If the callback doesn't arrive in time, the workflow can take a fallback action.
180
- - **For large payloads**, use a hook + reference token instead of passing the data through the workflow. The event log serializes all step inputs/outputs, so large payloads hurt performance.
180
+ - **For large payloads**, use a hook and reference token instead of passing the data through the workflow. The event log serializes all step inputs and outputs, so large payloads hurt performance.
181
181
 
182
182
  ## Key APIs
183
183
 
@@ -2,7 +2,7 @@
2
2
  title: Workflow Composition
3
3
  description: Call workflows from other workflows by direct await (flatten into the parent) or background spawn via start() (separate run).
4
4
  type: guide
5
- summary: Compose workflows two ways — direct await flattens the child into the parent's event log, while background spawn via start() runs the child as an independent run.
5
+ summary: Compose workflows two ways. Direct await flattens the child into the parent's event log, while background spawn via start() runs the child as an independent run.
6
6
  related:
7
7
  - /cookbook/advanced/child-workflows
8
8
  - /cookbook/common-patterns/idempotency
@@ -11,21 +11,21 @@ related:
11
11
  ---
12
12
 
13
13
  <CopyPrompt
14
- text="Compose these workflows. For direct composition, `await` the child workflow function from the parent &quot;use workflow&quot; function — the child's steps flatten into the parent's event log and run as a single run sharing the parent's lifecycle. For independent background work, spawn the child with `start()` from `workflow/api` called directly in the workflow (in v5, `start()` is step-backed and records a deterministic step boundary) and return `run.runId` so callers can track it with `getRun()`. Choose flattening when the parent needs the child's result; choose background spawn when the child should have its own run, retries, and lifetime. Keep all inputs and outputs serializable. Verify flattened execution, background spawn with a separate runId, and replay determinism."
14
+ text="Compose these workflows. For direct composition, `await` the child workflow function from the parent &quot;use workflow&quot; function: the child's steps flatten into the parent's event log and run as a single run sharing the parent's lifecycle. For independent background work, spawn the child with `start()` from `workflow/api` called directly in the workflow (in v5, `start()` is step-backed and records a deterministic step boundary) and return `run.runId` so callers can track it with `getRun()`. Choose flattening when the parent needs the child's result; choose background spawn when the child should have its own run, retries, and lifetime. Keep all inputs and outputs serializable. Verify flattened execution, background spawn with a separate runId, and replay determinism."
15
15
  />
16
16
 
17
- Workflows can call other workflows. Choose between two composition modes depending on whether the parent needs the child's result inline (direct await) or wants to fire the child off as an independent run (background spawn). For massive fan-out with hook-based waiting and partial-failure handling, see [Child Workflows](/cookbook/advanced/child-workflows).
17
+ Workflows can call other workflows. Choose between two composition modes depending on whether the parent needs the child's result inline (direct await) or starts the child as an independent run (background spawn). For large fan-out operations with hook-based waiting and partial-failure handling, see [Child Workflows](/cookbook/advanced/child-workflows).
18
18
 
19
19
  ## When to use this
20
20
 
21
- - **Direct await** — the parent needs the child's result before continuing, and you want a single unified event log
22
- - **Background spawn** — the parent doesn't need to wait, and you want the child to be observable as a separate run with its own `runId`
21
+ - **Direct await**: the parent needs the child's result before continuing, and you want a single unified event log
22
+ - **Background spawn**: the parent doesn't need to wait, and you want the child to be observable as a separate run with its own `runId`
23
23
 
24
24
  ## Pattern
25
25
 
26
26
  ### Direct await (flattening)
27
27
 
28
- Call a child workflow with `await` and the child's steps execute inline within the parent — they appear in the parent's event log as if you'd called them directly.
28
+ Call a child workflow with `await` and the child's steps execute inline within the parent. They appear in the parent's event log as if you'd called them directly.
29
29
 
30
30
  ```typescript lineNumbers
31
31
  declare function sendEmail(userId: string): Promise<void>; // @setup
@@ -80,21 +80,21 @@ export async function processOrder(orderId: string) {
80
80
  }
81
81
  ```
82
82
 
83
- The parent continues immediately after `start()` returns. The child runs independently and can be monitored separately using the returned `runId` (e.g., via [`getRun()`](/docs/api-reference/workflow-api/get-run)).
83
+ The parent continues immediately after `start()` returns. The child runs independently and can be monitored separately using the returned `runId`, such as through [`getRun()`](/docs/api-reference/workflow-api/get-run).
84
84
 
85
85
  <Callout type="info">
86
86
  Each background spawn creates a separate run. If duplicate requests must route to one active child workflow, have the child create a deterministic hook token from the business key and use that hook as the idempotency point. If concurrent starts race, the losing child can detect the conflict early with `await hook.getConflict()`, which resolves with the active owner so the child can point callers at it. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
87
87
  </Callout>
88
88
 
89
89
  <Callout type="info">
90
- If you want the child workflow to run on the latest deployment rather than the current one, pass [`deploymentId: "latest"`](/docs/api-reference/workflow-api/start#using-deploymentid-latest) in the `start()` options. See [Versioning](/docs/foundations/versioning) for the full model. This is currently a Vercel-specific feature, and other Worlds may map the concept to their own deployment runtimes. Be aware that the child workflow's function name, file path, argument types, and return type must remain compatible across deployments — renaming the function or changing its location will change the workflow ID, and modifying expected inputs or outputs can cause serialization failures.
90
+ If you want the child workflow to run on the latest deployment rather than the current one, pass [`deploymentId: "latest"`](/docs/api-reference/workflow-api/start#using-deploymentid-latest) in the `start()` options. See [Versioning](/docs/foundations/versioning) for the full model. This is currently a Vercel-specific feature, and other Worlds may map the concept to their own deployment runtimes. Be aware that the child workflow's function name, file path, argument types, and return type must remain compatible across deployments. Renaming the function or changing its location will change the workflow ID, and modifying expected inputs or outputs can cause serialization failures.
91
91
  </Callout>
92
92
 
93
93
  ## How it works
94
94
 
95
- 1. **Direct await flattens.** When a workflow function awaits another workflow function, the child's `"use workflow"` directive is treated as inline — the child's steps emit into the parent's event log and share the parent's run ID.
96
- 2. **`start()` mints a new run.** The child gets its own `runId`, its own event log, and its own retry boundary. The parent only sees the `runId` returned by `start()`.
97
- 3. **`start()` can run inside workflows.** In v5, `start()` is step-backed, so it can be called directly from a workflow function and still records a deterministic step boundary in the event log.
95
+ 1. **Direct await flattens the child workflow**: When a workflow function awaits another workflow function, the child's `"use workflow"` directive is treated as inline. The child's steps emit into the parent's event log and share the parent's run ID.
96
+ 2. **`start()` creates a new run**: The child gets its own `runId`, event log, and retry boundary. The parent only sees the `runId` returned by `start()`.
97
+ 3. **`start()` can run inside workflows**: In v5, `start()` is step-backed, so it can be called directly from a workflow function and still records a deterministic step boundary in the event log.
98
98
 
99
99
  ## Choosing between the two modes
100
100
 
@@ -108,14 +108,14 @@ If you want the child workflow to run on the latest deployment rather than the c
108
108
 
109
109
  ## Adapting to your use case
110
110
 
111
- - **Spawn many children at once** — call `start()` in a loop from the workflow. For more advanced fan-out (chunking, hook-based waiting, partial-failure handling), graduate to the [Child Workflows](/cookbook/advanced/child-workflows) recipe.
112
- - **Wait for a background child to finish** — combine `start()` with a completion hook the child resumes when done. The [Child Workflows](/cookbook/advanced/child-workflows) page covers the recommended `startAndWait()` pattern.
113
- - **Pass results back from background children** — the wrapped child resumes the parent's hook in `finally` with `{ status, value | error }`; the parent awaits the hook instead of polling `getRun().status`.
111
+ - **Spawn many children at once**: call `start()` in a loop from the workflow. For more advanced fan-out (chunking, hook-based waiting, partial-failure handling), graduate to the [Child Workflows](/cookbook/advanced/child-workflows) recipe.
112
+ - **Wait for a background child to finish**: combine `start()` with a completion hook the child resumes when done. The [Child Workflows](/cookbook/advanced/child-workflows) page covers the recommended `startAndWait()` pattern.
113
+ - **Pass results back from background children**: the wrapped child resumes the parent's hook in `finally` with `{ status, value | error }`; the parent awaits the hook instead of polling `getRun().status`.
114
114
 
115
115
  ## Key APIs
116
116
 
117
- - [`"use workflow"`](/docs/foundations/workflows-and-steps) — marks the orchestrator function
118
- - [`"use step"`](/docs/foundations/workflows-and-steps) — marks functions with full Node.js access
119
- - [`start()`](/docs/api-reference/workflow-api/start) — spawn a child workflow as a separate run
120
- - [`getRun()`](/docs/api-reference/workflow-api/get-run) — retrieve a workflow run's status and return value
121
- - [Idempotency](/docs/foundations/idempotency) — deduplicate step side effects and workflow starts
117
+ - [`"use workflow"`](/docs/foundations/workflows-and-steps): marks the orchestrator function
118
+ - [`"use step"`](/docs/foundations/workflows-and-steps): marks functions with full Node.js access
119
+ - [`start()`](/docs/api-reference/workflow-api/start): spawn a child workflow as a separate run
120
+ - [`getRun()`](/docs/api-reference/workflow-api/get-run): retrieve a workflow run's status and return value
121
+ - [Idempotency](/docs/foundations/idempotency): deduplicate step side effects and workflow starts
@@ -4,35 +4,35 @@ description: Best-practice workflow patterns with copy-paste code examples.
4
4
  type: overview
5
5
  ---
6
6
 
7
- A curated collection of workflow patterns with clean, copy-paste code examples for real use cases.
7
+ Use these workflow patterns and copy-paste code examples to implement common use cases.
8
8
 
9
- ## Agent Patterns
9
+ ## Agent patterns
10
10
 
11
- - [**WorkflowAgent**](/cookbook/agent-patterns/durable-agent) — Build durable, resumable AI agents with AI SDK's WorkflowAgent
12
- - [**Human-in-the-Loop**](/cookbook/agent-patterns/human-in-the-loop) — Pause an agent for human approval, then resume based on the decision
13
- - [**Agent Cancellation**](/cookbook/agent-patterns/agent-cancellation) — Stop a running agent immediately via `run.cancel()` or gracefully via a hook + `Promise.race`
11
+ - [**WorkflowAgent**](/cookbook/agent-patterns/durable-agent): Build durable, resumable AI agents with AI SDK's WorkflowAgent
12
+ - [**Human-in-the-Loop**](/cookbook/agent-patterns/human-in-the-loop): Pause an agent for human approval, then resume based on the decision
13
+ - [**Agent Cancellation**](/cookbook/agent-patterns/agent-cancellation): Stop a running agent immediately via `run.cancel()` or gracefully via a hook + `Promise.race`
14
14
 
15
- ## Common Patterns
15
+ ## Common patterns
16
16
 
17
- - [**Sequential & Parallel Execution**](/cookbook/common-patterns/sequential-and-parallel) — Compose steps with `await`, `Promise.all`, and `Promise.race` against durable sleeps and webhooks
18
- - [**Workflow Composition**](/cookbook/common-patterns/workflow-composition) — Call workflows from other workflows by direct await or background spawn via `start()`
19
- - [**Saga**](/cookbook/common-patterns/saga) — Coordinate multi-step transactions with automatic rollback when a step fails
20
- - [**Batching**](/cookbook/common-patterns/batching) — Process large collections in parallel batches with failure isolation
21
- - [**Rate Limiting**](/cookbook/common-patterns/rate-limiting) — Handle 429 responses and transient failures with RetryableError and backoff
22
- - [**Scheduling**](/cookbook/common-patterns/scheduling) — Use durable sleep to schedule actions minutes, hours, or weeks ahead
23
- - [**Timeouts**](/cookbook/common-patterns/timeouts) — Add deadlines to slow steps, hooks, and webhooks by racing them against a durable sleep
24
- - [**Idempotency**](/cookbook/common-patterns/idempotency) — Ensure side effects and duplicate starts are safe to retry
25
- - [**Webhooks**](/cookbook/common-patterns/webhooks) — Receive HTTP callbacks from external services and process them durably
17
+ - [**Sequential & Parallel Execution**](/cookbook/common-patterns/sequential-and-parallel): Compose steps with `await`, `Promise.all`, and `Promise.race` against durable sleeps and webhooks
18
+ - [**Workflow Composition**](/cookbook/common-patterns/workflow-composition): Call workflows from other workflows by direct await or background spawn via `start()`
19
+ - [**Saga**](/cookbook/common-patterns/saga): Coordinate multi-step transactions with automatic rollback when a step fails
20
+ - [**Batching**](/cookbook/common-patterns/batching): Process large collections in parallel batches with failure isolation
21
+ - [**Rate Limiting**](/cookbook/common-patterns/rate-limiting): Handle 429 responses and transient failures with RetryableError and backoff
22
+ - [**Scheduling**](/cookbook/common-patterns/scheduling): Use durable sleep to schedule actions minutes, hours, or weeks ahead
23
+ - [**Timeouts**](/cookbook/common-patterns/timeouts): Add deadlines to slow steps, hooks, and webhooks by racing them against a durable sleep
24
+ - [**Idempotency**](/cookbook/common-patterns/idempotency): Ensure side effects and duplicate starts are safe to retry
25
+ - [**Webhooks**](/cookbook/common-patterns/webhooks): Receive HTTP callbacks from external services and process them durably
26
26
 
27
27
  ## Integrations
28
28
 
29
- - [**AI SDK**](/cookbook/integrations/ai-sdk) — Use streamText() directly inside a workflow for lower-level control over model calls and tool execution
30
- - [**Chat SDK**](/cookbook/integrations/chat-sdk) — Build durable chat sessions with workflow persistence and AI SDK chat primitives
31
- - [**Sandbox**](/cookbook/integrations/sandbox) — Orchestrate Vercel Sandbox lifecycle inside durable workflows
29
+ - [**AI SDK**](/cookbook/integrations/ai-sdk): Use streamText() directly inside a workflow for lower-level control over model calls and tool execution
30
+ - [**Chat SDK**](/cookbook/integrations/chat-sdk): Build durable chat sessions with workflow persistence and AI SDK chat primitives
31
+ - [**Sandbox**](/cookbook/integrations/sandbox): Orchestrate Vercel Sandbox lifecycle inside durable workflows
32
32
 
33
33
  ## Advanced
34
34
 
35
- - [**Child Workflows**](/cookbook/advanced/child-workflows) — Spawn and orchestrate child workflows from a parent
36
- - [**Upgrading Workflows**](/cookbook/advanced/upgrading-workflows) — Identify a clean upgrade point in a long-running workflow and spawn a fresh run on the latest deployment carrying state forward
37
- - [**Serializable Steps**](/cookbook/advanced/serializable-steps) — Wrap non-serializable third-party objects so they cross the workflow boundary
38
- - [**Publishing Libraries**](/cookbook/advanced/publishing-libraries) — Ship npm packages that export reusable workflow functions
35
+ - [**Child Workflows**](/cookbook/advanced/child-workflows): Spawn and orchestrate child workflows from a parent
36
+ - [**Upgrading Workflows**](/cookbook/advanced/upgrading-workflows): Identify a clean upgrade point in a long-running workflow and spawn a fresh run on the latest deployment carrying state forward
37
+ - [**Serializable Steps**](/cookbook/advanced/serializable-steps): Wrap non-serializable third-party objects so they cross the workflow boundary
38
+ - [**Publishing Libraries**](/cookbook/advanced/publishing-libraries): Ship npm packages that export reusable workflow functions
@@ -2,7 +2,7 @@
2
2
  title: AI SDK
3
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 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.
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 large language model (LLM) calls inside a turn are not separately durable.
6
6
  related:
7
7
  - /docs/ai
8
8
  - /docs/ai/chat-session-modeling
@@ -15,27 +15,27 @@ related:
15
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
16
  />
17
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).
18
+ [AI SDK](https://ai-sdk.dev/) is Vercel's framework-agnostic TypeScript toolkit for building AI-powered apps and agents. It provides unified provider access, streaming, tool calling, structured output, and UI hooks. Workflow SDK makes the multi-turn loop durable, so the conversation state, hooks, and per-turn responses survive restarts and timeouts. In this pattern, the durability boundary is the entire turn, and individual tool calls inside a turn are **not** durable on their own (see [Pitfalls](#tools-are-not-individually-durable)).
19
19
 
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.
20
+ For the full AI SDK reference, including providers, `streamText`, `generateObject`, `useChat`, and tool calling, see the [AI SDK docs](https://ai-sdk.dev/docs). This page covers the Workflow-specific integration points.
21
21
 
22
22
  <Callout type="info">
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.
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.
24
24
  </Callout>
25
25
 
26
26
  ## When to use streamText directly
27
27
 
28
28
  Use [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) instead of `WorkflowAgent` when you need:
29
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`
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`
33
33
 
34
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).
35
35
 
36
36
  ## Multi-turn pattern
37
37
 
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.
38
+ One workflow run represents 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.
39
39
 
40
40
  <Callout type="info">
41
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.
@@ -57,8 +57,8 @@ export const turnHook = defineHook({ // [!code highlight]
57
57
  schema: z.object({ message: z.string() }),
58
58
  });
59
59
 
60
- // `streamText` runs tool executes inside `runTurn` (a step), so tool calls
61
- // are not individually durable — the entire turn retries together. See
60
+ // `streamText` runs tool execution inside `runTurn` (a step), so tool calls
61
+ // are not individually durable: the entire turn retries together. See
62
62
  // "Tools are not individually durable" below. Make side-effectful tools idempotent.
63
63
  async function lookupOrder({ orderId }: { orderId: string }) {
64
64
  const res = await fetch(`https://api.store.com/orders/${orderId}`);
@@ -86,7 +86,7 @@ const TOOLS = {
86
86
  },
87
87
  };
88
88
 
89
- // Per-turn step — streams one agent response to the durable writable // [!code highlight]
89
+ // Per-turn step: streams one agent response to the durable writable // [!code highlight]
90
90
  async function runTurn(messages: ModelMessage[]) {
91
91
  "use step";
92
92
 
@@ -99,7 +99,8 @@ async function runTurn(messages: ModelMessage[]) {
99
99
  });
100
100
 
101
101
  const writable = getWritable<UIMessageChunk>();
102
- // preventClose keeps the durable writable open so the next turn can // write to it. Each turn still emits its own start + finish chunks.
102
+ // preventClose keeps the durable writable open so the next turn can write
103
+ // to it. Each turn still emits its own start and finish chunks.
103
104
  await result.toUIMessageStream().pipeTo(writable, { preventClose: true }); // [!code highlight]
104
105
 
105
106
  const response = await result.response;
@@ -110,7 +111,7 @@ export async function supportWorkflow(initialMessages: ModelMessage[]) {
110
111
  "use workflow";
111
112
 
112
113
  const { workflowRunId } = getWorkflowMetadata();
113
- // Create the hook once, outside the loop — same token = HookConflictError // [!code highlight]
114
+ // Create the hook once, outside the loop: same token = HookConflictError // [!code highlight]
114
115
  const hook = turnHook.create({ token: workflowRunId }); // [!code highlight]
115
116
  let allMessages = initialMessages;
116
117
 
@@ -144,7 +145,8 @@ import { convertToModelMessages, createUIMessageStreamResponse } from "ai";
144
145
  import { start, getRun } from "workflow/api";
145
146
  import { supportWorkflow, turnHook } from "@/workflows/support";
146
147
 
147
- // Pump the durable stream until this turn's `finish` chunk, then close // the HTTP response. The source reader is released (not cancelled) so the
148
+ // Pump the durable stream until this turn's `finish` chunk, then close the
149
+ // HTTP response. The source reader is released (not canceled) so the
148
150
  // workflow's durable stream keeps flowing for the next turn.
149
151
  function sliceUntilFinish( // [!code highlight]
150
152
  source: ReadableStream<UIMessageChunk>
@@ -229,7 +231,7 @@ export async function POST(req: Request) {
229
231
  } catch (e: unknown) {
230
232
  const msg = e instanceof Error ? e.message.toLowerCase() : "";
231
233
  if (!msg.includes("not found") && !msg.includes("expired")) throw e;
232
- // Stale runId — fall through to start fresh
234
+ // Stale runId: fall through to start fresh
233
235
  }
234
236
  }
235
237
 
@@ -303,21 +305,21 @@ export function SupportChat() {
303
305
 
304
306
  ## How it works
305
307
 
306
- 1. **One workflow = one conversation.** The workflow loops on a hook, keeping `allMessages`, tool history, and state alive across turns.
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".
308
+ 1. **One workflow represents one conversation**: The workflow loops on a hook, keeping `allMessages`, tool history, and state alive across turns.
309
+ 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).
310
+ 3. **The hook is created once**: Call `turnHook.create({ token: workflowRunId })` outside the loop. Calling it twice with the same token throws `HookConflictError`.
311
+ 4. **`preventClose: true` keeps the writable open**: Set this option on `pipeTo` so the next turn can write to the durable writable.
312
+ 5. **`sliceUntilFinish` closes each HTTP response**: The API reads chunks until `type === "finish"`, then closes the HTTP response. The source reader is released, not canceled, so the workflow stream keeps flowing.
313
+ 6. **`startIndex: tailIndex + 1` returns only new chunks**: Each follow-up response avoids replaying previous turns.
314
+ 7. **`/done` exits the workflow**: The route resumes the hook so the workflow exits cleanly, then returns synthetic `start` and `finish` chunks so `useChat` transitions out of "streaming".
313
315
 
314
316
  ## Pitfalls
315
317
 
316
- Non-obvious correctness details worth knowing before adapting this pattern.
318
+ Review these correctness details before adapting this pattern.
317
319
 
318
320
  ### Tools are not individually durable
319
321
 
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.
322
+ `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 runs inline.
321
323
 
322
324
  The consequences:
323
325
 
@@ -327,8 +329,8 @@ The consequences:
327
329
 
328
330
  **Mitigations:**
329
331
 
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
+ - Make side-effectful tool implementations idempotent: deduplicate server-side on a stable key, such as `orderId` or an `Idempotency-Key` header.
333
+ - 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
334
 
333
335
  ### Snapshot `tailIndex` *before* resuming the hook
334
336
 
@@ -352,11 +354,11 @@ A `TransformStream` with `controller.terminate()` on the `finish` chunk seems li
352
354
 
353
355
  ### Release the source reader, don't cancel it
354
356
 
355
- In `sliceUntilFinish`, use `reader.releaseLock()` in the `finally` block rather than `source.cancel()`. Cancelling propagates upstream and closes the durable writable, breaking the next turn. Releasing the lock just detaches our reader; the durable stream keeps flowing.
357
+ In `sliceUntilFinish`, use `reader.releaseLock()` in the `finally` block rather than `source.cancel()`. Canceling propagates upstream and closes the durable writable, breaking the next turn. Releasing the lock only detaches our reader; the durable stream keeps flowing.
356
358
 
357
359
  ### Handle stale `runId` gracefully
358
360
 
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.
361
+ Clients can send a `runId` from a workflow that no longer exists, such as after using local storage, navigating back, or restarting the server. Wrap the follow-up path in a `try/catch` for `not found` or `expired`, then use the first-turn code path to start a new workflow.
360
362
 
361
363
  ### Make the first turn idempotent when needed
362
364
 
@@ -368,10 +370,10 @@ This example stores the `runId` after the first response. For strict one-session
368
370
  |---|---|---|
369
371
  | **Tool loop** | AI SDK handles via `stopWhen` | Handles internally (AI SDK–compatible options) |
370
372
  | **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 |
373
+ | **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
374
  | **Stop conditions** | `stopWhen`, `prepareStep` | `stopWhen`, `prepareStep` |
373
375
  | **Structured output** | `Output.object()`, `Output.array()` | `output` (`Output.object()`, `Output.text()`) |
374
- | **Step callbacks** | `onStepFinish`, `onChunk`, etc. | `onStepFinish`, `onFinish`, `onError`, `onAbort` (`onChunk` not available) |
376
+ | **Step callbacks** | `onStepFinish`, `onChunk`, and others | `onStepFinish`, `onFinish`, `onError`, `onAbort` (`onChunk` not available) |
375
377
  | **Setup** | Manual stream piping and turn slicing | Automatic |
376
378
 
377
379
  Use `WorkflowAgent` for most agent use cases. Use `streamText` when you need the raw AI SDK surface or a per-turn durability boundary.
@@ -380,17 +382,17 @@ Use `WorkflowAgent` for most agent use cases. Use `streamText` when you need the
380
382
 
381
383
  **AI SDK** ([docs](https://ai-sdk.dev/docs))
382
384
 
383
- * [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text) — core streaming function; `toUIMessageStream()` pipes into the durable writable
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))
385
- * [`stepCountIs()` / `stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/agents#stop-conditions) — bound the agent loop inside each turn
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
387
- * [`useChat()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat) — React hook that consumes the UI message stream on the client
385
+ * [`streamText()`](https://ai-sdk.dev/docs/reference/ai-sdk-core/stream-text): core streaming function; `toUIMessageStream()` pipes into the durable writable
386
+ * [`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))
387
+ * [`stepCountIs()` / `stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/agents#stop-conditions): bound the agent loop inside each turn
388
+ * [`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
389
+ * [`useChat()`](https://ai-sdk.dev/docs/reference/ai-sdk-ui/use-chat): React hook that consumes the UI message stream on the client
388
390
 
389
391
  **Workflow SDK**
390
392
 
391
- * [`"use step"`](/docs/foundations/workflows-and-steps#step-functions) — applied to `runTurn` to make each turn a durable, retryable unit
392
- * [`defineHook()`](/docs/api-reference/workflow/define-hook) — suspension point for follow-up messages
393
- * [`getWritable()`](/docs/api-reference/workflow/get-writable) — resumable stream output
394
- * [`getRun()`](/docs/api-reference/workflow-api/get-run) — `run.getReadable({ startIndex })` for slicing per-turn streams
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
393
+ * [`"use step"`](/docs/foundations/workflows-and-steps#step-functions): applied to `runTurn` to make each turn a durable, retryable unit
394
+ * [`defineHook()`](/docs/api-reference/workflow/define-hook): suspension point for follow-up messages
395
+ * [`getWritable()`](/docs/api-reference/workflow/get-writable): resumable stream output
396
+ * [`getRun()`](/docs/api-reference/workflow-api/get-run): `run.getReadable({ startIndex })` for slicing per-turn streams
397
+ * [`WorkflowChatTransport`](/docs/api-reference/workflow-ai/workflow-chat-transport): passes `runId` between turns
398
+ * [Idempotency](/docs/foundations/idempotency): protect duplicate-sensitive first turns and side effects
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  title: Chat SDK
3
- description: Make Chat SDK bot sessions durable — one workflow run per conversation thread, with hooks bridging inbound platform events into long-running agent logic.
3
+ description: Make Chat SDK bot sessions durable, with one workflow run per conversation thread and hooks bridging inbound platform events into long-running agent logic.
4
4
  type: guide
5
- summary: Chat SDK normalizes Slack, Teams, Discord, Telegram and friends into one thread/message model. Workflow SDK gives each thread a durable run that owns multi-turn state, can sleep for hours, and survives restarts.
5
+ summary: Chat SDK normalizes Slack, Teams, Discord, Telegram, and similar platforms into one thread and message model. Workflow SDK gives each thread a durable run that owns multi-turn state, can sleep for hours, and survives restarts.
6
6
  related:
7
7
  - /docs/cookbook/integrations/ai-sdk
8
8
  - /docs/cookbook/integrations/sandbox
@@ -15,13 +15,13 @@ related:
15
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
16
  />
17
17
 
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.
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. A single bot can support each platform. Chat SDK handles webhook verification, event normalization, subscriptions, and cross-platform features such as cards and modals.
19
19
 
20
20
  Workflow SDK complements it by making bot **sessions** durable. Each conversation thread maps to a long-running workflow run that:
21
21
 
22
22
  - Owns multi-turn state in the durable event log instead of Redis-by-hand bookkeeping
23
23
  - Can `sleep()` for hours or days waiting for a user reply, an approval, or a scheduled follow-up
24
- - Survives deploys, cold starts, and crashes — the session picks up from the last step on replay
24
+ - Survives deploys, cold starts, and crashes: the session picks up from the last step on replay
25
25
  - Receives follow-up messages via hooks, so the bot stays responsive while the workflow is still running
26
26
 
27
27
  <Callout type="info">
@@ -30,9 +30,9 @@ One thread mapped to one workflow run also means the thread stays on the deploym
30
30
 
31
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.
32
32
 
33
- ## How It Fits Together
33
+ ## How it fits together
34
34
 
35
- Chat SDK owns the edge — webhook verification, event routing, `thread.post()` / `thread.stream()`. Workflow owns the session — state, loops, sleeps, retries. They meet at exactly two points:
35
+ Chat SDK owns the edge: webhook verification, event routing, `thread.post()` / `thread.stream()`. Workflow owns the session: state, loops, sleeps, retries. They meet at exactly two points:
36
36
 
37
37
  ```mermaid
38
38
  flowchart TD
@@ -44,8 +44,8 @@ flowchart TD
44
44
  E --> F["&quot;use step&quot; helpers<br/>thread.post(), thread.subscribe(), thread.setState(), …"]
45
45
  ```
46
46
 
47
- - **Inbound** — Chat SDK handlers decide whether to `start(workflow, [thread, message])` or `resumeHook(runId, { message })`. The `runId` lives in Chat SDK's thread state (Redis, Postgres, or any state adapter).
48
- - **Outbound** — the workflow calls Chat SDK APIs (`thread.post()`, `thread.subscribe()`, `thread.setState()`) from inside step functions. Never from the top level of a workflow file — adapter packages use Node-only modules that aren't available in the workflow sandbox.
47
+ - **Inbound**: Chat SDK handlers decide whether to `start(workflow, [thread, message])` or `resumeHook(runId, { message })`. The `runId` lives in Chat SDK's thread state (Redis, Postgres, or any state adapter).
48
+ - **Outbound**: the workflow calls Chat SDK APIs (`thread.post()`, `thread.subscribe()`, `thread.setState()`) from inside step functions. Never from the top level of a workflow file, since adapter packages use Node-only modules that aren't available in the workflow sandbox.
49
49
 
50
50
  ## Why Workflow + Chat SDK
51
51
 
@@ -60,11 +60,11 @@ Workflow replaces all of that with a single durable function. The bot can:
60
60
  - Schedule a follow-up message 24 hours later via `sleep("24h")`
61
61
  - Pause on sandbox snapshot, resume when the user sends the next command (see the [Sandbox integration](/docs/cookbook/integrations/sandbox))
62
62
 
63
- Because the session *is* a workflow run, its history is recoverable from the event log — no separate message store to keep in sync.
63
+ Because the session *is* a workflow run, its history is recoverable from the event log, so there's no separate message store to keep in sync.
64
64
 
65
- ## The Pattern: One Thread = One Workflow Run
65
+ ## The pattern: one thread = one workflow run
66
66
 
67
- Three files. The bot definition is separate from the workflow so adapter packages stay out of the workflow sandbox.
67
+ This pattern uses three files. The bot definition is separate from the workflow so adapter packages stay out of the workflow sandbox.
68
68
 
69
69
  <Tabs items={['Bot Setup', 'Workflow', 'Event Handlers']}>
70
70
 
@@ -125,7 +125,7 @@ async function postAssistantMessage(
125
125
 
126
126
  async function runTurn(text: string) {
127
127
  "use step";
128
- // Your AI SDK call, database lookup, tool loop, etc.
128
+ // Your AI SDK call, database lookup, tool loop, and other operations.
129
129
  return `You said: ${text}`;
130
130
  }
131
131
 
@@ -157,7 +157,7 @@ export async function durableChatSession(payload: string) {
157
157
  if (!(await handleMessage(thread, message))) return;
158
158
 
159
159
  // Each hook resumption is one turn. The workflow stays suspended between
160
- // messages — zero compute cost while idle.
160
+ // messages: zero compute cost while idle.
161
161
  while (true) {
162
162
  const { message: nextRaw } = await hook; // [!code highlight]
163
163
  const next = Message.fromJSON(nextRaw);
@@ -204,7 +204,7 @@ async function startSession(thread: Thread<ThreadState>, message: Message) {
204
204
  async function routeTurn(thread: Thread<ThreadState>, message: Message) {
205
205
  const state = await thread.state;
206
206
 
207
- // No run yet, or the previous run finished — start fresh.
207
+ // No run yet, or the previous run finished: start fresh.
208
208
  if (!state?.runId || !(await getRun(state.runId).exists)) {
209
209
  await startSession(thread, message);
210
210
  return;
@@ -217,7 +217,7 @@ async function routeTurn(thread: Thread<ThreadState>, message: Message) {
217
217
  } catch (err) {
218
218
  const msg = err instanceof Error ? err.message.toLowerCase() : "";
219
219
  if (msg.includes("not found") || msg.includes("expired")) {
220
- // Stale runId — start a new session rather than dropping the message.
220
+ // Stale runId: start a new session rather than dropping the message.
221
221
  await startSession(thread, message);
222
222
  return;
223
223
  }
@@ -260,30 +260,30 @@ export async function POST(
260
260
 
261
261
  </Tabs>
262
262
 
263
- ## How It Works
263
+ ## How it works
264
264
 
265
- 1. **Thread state stores the `runId`.** Chat SDK's state adapter (Redis, Postgres, memory) holds `{ runId }` per thread. That's the only piece of glue between the two SDKs.
266
- 2. **First mention → `start()`.** Handler serializes `thread` + `message` with `toJSON()`, passes them through `start(durableChatSession, [payload])`, stashes the returned `runId` in thread state.
267
- 3. **Subsequent messages → `resumeHook()`.** Handler looks up the `runId`, serializes the new message, and resumes the workflow's hook. The workflow picks up on the next `await hook` iteration.
268
- 4. **Workflow posts back via steps.** All Chat SDK side effects (`thread.post`, `thread.subscribe`, `thread.setState`) happen inside `"use step"` helpers that dynamically import the bot. This keeps adapter packages outside the workflow sandbox.
269
- 5. **Session ends — two ways.** The workflow returns normally (user said `done`, approval granted, etc.), or the workflow throws. Either way the run completes; the next inbound message with the stale `runId` falls through to `startSession()`.
265
+ 1. **Thread state stores the `runId`**: Chat SDK's state adapter (Redis, Postgres, or memory) holds `{ runId }` per thread. This state connects the two SDKs.
266
+ 2. **The first mention calls `start()`**: The handler serializes `thread` and `message` with `toJSON()`, passes them through `start(durableChatSession, [payload])`, and stores the returned `runId` in thread state.
267
+ 3. **Subsequent messages call `resumeHook()`**: The handler looks up the `runId`, serializes the new message, and resumes the workflow's hook. The workflow continues on the next `await hook` iteration.
268
+ 4. **The workflow posts through steps**: All Chat SDK side effects (`thread.post`, `thread.subscribe`, and `thread.setState`) happen inside `"use step"` helpers that dynamically import the bot. This keeps adapter packages outside the workflow sandbox.
269
+ 5. **The session ends in two ways**: The workflow returns normally when the user sends `done` or an approval is granted, or the workflow throws. Either way, the run completes. The next inbound message with the stale `runId` falls through to `startSession()`.
270
270
 
271
271
  The workflow is fully durable between turns: `await hook` suspends with zero compute cost, and platform webhooks can fire from anywhere without concern for which server instance handled the previous turn.
272
272
 
273
- ## Extending the Pattern
273
+ ## Extending the pattern
274
274
 
275
- Because the session is just a workflow, everything else from the cookbook composes naturally:
275
+ Because the session is a workflow, everything else from the cookbook composes naturally:
276
276
 
277
- - **Stream AI SDK responses into the thread.** Use the [AI SDK integration](/docs/cookbook/integrations/ai-sdk) pattern inside a step, then pass `result.fullStream` to `thread.post()` — Chat SDK handles platform-specific streaming (Slack edit-in-place, Telegram message-per-chunk, etc.).
277
+ - **Stream AI SDK responses into the thread.** Use the [AI SDK integration](/docs/cookbook/integrations/ai-sdk) pattern inside a step, then pass `result.fullStream` to `thread.post()`. Chat SDK handles platform-specific streaming, including Slack edit-in-place and Telegram message-per-chunk.
278
278
  - **Give the bot a sandbox.** Combine with the [Sandbox integration](/docs/cookbook/integrations/sandbox): each thread gets its own persistent sandbox session, snapshots on idle, resumes on the next message. That's effectively a coding-agent bot.
279
279
  - **Human-in-the-loop approvals.** `Promise.race([hook, approvalHook])` inside the workflow, post buttons in the thread via [cards](https://chat-sdk.dev/docs/cards), resume `approvalHook` from `bot.onAction(...)`.
280
- - **Scheduled follow-ups.** `sleep("24h")` before a proactive check-in. Surviving restarts is free.
280
+ - **Scheduled follow-ups.** Call `sleep("24h")` before a proactive check-in. The workflow preserves the timer across restarts.
281
281
 
282
282
  ## Pitfalls
283
283
 
284
284
  ### Don't import the bot at the top of workflow files
285
285
 
286
- Adapter packages (`@chat-adapter/slack`, `@chat-adapter/telegram`, etc.) depend on Node-only modules that aren't available in the workflow bundler's sandbox. Keep `import { bot } from "@/lib/bot"` inside `"use step"` functions with `await import(...)`. Use `reviver` from `chat` for deserialization inside the workflow — it's standalone and has no adapter dependencies.
286
+ Adapter packages such as `@chat-adapter/slack` and `@chat-adapter/telegram` depend on Node-only modules that aren't available in the workflow bundler's sandbox. Keep `import { bot } from "@/lib/bot"` inside `"use step"` functions with `await import(...)`. Use `reviver` from `chat` for deserialization inside the workflow: it's standalone and has no adapter dependencies.
287
287
 
288
288
  ### Register the bot as a singleton
289
289
 
@@ -307,14 +307,14 @@ One `chatTurnHook.create({ token: workflowRunId })` per workflow run, reused eve
307
307
 
308
308
  ### Platform timeouts are separate from workflow timeouts
309
309
 
310
- Slack wants a 200 within 3 seconds. The webhook handler returns immediately after `resumeHook` (which is fast) — the workflow then runs in the background and posts back via `thread.post`. Don't try to `await` the whole turn inside the webhook handler; that's what breaks in the naive integration.
310
+ Slack requires an HTTP 200 response within 3s. The webhook handler returns after `resumeHook`, then the workflow runs in the background and posts through `thread.post`. Don't `await` the whole turn inside the webhook handler because that synchronous integration exceeds the platform timeout.
311
311
 
312
312
  ## Key APIs
313
313
 
314
- - [`Chat`](https://chat-sdk.dev/docs/api/chat) / [`Thread`](https://chat-sdk.dev/docs/api/thread) / [`Message`](https://chat-sdk.dev/docs/api/message) — Chat SDK primitives. `toJSON()` / `fromJSON()` / `reviver` are the serialization layer.
315
- - [`start()`](/docs/api-reference/workflow-api/start) — start a new session workflow. Store the returned `runId` in thread state.
316
- - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) — forward a new platform message to the running workflow.
317
- - [`getRun()`](/docs/api-reference/workflow-api/get-run) — `run.exists` before resuming, to detect stale `runId`s.
318
- - [`defineHook()`](/docs/api-reference/workflow/define-hook) — per-turn suspension point inside the workflow.
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.
314
+ - [`Chat`](https://chat-sdk.dev/docs/api/chat) / [`Thread`](https://chat-sdk.dev/docs/api/thread) / [`Message`](https://chat-sdk.dev/docs/api/message): Chat SDK primitives. `toJSON()` / `fromJSON()` / `reviver` are the serialization layer.
315
+ - [`start()`](/docs/api-reference/workflow-api/start): start a new session workflow. Store the returned `runId` in thread state.
316
+ - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook): forward a new platform message to the running workflow.
317
+ - [`getRun()`](/docs/api-reference/workflow-api/get-run): `run.exists` before resuming, to detect stale `runId`s.
318
+ - [`defineHook()`](/docs/api-reference/workflow/define-hook): per-turn suspension point inside the workflow.
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.