workflow 5.0.0-beta.5 → 5.0.0-beta.51

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 (256) hide show
  1. package/README.md +68 -23
  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 +3 -3
  6. package/dist/api.d.ts.map +1 -1
  7. package/dist/api.js +5 -7
  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 +20 -3
  12. package/dist/internal/builtins.d.ts.map +1 -1
  13. package/dist/internal/builtins.js +68 -4
  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 +4 -1
  28. package/docs/ai/chat-session-modeling.mdx +176 -422
  29. package/docs/ai/defining-tools.mdx +6 -7
  30. package/docs/ai/human-in-the-loop.mdx +11 -11
  31. package/docs/ai/index.mdx +71 -75
  32. package/docs/ai/message-queueing.mdx +71 -110
  33. package/docs/ai/meta.json +1 -0
  34. package/docs/ai/resumable-streams.mdx +40 -28
  35. package/docs/ai/sleep-and-delays.mdx +10 -10
  36. package/docs/ai/streaming-updates-from-tools.mdx +6 -6
  37. package/docs/api-reference/index.mdx +25 -1
  38. package/docs/api-reference/meta.json +8 -0
  39. package/docs/api-reference/vitest/index.mdx +9 -15
  40. package/docs/api-reference/workflow/create-hook.mdx +89 -10
  41. package/docs/api-reference/workflow/create-webhook.mdx +16 -15
  42. package/docs/api-reference/workflow/define-hook.mdx +35 -33
  43. package/docs/api-reference/workflow/fatal-error.mdx +30 -8
  44. package/docs/api-reference/workflow/fetch.mdx +14 -10
  45. package/docs/api-reference/workflow/get-step-metadata.mdx +2 -2
  46. package/docs/api-reference/workflow/get-workflow-metadata.mdx +3 -3
  47. package/docs/api-reference/workflow/get-writable.mdx +7 -7
  48. package/docs/api-reference/workflow/index.mdx +4 -1
  49. package/docs/api-reference/workflow/retryable-error.mdx +1 -1
  50. package/docs/api-reference/workflow/set-attributes.mdx +61 -0
  51. package/docs/api-reference/workflow/sleep.mdx +4 -4
  52. package/docs/api-reference/workflow-ai/durable-agent.mdx +63 -101
  53. package/docs/api-reference/workflow-ai/index.mdx +5 -5
  54. package/docs/api-reference/workflow-ai/workflow-chat-transport.mdx +67 -24
  55. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +26 -12
  56. package/docs/api-reference/workflow-api/get-run.mdx +43 -8
  57. package/docs/api-reference/workflow-api/index.mdx +6 -10
  58. package/docs/api-reference/workflow-api/resume-hook.mdx +73 -12
  59. package/docs/api-reference/workflow-api/resume-webhook.mdx +11 -9
  60. package/docs/api-reference/workflow-api/start.mdx +60 -13
  61. package/docs/api-reference/workflow-astro/index.mdx +18 -0
  62. package/docs/api-reference/workflow-astro/meta.json +4 -0
  63. package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
  64. package/docs/api-reference/workflow-errors/entity-conflict-error.mdx +4 -4
  65. package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
  66. package/docs/api-reference/workflow-errors/hook-not-found-error.mdx +8 -8
  67. package/docs/api-reference/workflow-errors/index.mdx +88 -0
  68. package/docs/api-reference/workflow-errors/meta.json +6 -0
  69. package/docs/api-reference/workflow-errors/precondition-failed-error.mdx +68 -0
  70. package/docs/api-reference/workflow-errors/run-expired-error.mdx +2 -2
  71. package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
  72. package/docs/api-reference/workflow-errors/step-not-registered-error.mdx +5 -5
  73. package/docs/api-reference/workflow-errors/throttle-error.mdx +2 -2
  74. package/docs/api-reference/workflow-errors/too-early-error.mdx +2 -2
  75. package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
  76. package/docs/api-reference/workflow-errors/workflow-not-registered-error.mdx +5 -6
  77. package/docs/api-reference/workflow-errors/workflow-run-cancelled-error.mdx +6 -6
  78. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +5 -5
  79. package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
  80. package/docs/api-reference/workflow-errors/workflow-run-not-found-error.mdx +4 -4
  81. package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
  82. package/docs/api-reference/workflow-errors/workflow-world-error.mdx +8 -8
  83. package/docs/api-reference/workflow-globals.mdx +14 -10
  84. package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
  85. package/docs/api-reference/workflow-nest/index.mdx +31 -0
  86. package/docs/api-reference/workflow-nest/meta.json +9 -0
  87. package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
  88. package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
  89. package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
  90. package/docs/api-reference/workflow-next/with-workflow.mdx +39 -17
  91. package/docs/api-reference/workflow-nitro/index.mdx +60 -0
  92. package/docs/api-reference/workflow-nuxt/index.mdx +48 -0
  93. package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
  94. package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
  95. package/docs/api-reference/workflow-observability/index.mdx +62 -0
  96. package/docs/api-reference/workflow-observability/meta.json +11 -0
  97. package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
  98. package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
  99. package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
  100. package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
  101. package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
  102. package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
  103. package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +11 -14
  104. package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
  105. package/docs/api-reference/workflow-runtime/index.mdx +41 -0
  106. package/docs/api-reference/workflow-runtime/meta.json +12 -0
  107. package/docs/api-reference/workflow-runtime/set-world.mdx +51 -0
  108. package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +43 -0
  109. package/docs/api-reference/workflow-runtime/world/analytics.mdx +315 -0
  110. package/docs/api-reference/workflow-runtime/world/index.mdx +60 -0
  111. package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
  112. package/docs/api-reference/workflow-runtime/world/queue.mdx +88 -0
  113. package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +98 -34
  114. package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +8 -8
  115. package/docs/api-reference/workflow-serde/index.mdx +1 -2
  116. package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +3 -4
  117. package/docs/api-reference/workflow-serde/workflow-serialize.mdx +8 -8
  118. package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
  119. package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
  120. package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
  121. package/docs/api-reference/workflow-vite/index.mdx +18 -0
  122. package/docs/api-reference/workflow-vite/meta.json +4 -0
  123. package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
  124. package/docs/changelog/attributes-mvp.mdx +380 -0
  125. package/docs/changelog/batched-event-writes.mdx +79 -0
  126. package/docs/changelog/eager-processing.mdx +110 -436
  127. package/docs/changelog/index.mdx +4 -2
  128. package/docs/changelog/lazy-event-creation.md +127 -0
  129. package/docs/changelog/lazy-hook-resume.mdx +78 -0
  130. package/docs/changelog/meta.json +11 -1
  131. package/docs/changelog/resilient-resume.mdx +32 -0
  132. package/docs/changelog/resilient-start.mdx +33 -285
  133. package/docs/changelog/step-message-ownership.mdx +360 -0
  134. package/docs/changelog/turbo-mode.md +87 -0
  135. package/docs/comparisons/index.mdx +66 -0
  136. package/docs/comparisons/meta.json +11 -0
  137. package/docs/comparisons/workflow-sdk-vs-aws-agentcore.mdx +55 -0
  138. package/docs/comparisons/workflow-sdk-vs-aws-step-functions.mdx +111 -0
  139. package/docs/comparisons/workflow-sdk-vs-cloudflare-workflows.mdx +71 -0
  140. package/docs/comparisons/workflow-sdk-vs-inngest.mdx +102 -0
  141. package/docs/comparisons/workflow-sdk-vs-temporal.mdx +123 -0
  142. package/docs/comparisons/workflow-sdk-vs-trigger-dev.mdx +104 -0
  143. package/docs/configuration/build-and-diagnostics.mdx +70 -0
  144. package/docs/configuration/cli-and-web-ui.mdx +241 -0
  145. package/docs/configuration/framework-options.mdx +165 -0
  146. package/docs/configuration/index.mdx +32 -0
  147. package/docs/configuration/meta.json +12 -0
  148. package/docs/configuration/runtime-tuning.mdx +381 -0
  149. package/docs/configuration/worlds.mdx +313 -0
  150. package/docs/cookbook/advanced/child-workflows.mdx +211 -264
  151. package/docs/cookbook/advanced/meta.json +6 -1
  152. package/docs/cookbook/advanced/publishing-libraries.mdx +65 -56
  153. package/docs/cookbook/advanced/serializable-steps.mdx +48 -68
  154. package/docs/cookbook/advanced/upgrading-workflows.mdx +199 -0
  155. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +78 -60
  156. package/docs/cookbook/agent-patterns/durable-agent.mdx +23 -131
  157. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +180 -195
  158. package/docs/cookbook/common-patterns/batching.mdx +18 -14
  159. package/docs/cookbook/common-patterns/idempotency.mdx +41 -53
  160. package/docs/cookbook/common-patterns/rate-limiting.mdx +8 -4
  161. package/docs/cookbook/common-patterns/saga.mdx +23 -19
  162. package/docs/cookbook/common-patterns/scheduling.mdx +34 -22
  163. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +29 -25
  164. package/docs/cookbook/common-patterns/timeouts.mdx +26 -21
  165. package/docs/cookbook/common-patterns/webhooks.mdx +10 -6
  166. package/docs/cookbook/common-patterns/workflow-composition.mdx +30 -27
  167. package/docs/cookbook/index.mdx +22 -21
  168. package/docs/cookbook/integrations/ai-sdk.mdx +86 -48
  169. package/docs/cookbook/integrations/chat-sdk.mdx +50 -33
  170. package/docs/cookbook/integrations/sandbox.mdx +62 -45
  171. package/docs/deploying.mdx +95 -0
  172. package/docs/errors/abort-signal-timeout-in-workflow.mdx +16 -12
  173. package/docs/errors/corrupted-event-log.mdx +39 -18
  174. package/docs/errors/deployment-mismatch.mdx +71 -0
  175. package/docs/errors/fetch-in-workflow.mdx +15 -14
  176. package/docs/errors/hook-conflict.mdx +69 -13
  177. package/docs/errors/index.mdx +2 -36
  178. package/docs/errors/node-js-module-in-workflow.mdx +9 -5
  179. package/docs/errors/replay-divergence.mdx +27 -0
  180. package/docs/errors/run-expired.mdx +85 -0
  181. package/docs/errors/runtime-decryption-failed.mdx +77 -0
  182. package/docs/errors/serialization-failed.mdx +44 -12
  183. package/docs/errors/start-invalid-workflow-function.mdx +9 -5
  184. package/docs/errors/step-executed-multiple-times.mdx +23 -0
  185. package/docs/errors/step-not-registered.mdx +6 -6
  186. package/docs/errors/timeout-in-workflow.mdx +12 -8
  187. package/docs/errors/webhook-invalid-respond-with-value.mdx +18 -18
  188. package/docs/errors/webhook-response-not-sent.mdx +20 -16
  189. package/docs/errors/workflow-not-registered.mdx +5 -5
  190. package/docs/foundations/cancellation.mdx +31 -32
  191. package/docs/foundations/errors-and-retries.mdx +42 -11
  192. package/docs/foundations/hooks.mdx +64 -35
  193. package/docs/foundations/idempotency.mdx +244 -12
  194. package/docs/foundations/index.mdx +1 -23
  195. package/docs/foundations/meta.json +2 -1
  196. package/docs/foundations/serialization.mdx +21 -22
  197. package/docs/foundations/starting-workflows.mdx +106 -30
  198. package/docs/foundations/streaming.mdx +108 -60
  199. package/docs/foundations/versioning.mdx +263 -0
  200. package/docs/foundations/workflows-and-steps.mdx +9 -9
  201. package/docs/getting-started/astro.mdx +22 -18
  202. package/docs/getting-started/express.mdx +15 -11
  203. package/docs/getting-started/fastify.mdx +15 -11
  204. package/docs/getting-started/hono.mdx +15 -11
  205. package/docs/getting-started/index.mdx +10 -3
  206. package/docs/getting-started/meta.json +3 -1
  207. package/docs/getting-started/nestjs.mdx +87 -20
  208. package/docs/getting-started/next.mdx +22 -16
  209. package/docs/getting-started/nitro.mdx +22 -18
  210. package/docs/getting-started/nuxt.mdx +15 -11
  211. package/docs/getting-started/python.mdx +190 -41
  212. package/docs/getting-started/react-router/index.mdx +33 -0
  213. package/docs/getting-started/react-router/meta.json +5 -0
  214. package/docs/getting-started/react-router/v7.mdx +237 -0
  215. package/docs/getting-started/react-router/v8.mdx +232 -0
  216. package/docs/getting-started/sveltekit.mdx +20 -16
  217. package/docs/getting-started/tanstack-start.mdx +17 -13
  218. package/docs/getting-started/vite.mdx +15 -11
  219. package/docs/how-it-works/cancellation.mdx +63 -63
  220. package/docs/how-it-works/code-transform.mdx +83 -67
  221. package/docs/how-it-works/encryption.mdx +30 -26
  222. package/docs/how-it-works/event-sourcing.mdx +125 -34
  223. package/docs/how-it-works/framework-integrations.mdx +96 -337
  224. package/docs/how-it-works/understanding-directives.mdx +22 -22
  225. package/docs/internal/index.mdx +6 -4
  226. package/docs/internal/meta.json +6 -1
  227. package/docs/internal/nitro-native-build.mdx +38 -0
  228. package/docs/internal/nitro-web-ui.mdx +24 -0
  229. package/docs/internal/serializable-abort-controller.mdx +7 -7
  230. package/docs/meta.json +3 -2
  231. package/docs/observability/attributes.mdx +134 -0
  232. package/docs/observability/index.mdx +32 -10
  233. package/docs/observability/meta.json +1 -1
  234. package/docs/observability/retention.mdx +95 -0
  235. package/docs/observability/tracing.mdx +124 -0
  236. package/docs/testing/index.mdx +36 -36
  237. package/docs/testing/server-based.mdx +10 -10
  238. package/docs/whats-new.mdx +186 -0
  239. package/package.json +17 -14
  240. package/docs/api-reference/workflow-api/world/index.mdx +0 -58
  241. package/docs/api-reference/workflow-api/world/meta.json +0 -4
  242. package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
  243. package/docs/api-reference/workflow-api/world/queue.mdx +0 -86
  244. package/docs/deploying/building-a-world.mdx +0 -251
  245. package/docs/deploying/index.mdx +0 -95
  246. package/docs/deploying/meta.json +0 -4
  247. package/docs/deploying/world/local-world.mdx +0 -84
  248. package/docs/deploying/world/meta.json +0 -4
  249. package/docs/deploying/world/postgres-world.mdx +0 -224
  250. package/docs/deploying/world/vercel-world.mdx +0 -179
  251. package/docs/migration-guides/index.mdx +0 -34
  252. package/docs/migration-guides/meta.json +0 -9
  253. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +0 -363
  254. package/docs/migration-guides/migrating-from-inngest.mdx +0 -314
  255. package/docs/migration-guides/migrating-from-temporal.mdx +0 -318
  256. package/docs/migration-guides/migrating-from-trigger-dev.mdx +0 -337
@@ -2,9 +2,13 @@
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
+ <CopyPrompt
9
+ text="Implement durable webhook handling for this workflow. Use `createWebhook()` from `workflow` inside the &quot;use workflow&quot; function when the workflow needs a generated callback URL. Use `createWebhook({ respondWith: &quot;manual&quot; })` when the workflow must validate the request before sending an HTTP response. Await the webhook request, pass the `RequestWithResponse` into a &quot;use step&quot; function for validation and side effects, and call `request.respondWith(Response.json(...))` on every code path. Race the webhook against `sleep()` for deadlines and throw `FatalError` for permanent timeout/failure paths. For fixed public callbacks or large payloads, use `defineHook()` plus `resumeHook()` from `workflow/api` and pass only a token or blob reference into the hook payload. Verify success response, invalid response, timeout, duplicate callback, and large-payload behavior."
10
+ />
11
+
8
12
  Use webhooks when external services push events to your application via HTTP callbacks. The workflow creates a webhook URL, suspends with zero compute cost, and resumes when a request arrives.
9
13
 
10
14
  ## When to use this
@@ -13,7 +17,7 @@ Use webhooks when external services push events to your application via HTTP cal
13
17
  - Waiting for third-party verification or processing results
14
18
  - Any integration where an external system calls you back asynchronously
15
19
 
16
- ## Pattern: Processing webhook events
20
+ ## Pattern: processing webhook events
17
21
 
18
22
  Create a webhook with manual response control, then iterate over incoming requests:
19
23
 
@@ -73,7 +77,7 @@ async function processEvent(
73
77
  }
74
78
  ```
75
79
 
76
- ## Pattern: Async request-reply with timeout
80
+ ## Pattern: async request-reply with timeout
77
81
 
78
82
  Submit a request to an external service, pass it your webhook URL, then race the callback against a deadline:
79
83
 
@@ -124,7 +128,7 @@ async function processCallback(
124
128
  }
125
129
  ```
126
130
 
127
- ## Pattern: Large payload by reference
131
+ ## Pattern: large payload by reference
128
132
 
129
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:
130
134
 
@@ -139,7 +143,7 @@ export async function importLargeFile(importId: string) {
139
143
  // Suspend until the external system signals the blob is uploaded
140
144
  const { blobToken } = await blobReady.create({ token: `upload:${importId}` }); // [!code highlight]
141
145
 
142
- // Process by reference -- the full payload never enters the event log
146
+ // Process by reference. The full payload never enters the event log.
143
147
  await processBlob(blobToken);
144
148
 
145
149
  return { importId, blobToken, status: "indexed" };
@@ -173,7 +177,7 @@ export async function POST(request: Request) {
173
177
  - **`for await` on a webhook** lets you process multiple events from the same URL. Use `break` to stop listening after a terminal event.
174
178
  - **Webhooks auto-generate URLs** at `/.well-known/workflow/v1/webhook/:token`. Pass this URL to external services.
175
179
  - **Race webhooks against `sleep()`** for deadlines. If the callback doesn't arrive in time, the workflow can take a fallback action.
176
- - **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.
177
181
 
178
182
  ## Key APIs
179
183
 
@@ -2,25 +2,30 @@
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
+ - /cookbook/common-patterns/idempotency
8
9
  - /docs/api-reference/workflow-api/start
9
10
  - /docs/api-reference/workflow-api/get-run
10
11
  ---
11
12
 
12
- Workflows can call other workflows. Choose between two composition modes depending on whether the parent needs the child's result inline (direct await) or wants to fire the child off as an independent run (background spawn). For massive fan-out with polling and partial-failure handling, see [Child Workflows](/cookbook/advanced/child-workflows).
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."
15
+ />
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 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).
13
18
 
14
19
  ## When to use this
15
20
 
16
- - **Direct await** — the parent needs the child's result before continuing, and you want a single unified event log
17
- - **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`
18
23
 
19
24
  ## Pattern
20
25
 
21
26
  ### Direct await (flattening)
22
27
 
23
- 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.
24
29
 
25
30
  ```typescript lineNumbers
26
31
  declare function sendEmail(userId: string): Promise<void>; // @setup
@@ -53,7 +58,7 @@ The parent waits for the child to finish before continuing. Both functions share
53
58
 
54
59
  ### Background spawn via `start()`
55
60
 
56
- To run a child workflow independently without blocking the parent, call [`start()`](/docs/api-reference/workflow-api/start) from a step. This launches the child as a separate workflow run with its own `runId`.
61
+ To run a child workflow independently without blocking the parent, call [`start()`](/docs/api-reference/workflow-api/start) from the parent workflow. This launches the child as a separate workflow run with its own `runId`.
57
62
 
58
63
  ```typescript lineNumbers
59
64
  import { start } from "workflow/api";
@@ -62,37 +67,34 @@ declare function generateReport(reportId: string): Promise<void>; // @setup
62
67
  declare function fulfillOrder(orderId: string): Promise<{ id: string }>; // @setup
63
68
  declare function sendConfirmation(orderId: string): Promise<void>; // @setup
64
69
 
65
- async function triggerReportGeneration(reportId: string) {
66
- "use step"; // [!code highlight]
67
-
68
- const run = await start(generateReport, [reportId]); // [!code highlight]
69
- return run.runId;
70
- }
71
-
72
70
  export async function processOrder(orderId: string) {
73
71
  "use workflow";
74
72
 
75
73
  const order = await fulfillOrder(orderId);
76
74
 
77
- const reportRunId = await triggerReportGeneration(orderId); // [!code highlight]
75
+ const reportRun = await start(generateReport, [orderId]); // [!code highlight]
78
76
 
79
77
  await sendConfirmation(orderId);
80
78
 
81
- return { orderId, reportRunId };
79
+ return { orderId, reportRunId: reportRun.runId };
82
80
  }
83
81
  ```
84
82
 
85
- 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
+
85
+ <Callout type="info">
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
+ </Callout>
86
88
 
87
89
  <Callout type="info">
88
- If you want the child workflow to run on the latest deployment rather than the current one, pass [`deploymentId: "latest"`](/docs/api-reference/workflow-api/start#using-deploymentid-latest) in the `start()` options. This is currently a Vercel-specific feature. Be aware that the child workflow's function name, file path, argument types, and return type must remain compatible across deployments — renaming the function or changing its location will change the workflow ID, and modifying expected inputs or outputs can cause serialization failures.
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.
89
91
  </Callout>
90
92
 
91
93
  ## How it works
92
94
 
93
- 1. **Direct await flattens.** When a workflow function awaits another workflow function, the child's `"use workflow"` directive is treated as inline — the child's steps emit into the parent's event log and share the parent's run ID.
94
- 2. **`start()` mints a new run.** The child gets its own `runId`, its own event log, and its own retry boundary. The parent only sees the `runId` returned by `start()`.
95
- 3. **`start()` must be called from a step.** Calling `start()` directly from a workflow function is not allowed — wrap it in a `"use step"` function. This keeps the spawn deterministic across replays.
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.
96
98
 
97
99
  ## Choosing between the two modes
98
100
 
@@ -106,13 +108,14 @@ If you want the child workflow to run on the latest deployment rather than the c
106
108
 
107
109
  ## Adapting to your use case
108
110
 
109
- - **Spawn many children at once** — call `start()` in a loop inside a step. For more advanced fan-out (chunking, polling, partial-failure handling), graduate to the [Child Workflows](/cookbook/advanced/child-workflows) recipe.
110
- - **Wait for a background child to finish** — combine `start()` with `getRun()` polling. The [Child Workflows](/cookbook/advanced/child-workflows) page covers the full polling loop.
111
- - **Pass results back from background children** — the spawn step returns the `runId`; later, a poll step uses `getRun(runId).returnValue` to fetch the final result.
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`.
112
114
 
113
115
  ## Key APIs
114
116
 
115
- - [`"use workflow"`](/docs/foundations/workflows-and-steps) — marks the orchestrator function
116
- - [`"use step"`](/docs/foundations/workflows-and-steps) — marks functions with full Node.js access
117
- - [`start()`](/docs/api-reference/workflow-api/start) — spawn a child workflow as a separate run
118
- - [`getRun()`](/docs/api-reference/workflow-api/get-run) — retrieve a workflow run's status and return value
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,34 +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
- - [**Durable Agent**](/cookbook/agent-patterns/durable-agent) — Replace a stateless AI agent with one that survives crashes and retries tool calls
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 happen exactly once, even when steps 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
- - [**Serializable Steps**](/cookbook/advanced/serializable-steps) — Wrap non-serializable third-party objects so they cross the workflow boundary
37
- - [**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
@@ -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 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
@@ -11,26 +11,35 @@ 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
+ />
15
17
 
16
- For the full AI SDK reference (providers, `streamText`, `generateObject`, `useChat`, tool calling, etc.) see the [AI SDK docs](https://ai-sdk.dev/docs). This page covers the Workflow-specific integration points.
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
+
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.
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
- 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
+
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>
34
43
 
35
44
  <Tabs items={['Workflow', 'API Route', 'Client']}>
36
45
 
@@ -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 execution 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 }),
@@ -76,12 +86,12 @@ const TOOLS = {
76
86
  },
77
87
  };
78
88
 
79
- // 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]
80
90
  async function runTurn(messages: ModelMessage[]) {
81
91
  "use step";
82
92
 
83
93
  const result = streamText({
84
- model: "anthropic/claude-haiku-4.5",
94
+ model: "spacexai/grok-4.6",
85
95
  system: "You are a customer support agent.",
86
96
  messages,
87
97
  tools: TOOLS,
@@ -89,7 +99,8 @@ async function runTurn(messages: ModelMessage[]) {
89
99
  });
90
100
 
91
101
  const writable = getWritable<UIMessageChunk>();
92
- // 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.
93
104
  await result.toUIMessageStream().pipeTo(writable, { preventClose: true }); // [!code highlight]
94
105
 
95
106
  const response = await result.response;
@@ -100,7 +111,7 @@ export async function supportWorkflow(initialMessages: ModelMessage[]) {
100
111
  "use workflow";
101
112
 
102
113
  const { workflowRunId } = getWorkflowMetadata();
103
- // 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]
104
115
  const hook = turnHook.create({ token: workflowRunId }); // [!code highlight]
105
116
  let allMessages = initialMessages;
106
117
 
@@ -124,13 +135,18 @@ export async function supportWorkflow(initialMessages: ModelMessage[]) {
124
135
 
125
136
  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
137
 
138
+ <Callout type="info">
139
+ 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).
140
+ </Callout>
141
+
127
142
  ```typescript title="app/api/support/route.ts" lineNumbers
128
143
  import type { UIMessage, UIMessageChunk } from "ai";
129
144
  import { convertToModelMessages, createUIMessageStreamResponse } from "ai";
130
145
  import { start, getRun } from "workflow/api";
131
146
  import { supportWorkflow, turnHook } from "@/workflows/support";
132
147
 
133
- // 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
134
150
  // workflow's durable stream keeps flowing for the next turn.
135
151
  function sliceUntilFinish( // [!code highlight]
136
152
  source: ReadableStream<UIMessageChunk>
@@ -215,7 +231,7 @@ export async function POST(req: Request) {
215
231
  } catch (e: unknown) {
216
232
  const msg = e instanceof Error ? e.message.toLowerCase() : "";
217
233
  if (!msg.includes("not found") && !msg.includes("expired")) throw e;
218
- // Stale runId — fall through to start fresh
234
+ // Stale runId: fall through to start fresh
219
235
  }
220
236
  }
221
237
 
@@ -240,7 +256,7 @@ Store the `runId` in a ref and pass it in the body of every follow-up. `Workflow
240
256
  "use client";
241
257
 
242
258
  import { useChat } from "@ai-sdk/react";
243
- import { WorkflowChatTransport } from "@workflow/ai";
259
+ import { WorkflowChatTransport } from "@ai-sdk/workflow";
244
260
  import { useMemo, useRef, useState } from "react";
245
261
 
246
262
  export function SupportChat() {
@@ -289,16 +305,32 @@ export function SupportChat() {
289
305
 
290
306
  ## How it works
291
307
 
292
- 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".
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".
298
315
 
299
316
  ## Pitfalls
300
317
 
301
- Non-obvious correctness details worth knowing before adapting this pattern.
318
+ Review these correctness details before adapting this pattern.
319
+
320
+ ### Tools are not individually durable
321
+
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.
323
+
324
+ The consequences:
325
+
326
+ - The atomic retry unit is the entire `runTurn`, not the individual tool call.
327
+ - If `processRefund` succeeds and then the model call (or a later tool) throws, the whole turn retries, and `processRefund` will run again.
328
+ - Tool calls do not appear as separate entries in the event log or observability dashboard.
329
+
330
+ **Mitigations:**
331
+
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.
302
334
 
303
335
  ### Snapshot `tailIndex` *before* resuming the hook
304
336
 
@@ -322,39 +354,45 @@ A `TransformStream` with `controller.terminate()` on the `finish` chunk seems li
322
354
 
323
355
  ### Release the source reader, don't cancel it
324
356
 
325
- 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.
326
358
 
327
359
  ### Handle stale `runId` gracefully
328
360
 
329
- 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.
362
+
363
+ ### Make the first turn idempotent when needed
364
+
365
+ 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).
330
366
 
331
- ## streamText vs DurableAgent
367
+ ## streamText vs WorkflowAgent
332
368
 
333
- | | `streamText()` | `DurableAgent` |
369
+ | | `streamText()` (this pattern) | `WorkflowAgent` |
334
370
  |---|---|---|
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 |
371
+ | **Tool loop** | AI SDK handles via `stopWhen` | Handles internally (AI SDK–compatible options) |
372
+ | **LLM call durability** | Re-executes with the parent turn | Each LLM call is a durable step |
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 |
374
+ | **Stop conditions** | `stopWhen`, `prepareStep` | `stopWhen`, `prepareStep` |
375
+ | **Structured output** | `Output.object()`, `Output.array()` | `output` (`Output.object()`, `Output.text()`) |
376
+ | **Step callbacks** | `onStepFinish`, `onChunk`, and others | `onStepEnd`, `onEnd`, `onError`, `onAbort` (`onChunk` not available) |
377
+ | **Setup** | Manual stream piping and turn slicing | Automatic |
341
378
 
342
- Use `DurableAgent` for most agent use cases. Use `streamText` when you need the additional control.
379
+ Use `WorkflowAgent` for most agent use cases. Use `streamText` when you need the raw AI SDK surface or a per-turn durability boundary.
343
380
 
344
381
  ## Key APIs
345
382
 
346
383
  **AI SDK** ([docs](https://ai-sdk.dev/docs))
347
384
 
348
- * [`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
350
- * [`stepCountIs()` / `stopWhen`](https://ai-sdk.dev/docs/ai-sdk-core/agents#stop-conditions) — bound the agent loop inside each turn
351
- * [`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
- * [`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
353
390
 
354
391
  **Workflow SDK**
355
392
 
356
- * [`"use step"`](/docs/api-reference/workflow/use-step) — makes tool executions durable
357
- * [`defineHook()`](/docs/api-reference/workflow/define-hook) — suspension point for follow-up messages
358
- * [`getWritable()`](/docs/api-reference/workflow/get-writable) — resumable stream output
359
- * [`getRun()`](/docs/api-reference/workflow-api/get-run) — `run.getReadable({ startIndex })` for slicing per-turn streams
360
- * [`WorkflowChatTransport`](/docs/api-reference/workflow-ai/workflow-chat-transport) — passes `runId` between turns
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