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
@@ -1,8 +1,8 @@
1
1
  ---
2
2
  title: Chat Session Modeling
3
- description: Model chat sessions at different architectural layers to control state ownership and handle interruptions.
3
+ description: Model WorkflowAgent chat sessions at different architectural layers to control state ownership and handle interruptions.
4
4
  type: guide
5
- summary: Choose between single-turn and multi-turn workflow patterns for managing chat session state.
5
+ summary: Choose between single-turn and multi-turn WorkflowAgent patterns for managing chat session state.
6
6
  prerequisites:
7
7
  - /docs/ai
8
8
  - /docs/foundations/workflows-and-steps
@@ -10,525 +10,278 @@ related:
10
10
  - /docs/ai/message-queueing
11
11
  - /docs/ai/resumable-streams
12
12
  - /docs/foundations/hooks
13
- - /docs/api-reference/workflow-ai/durable-agent
14
13
  - /docs/api-reference/workflow/define-hook
15
14
  ---
16
15
 
17
- Chat sessions in AI agents can be modeled at different layers of your architecture. The choice affects state ownership and how you handle interruptions and reconnections.
16
+ Chat sessions can be modeled at different layers of your architecture. The choice determines who owns message history, how long a workflow run stays active, and how clients reconnect after an interruption.
18
17
 
19
- While there are many ways to model chat sessions, the two most common categories are single-turn and multi-turn.
18
+ Workflow 5 applications should use AI SDK's [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent) for both patterns.
20
19
 
21
- ## Single-Turn Workflows
20
+ ## Single-turn workflows
22
21
 
23
- Each user message triggers a new workflow run. The client or API route owns the conversation history and sends the full message array with each request.
22
+ Each user turn starts a new workflow run. The client or API owns conversation history and sends the complete `UIMessage[]` array with every request.
24
23
 
25
- <Tabs items={['Workflow', 'API Route', 'Client']}>
24
+ ### Workflow
26
25
 
27
- <Tab value="Workflow">
26
+ Convert UI messages to model messages inside the workflow. `WorkflowAgent` writes durable `ModelCallStreamPart` values to the run stream.
28
27
 
29
- ```typescript title="workflows/chat/index.ts" lineNumbers
30
- import { DurableAgent } from "@workflow/ai/agent";
28
+ ```typescript title="workflows/chat.ts" lineNumbers
29
+ import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
30
+ import { convertToModelMessages, type UIMessage } from "ai";
31
31
  import { getWritable } from "workflow";
32
- import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./steps/tools";
33
- import { convertToModelMessages, type UIMessage, type UIMessageChunk } from "ai";
32
+ import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./tools";
34
33
 
35
34
  export async function chat(messages: UIMessage[]) {
36
35
  "use workflow";
37
36
 
38
- const writable = getWritable<UIMessageChunk>();
39
-
40
- const agent = new DurableAgent({
41
- model: "bedrock/claude-haiku-4-5-20251001-v1",
37
+ const agent = new WorkflowAgent({
38
+ model: "spacexai/grok-4.6",
42
39
  instructions: FLIGHT_ASSISTANT_PROMPT,
43
40
  tools: flightBookingTools,
44
41
  });
45
42
 
46
- await agent.stream({
47
- messages: await convertToModelMessages(messages), // [!code highlight] Full history from client
48
- writable,
43
+ return agent.stream({
44
+ messages: await convertToModelMessages(messages),
45
+ writable: getWritable<ModelCallStreamPart>(),
49
46
  });
50
47
  }
51
48
  ```
52
49
 
53
- </Tab>
50
+ ### API route
54
51
 
55
- <Tab value="API Route">
52
+ Convert the durable model-call stream to AI SDK UI chunks at the response boundary:
56
53
 
57
54
  ```typescript title="app/api/chat/route.ts" lineNumbers
55
+ import { createModelCallToUIChunkTransform } from "@ai-sdk/workflow";
58
56
  import { createUIMessageStreamResponse, type UIMessage } from "ai";
59
57
  import { start } from "workflow/api";
60
58
  import { chat } from "@/workflows/chat";
61
59
 
62
- export async function POST(req: Request) {
63
- const { messages }: { messages: UIMessage[] } = await req.json();
64
-
65
- const run = await start(chat, [messages]); // [!code highlight]
60
+ export async function POST(request: Request) {
61
+ const { messages }: { messages: UIMessage[] } = await request.json();
62
+ const run = await start(chat, [messages]);
66
63
 
67
64
  return createUIMessageStreamResponse({
68
- stream: run.readable,
69
- headers: {
70
- "x-workflow-run-id": run.runId, // [!code highlight] For stream reconnection
71
- },
65
+ stream: run.readable.pipeThrough(createModelCallToUIChunkTransform()),
66
+ headers: { "x-workflow-run-id": run.runId },
72
67
  });
73
68
  }
74
69
  ```
75
70
 
76
- </Tab>
77
-
78
- <Tab value="Client">
71
+ ### Client
79
72
 
80
- Chat messages need to be stored somewhere—typically a database. In this example, we assume a route like `/chats/:id` passes the session ID, allowing us to fetch existing messages and persist new ones.
73
+ `WorkflowChatTransport` reconnects when the HTTP connection ends before the workflow stream finishes:
81
74
 
82
- ```typescript title="app/chats/[id]/page.tsx" lineNumbers
75
+ ```tsx title="app/chat.tsx" lineNumbers
83
76
  "use client";
84
77
 
85
78
  import { useChat } from "@ai-sdk/react";
86
- import { WorkflowChatTransport } from "@workflow/ai"; // [!code highlight]
87
- import { useParams } from "next/navigation";
79
+ import { WorkflowChatTransport } from "@ai-sdk/workflow";
88
80
  import { useMemo } from "react";
89
81
 
90
- // Fetch existing messages from your backend
91
- async function getMessages(sessionId: string) { // [!code highlight]
92
- const res = await fetch(`/api/chats/${sessionId}/messages`); // [!code highlight]
93
- return res.json(); // [!code highlight]
94
- } // [!code highlight]
95
-
96
82
  export function Chat({ initialMessages }) {
97
- const { id: sessionId } = useParams<{ id: string }>();
98
-
99
- const transport = useMemo( // [!code highlight]
100
- () => // [!code highlight]
101
- new WorkflowChatTransport({ // [!code highlight]
102
- api: "/api/chat", // [!code highlight]
103
- onChatEnd: async () => { // [!code highlight]
104
- // Persist the updated messages to the chat session // [!code highlight]
105
- await fetch(`/api/chats/${sessionId}/messages`, { // [!code highlight]
106
- method: "PUT", // [!code highlight]
107
- headers: { "Content-Type": "application/json" }, // [!code highlight]
108
- body: JSON.stringify({ messages }), // [!code highlight]
109
- }); // [!code highlight]
110
- }, // [!code highlight]
111
- }), // [!code highlight]
112
- [sessionId] // [!code highlight]
113
- ); // [!code highlight]
114
-
115
- const { messages, input, handleInputChange, handleSubmit } = useChat({
116
- initialMessages, // [!code highlight] Loaded via getMessages(sessionId)
117
- transport, // [!code highlight]
83
+ const transport = useMemo(
84
+ () => new WorkflowChatTransport({ api: "/api/chat" }),
85
+ []
86
+ );
87
+ const { messages, sendMessage } = useChat({
88
+ messages: initialMessages,
89
+ transport,
118
90
  });
119
91
 
120
- return (
121
- <form onSubmit={handleSubmit}>
122
- {/* ... render messages ... */}
123
- <input value={input} onChange={handleInputChange} />
124
- </form>
125
- );
92
+ // Render messages and call sendMessage({ text }) from your form.
126
93
  }
127
94
  ```
128
95
 
129
- </Tab>
130
-
131
- </Tabs>
132
-
133
- This is the pattern used in the [Building Durable AI Agents](/docs/ai) guide.
134
-
135
- In this pattern, the client owns conversation state, with the latest turn managed by the AI SDK's `useChat`, and past turns persisted to a user-managed database.
136
-
137
- Persisting the turn is usually done through either:
138
-
139
- - A step on the workflow that runs after `agent.stream()` and takes the message history from the agent return value (either `messages: ModelMessage[]` or `uiMessages: UIMessage[]`)
140
- - A hook on `useChat`in the client that calls an API to persist state (or localStorage, etc.), either on every new message, or `onFinish`
141
- - The resumable stream attached to the workflow (see [Resumable Streams](/docs/ai/resumable-streams))
142
- - Note that user messages are not persisted to the stream by default, and need to be explicitly persisted separately
96
+ Persist `UIMessage[]` in your application database. `WorkflowAgent.stream()` returns `ModelMessage[]`, but there is no general conversion from model messages back to UI messages with all UI metadata intact.
143
97
 
144
- ## Multi-Turn Workflows
98
+ Use the single-turn pattern when:
145
99
 
146
- A single workflow handles the entire conversation session across multiple turns, and owns the current conversation state. The clients/API routes inject new messages via hooks. The workflow run ID serves as the session identifier.
100
+ - Your application already owns chat history
101
+ - Each turn should run on the latest deployment
102
+ - You want a simple request-to-run mapping
103
+ - Approval responses or client-side tool results arrive as another message turn
147
104
 
148
- For a full example of an agent using multi-turn workflows, check out the Flight Booking App example in the [Workflow Examples](https://github.com/vercel/workflow-examples/tree/main/flight-booking-app) repository.
105
+ ## Multi-turn workflows
149
106
 
150
- A key challenge in multi-turn workflows is ensuring user messages appear in the correct order when replaying the stream (e.g., after a page refresh). Since the stream primarily contains AI responses, user messages must be explicitly marked in the stream so the client can reconstruct the full conversation.
107
+ A single workflow run can own the model-message history for the whole session. It waits on a Hook between turns, and external callers resume the Hook with the next message. The workflow run ID becomes the session identifier.
151
108
 
152
- <Tabs items={['Workflow', 'API Routes', 'Hook Definition', 'Client Hook']}>
153
-
154
- <Tab value="Workflow">
109
+ ```typescript title="workflows/chat-session.ts" lineNumbers
110
+ import { WorkflowAgent, type ModelCallStreamPart } from "@ai-sdk/workflow";
111
+ import { type ModelMessage } from "ai";
112
+ import { defineHook, getWorkflowMetadata, getWritable } from "workflow";
113
+ import { z } from "zod";
114
+ import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./tools";
155
115
 
156
- ```typescript title="workflows/chat/index.ts" lineNumbers
157
- import {
158
- convertToModelMessages,
159
- type UIMessageChunk,
160
- type UIMessage,
161
- type ModelMessage,
162
- } from "ai";
163
- import { DurableAgent } from "@workflow/ai/agent";
164
- import { getWritable, getWorkflowMetadata } from "workflow";
165
- import { chatMessageHook } from "./hooks/chat-message";
166
- import { flightBookingTools, FLIGHT_ASSISTANT_PROMPT } from "./steps/tools";
167
- import { writeUserMessageMarker, writeStreamClose } from "./steps/writer"; // [!code highlight]
116
+ export const chatMessageHook = defineHook({
117
+ schema: z.object({ message: z.string() }),
118
+ });
168
119
 
169
- export async function chat(initialMessages: UIMessage[]) {
120
+ export async function chatSession(initialMessages: ModelMessage[]) {
170
121
  "use workflow";
171
122
 
172
- const { workflowRunId: runId } = getWorkflowMetadata();
173
- const writable = getWritable<UIMessageChunk>();
174
- const messages: ModelMessage[] = await convertToModelMessages(initialMessages);
175
-
176
- // Write markers for initial user messages (for replay) // [!code highlight]
177
- for (const msg of initialMessages) { // [!code highlight]
178
- if (msg.role === "user") { // [!code highlight]
179
- const text = msg.parts.filter((p) => p.type === "text").map((p) => p.text).join(""); // [!code highlight]
180
- if (text) await writeUserMessageMarker(writable, text, msg.id); // [!code highlight]
181
- } // [!code highlight]
182
- } // [!code highlight]
123
+ const { workflowRunId } = getWorkflowMetadata();
124
+ const hook = chatMessageHook.create({ token: workflowRunId });
125
+ const writable = getWritable<ModelCallStreamPart>();
126
+ let messages = [...initialMessages];
183
127
 
184
- const agent = new DurableAgent({
185
- model: "bedrock/claude-haiku-4-5-20251001-v1",
128
+ const agent = new WorkflowAgent({
129
+ model: "spacexai/grok-4.6",
186
130
  instructions: FLIGHT_ASSISTANT_PROMPT,
187
131
  tools: flightBookingTools,
188
132
  });
189
133
 
190
- // Use run ID as the hook token for easy resumption
191
- const hook = chatMessageHook.create({ token: runId });
192
- let turnNumber = 0;
193
-
194
- while (true) {
195
- turnNumber++;
134
+ const maxTurns = 100;
135
+ for (let turn = 0; turn < maxTurns; turn++) {
196
136
  const result = await agent.stream({
197
137
  messages,
198
138
  writable,
199
- preventClose: true, // [!code highlight] Keep stream open for follow-ups
200
- sendStart: turnNumber === 1,
139
+ preventClose: true,
201
140
  sendFinish: false,
202
141
  });
203
- messages.push(...result.messages.slice(messages.length));
142
+ messages = result.messages;
204
143
 
205
- // Wait for next user message via hook
206
- const { message: followUp } = await hook;
207
- if (followUp === "/done") break;
144
+ // Do not accept a follow-up that this run has no remaining turn to process.
145
+ if (turn === maxTurns - 1) break;
208
146
 
209
- // Write marker and add to messages // [!code highlight]
210
- const followUpId = `user-${runId}-${turnNumber}`; // [!code highlight]
211
- await writeUserMessageMarker(writable, followUp, followUpId); // [!code highlight]
212
- messages.push({ role: "user", content: followUp });
147
+ const { message } = await hook;
148
+ if (message === "/done") break;
149
+ messages = [...messages, { role: "user", content: message }];
213
150
  }
214
151
 
215
- await writeStreamClose(writable); // [!code highlight]
216
152
  return { messages };
217
153
  }
218
154
  ```
219
155
 
220
- The `writeUserMessageMarker` helper writes a `data-workflow` chunk to mark user turns:
221
-
222
- ```typescript title="workflows/chat/steps/writer.ts" lineNumbers
223
- import type { UIMessageChunk } from "ai";
224
-
225
- export async function writeUserMessageMarker( // [!code highlight]
226
- writable: WritableStream<UIMessageChunk>,
227
- content: string,
228
- messageId: string
229
- ) {
230
- "use step"; // [!code highlight]
231
- const writer = writable.getWriter();
232
- try {
233
- await writer.write({
234
- type: "data-workflow", // [!code highlight]
235
- data: { type: "user-message", id: messageId, content, timestamp: Date.now() }, // [!code highlight]
236
- } as UIMessageChunk);
237
- } finally {
238
- writer.releaseLock();
239
- }
240
- }
241
-
242
- export async function writeStreamClose(writable: WritableStream<UIMessageChunk>) {
243
- const writer = writable.getWriter();
244
- await writer.write({ type: "finish" });
245
- await writer.close();
246
- }
247
- ```
248
-
249
- </Tab>
156
+ Create the Hook once, outside the loop. Recreating the same token on every turn causes a Hook conflict. Intermediate turns use `preventClose: true` with `sendFinish: false`; the workflow closes the durable stream and emits the final UI `finish` when the run returns.
250
157
 
251
- <Tab value="API Routes">
158
+ ### Start and resume the session
252
159
 
253
- Three endpoints: start a session, send follow-up messages, and reconnect to the stream.
160
+ Use one route to create the run and a second route to deliver follow-up messages:
254
161
 
255
162
  ```typescript title="app/api/chat/route.ts" lineNumbers
256
- import { createUIMessageStreamResponse, type UIMessage } from "ai";
163
+ import { convertToModelMessages, type UIMessage } from "ai";
257
164
  import { start } from "workflow/api";
258
- import { chat } from "@/workflows/chat";
259
-
260
- export async function POST(req: Request) {
261
- const { initialMessage }: { initialMessage: UIMessage } = await req.json();
165
+ import { chatSession } from "@/workflows/chat-session";
262
166
 
263
- const run = await start(chat, [[initialMessage]]); // [!code highlight]
264
-
265
- return createUIMessageStreamResponse({
266
- stream: run.readable,
267
- headers: {
268
- "x-workflow-run-id": run.runId, // [!code highlight] For follow-ups and reconnection
269
- },
270
- });
167
+ export async function POST(request: Request) {
168
+ const { messages }: { messages: UIMessage[] } = await request.json();
169
+ const run = await start(chatSession, [await convertToModelMessages(messages)]);
170
+ return Response.json({ runId: run.runId });
271
171
  }
272
172
  ```
273
173
 
274
- ```typescript title="app/api/chat/[id]/route.ts" lineNumbers
275
- import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
174
+ ```typescript title="app/api/chat/[runId]/message/route.ts" lineNumbers
175
+ import { chatMessageHook } from "@/workflows/chat-session";
276
176
 
277
177
  export async function POST(
278
- req: Request,
279
- { params }: { params: Promise<{ id: string }> }
178
+ request: Request,
179
+ { params }: { params: Promise<{ runId: string }> }
280
180
  ) {
281
- const { id: runId } = await params;
282
- const { message } = await req.json();
283
-
284
- // Resume the hook using the workflow run ID // [!code highlight]
285
- await chatMessageHook.resume(runId, { message }); // [!code highlight]
286
-
181
+ const { runId } = await params;
182
+ const { message }: { message: string } = await request.json();
183
+ await chatMessageHook.resume(runId, { message });
287
184
  return Response.json({ success: true });
288
185
  }
289
186
  ```
290
187
 
291
- ```typescript title="app/api/chat/[id]/stream/route.ts" lineNumbers
188
+ ### Reconnect to the stream
189
+
190
+ `WorkflowChatTransport` counts transformed UI chunks, while the durable stream stores raw `ModelCallStreamPart` values. Always replay the raw stream from index `0` and apply the UI cursor in `createModelCallToUIChunkTransform()`:
191
+
192
+ {/* @skip-typecheck: requires AI SDK 7 and @ai-sdk/workflow */}
193
+ ```typescript title="app/api/chat/[runId]/stream/route.ts" lineNumbers
194
+ import { createModelCallToUIChunkTransform } from "@ai-sdk/workflow";
292
195
  import { createUIMessageStreamResponse } from "ai";
293
196
  import { getRun } from "workflow/api";
294
197
 
295
198
  export async function GET(
296
199
  request: Request,
297
- { params }: { params: Promise<{ id: string }> }
200
+ { params }: { params: Promise<{ runId: string }> }
298
201
  ) {
299
- const { id } = await params;
300
- const { searchParams } = new URL(request.url);
301
- const startIndex = searchParams.get("startIndex");
302
-
303
- const run = getRun(id); // [!code highlight]
304
- const stream = run.getReadable({ // [!code highlight]
305
- startIndex: startIndex ? parseInt(startIndex, 10) : undefined, // [!code highlight]
306
- }); // [!code highlight]
307
-
308
- return createUIMessageStreamResponse({ stream });
309
- }
310
- ```
311
-
312
- </Tab>
313
-
314
- <Tab value="Hook Definition">
315
-
316
- ```typescript title="workflows/chat/hooks/chat-message.ts" lineNumbers
317
- import { defineHook } from "workflow";
318
- import { z } from "zod";
319
-
320
- export const chatMessageHook = defineHook({
321
- schema: z.object({
322
- message: z.string(),
323
- }),
324
- });
325
- ```
326
-
327
- </Tab>
328
-
329
- <Tab value="Client Hook">
330
-
331
- A custom hook wraps `useChat` to manage the multi-turn session. It handles:
332
-
333
- - Routing between the initial message endpoint and follow-up endpoint
334
- - Reconstructing user messages from stream markers for correct ordering on replay
335
-
336
- ```typescript title="hooks/use-multi-turn-chat.ts" lineNumbers
337
- "use client";
202
+ const { runId } = await params;
203
+ const startIndex = Number(
204
+ new URL(request.url).searchParams.get("startIndex") ?? "0"
205
+ );
338
206
 
339
- import type { UIMessage, UIDataTypes, ChatStatus } from "ai";
340
- import { useChat } from "@ai-sdk/react";
341
- import { WorkflowChatTransport } from "@workflow/ai";
342
- import { useState, useCallback, useMemo, useEffect, useRef } from "react";
207
+ if (!Number.isSafeInteger(startIndex) || startIndex < 0) {
208
+ return Response.json(
209
+ { error: "startIndex must be a non-negative safe integer" },
210
+ { status: 400 }
211
+ );
212
+ }
343
213
 
344
- const STORAGE_KEY = "workflow-run-id";
214
+ const run = getRun(runId);
215
+ const stream = run
216
+ .getReadable({ startIndex: 0 })
217
+ .pipeThrough(createModelCallToUIChunkTransform({ uiStartIndex: startIndex }));
345
218
 
346
- interface UserMessageData {
347
- type: "user-message";
348
- id: string;
349
- content: string;
350
- timestamp: number;
219
+ return createUIMessageStreamResponse({
220
+ stream,
221
+ headers: { "x-workflow-run-id": runId },
222
+ });
351
223
  }
224
+ ```
352
225
 
353
- export function useMultiTurnChat() {
354
- const [runId, setRunId] = useState<string | null>(null);
355
- const [shouldResume, setShouldResume] = useState(false);
356
- const userMessagesRef = useRef<Map<string, UIMessage>>(new Map());
226
+ Persist `UIMessage[]` separately if the client must reconstruct user messages and UI metadata after a refresh. The workflow-owned `ModelMessage[]` history is the model's durable context, not a replacement for an application chat table.
357
227
 
358
- // Check for existing session on mount // [!code highlight]
359
- useEffect(() => {
360
- const storedRunId = localStorage.getItem(STORAGE_KEY);
361
- if (storedRunId) {
362
- setRunId(storedRunId);
363
- setShouldResume(true);
364
- }
365
- }, []);
228
+ ### Persist and display the full session
366
229
 
367
- const transport = useMemo(
368
- () =>
369
- new WorkflowChatTransport({
370
- api: "/api/chat",
371
- onChatSendMessage: (response) => {
372
- const workflowRunId = response.headers.get("x-workflow-run-id");
373
- if (workflowRunId) {
374
- setRunId(workflowRunId);
375
- localStorage.setItem(STORAGE_KEY, workflowRunId);
376
- }
377
- },
378
- onChatEnd: () => {
379
- setRunId(null);
380
- localStorage.removeItem(STORAGE_KEY);
381
- userMessagesRef.current.clear();
382
- },
383
- prepareReconnectToStreamRequest: ({ api, ...rest }) => {
384
- const storedRunId = localStorage.getItem(STORAGE_KEY);
385
- if (!storedRunId) throw new Error("No active session");
386
- return { ...rest, api: `/api/chat/${storedRunId}/stream` };
387
- },
388
- }),
389
- []
390
- );
230
+ A multi-turn chat has three related records with different responsibilities:
391
231
 
392
- const { messages: rawMessages, sendMessage: baseSendMessage, status, stop, setMessages } =
393
- useChat({ resume: shouldResume, transport });
394
-
395
- // Reconstruct conversation order from stream markers // [!code highlight]
396
- const messages = useMemo(() => { // [!code highlight]
397
- const result: UIMessage[] = []; // [!code highlight]
398
- const seenContent = new Set<string>(); // [!code highlight]
399
- // [!code highlight]
400
- // Collect content from optimistic user messages // [!code highlight]
401
- for (const msg of rawMessages) { // [!code highlight]
402
- if (msg.role === "user") { // [!code highlight]
403
- const text = msg.parts.filter((p) => p.type === "text").map((p) => p.text).join(""); // [!code highlight]
404
- if (text) seenContent.add(text); // [!code highlight]
405
- } // [!code highlight]
406
- } // [!code highlight]
407
- // [!code highlight]
408
- for (const msg of rawMessages) { // [!code highlight]
409
- if (msg.role === "user") { // [!code highlight]
410
- result.push(msg); // [!code highlight]
411
- continue; // [!code highlight]
412
- } // [!code highlight]
413
- // [!code highlight]
414
- if (msg.role === "assistant") { // [!code highlight]
415
- // Process parts in order, splitting on user-message markers // [!code highlight]
416
- let currentParts: typeof msg.parts = []; // [!code highlight]
417
- let partIndex = 0; // [!code highlight]
418
- // [!code highlight]
419
- for (const part of msg.parts) { // [!code highlight]
420
- if (part.type === "data-workflow" && "data" in part) { // [!code highlight]
421
- const data = part.data as UserMessageData; // [!code highlight]
422
- if (data?.type === "user-message") { // [!code highlight]
423
- // Flush accumulated assistant parts // [!code highlight]
424
- if (currentParts.length > 0) { // [!code highlight]
425
- result.push({ ...msg, id: `${msg.id}-${partIndex++}`, parts: currentParts }); // [!code highlight]
426
- currentParts = []; // [!code highlight]
427
- } // [!code highlight]
428
- // Add user message if not duplicate // [!code highlight]
429
- if (!seenContent.has(data.content)) { // [!code highlight]
430
- seenContent.add(data.content); // [!code highlight]
431
- result.push({ id: data.id, role: "user", parts: [{ type: "text", text: data.content }] }); // [!code highlight]
432
- } // [!code highlight]
433
- continue; // [!code highlight]
434
- } // [!code highlight]
435
- } // [!code highlight]
436
- currentParts.push(part); // [!code highlight]
437
- } // [!code highlight]
438
- // [!code highlight]
439
- if (currentParts.length > 0) { // [!code highlight]
440
- result.push({ ...msg, id: partIndex > 0 ? `${msg.id}-${partIndex}` : msg.id, parts: currentParts }); // [!code highlight]
441
- } // [!code highlight]
442
- } // [!code highlight]
443
- } // [!code highlight]
444
- return result; // [!code highlight]
445
- }, [rawMessages]); // [!code highlight]
446
-
447
- // Route messages to appropriate endpoint
448
- const sendMessage = useCallback(
449
- async (text: string) => {
450
- if (runId) {
451
- // Follow-up: send via hook resumption // [!code highlight]
452
- await fetch(`/api/chat/${runId}`, {
453
- method: "POST",
454
- headers: { "Content-Type": "application/json" },
455
- body: JSON.stringify({ message: text }),
456
- });
457
- } else {
458
- // First message: start new workflow
459
- await baseSendMessage({ text, metadata: { createdAt: Date.now() } });
460
- }
461
- },
462
- [runId, baseSendMessage]
463
- );
232
+ 1. **Workflow model history**: `ModelMessage[]` is the durable context sent back to the model on each turn.
233
+ 2. **Workflow run stream**: `ModelCallStreamPart` values contain durable model and tool output for live delivery and reconnection.
234
+ 3. **Application chat history**: `UIMessage[]` preserves user messages, display metadata, attachments, and application-specific parts.
464
235
 
465
- const endSession = useCallback(async () => {
466
- if (runId) {
467
- await fetch(`/api/chat/${runId}`, {
468
- method: "POST",
469
- headers: { "Content-Type": "application/json" },
470
- body: JSON.stringify({ message: "/done" }),
471
- });
472
- }
473
- setRunId(null);
474
- setShouldResume(false);
475
- localStorage.removeItem(STORAGE_KEY);
476
- userMessagesRef.current.clear();
477
- setMessages([]);
478
- }, [runId, setMessages]);
479
-
480
- return { messages, status, runId, sendMessage, endSession, stop };
481
- }
482
- ```
483
-
484
- </Tab>
485
-
486
- </Tabs>
236
+ When the user sends the first message, start the run, store its run ID with the application chat record, and connect to the run stream. For each follow-up, optimistically add and persist the user `UIMessage`, then resume `chatMessageHook` with the corresponding text. After a refresh, load the persisted UI messages and reconnect through the stream route above using the last persisted UI chunk cursor. If you persist only user messages, replay model output from the beginning and merge by stable message IDs.
487
237
 
488
- In this pattern, the workflow owns the entire conversation session. All messages are persisted in the workflow, and follow-up messages are injected via hooks. The workflow writes **user message markers** to the stream using `data-workflow` chunks, which allows the client to reconstruct the full conversation in the correct order when replaying the stream (e.g., after a page refresh).
238
+ Do not treat the run stream as the only chat database. `WorkflowAgent` deliberately stores raw model-call parts, and user messages resumed through a Hook are not automatically written to that output stream. Keeping the application history separate avoids synthetic stream markers and preserves UI information that cannot be reconstructed from `ModelMessage[]`.
489
239
 
490
- The client hook processes these markers by:
240
+ <Callout type="info">
241
+ The reconnect route replays raw `ModelCallStreamPart` values from index `0` because raw parts and transformed UI chunks do not have matching indexes. `uiStartIndex` prevents already-delivered UI chunks from being sent to the client again, but the server still transforms the earlier raw history. For very long streams, split conversations into bounded runs until WorkflowAgent exposes a persisted raw-to-UI cursor mapping.
242
+ </Callout>
491
243
 
492
- 1. Iterating through message parts in order
493
- 2. When a `user-message` marker is found, flushing any accumulated assistant content and inserting the user message
494
- 3. Deduplicating against optimistic sends from the initial message
244
+ Use the multi-turn pattern when:
495
245
 
496
- This ensures the conversation displays as User AI → User → AI regardless of whether viewing live or replaying from the stream.
246
+ - One workflow should own the session's model context
247
+ - Backend events or other users need to inject messages through Hooks
248
+ - Full-session tracing is more important than running every turn on the newest deployment
249
+ - The application is prepared to manage a long-lived run and stream cursor
497
250
 
498
- ## Choosing a Pattern
251
+ ## Choosing a pattern
499
252
 
500
- | Consideration | Single-Turn | Multi-Turn |
253
+ | Consideration | Single-turn | Multi-turn |
501
254
  |--------------|-------------|------------|
502
- | State ownership | Client or API route | Workflow |
503
- | Message injection from backend | Requires stitching together runs | Native via hooks |
255
+ | State ownership | Client or application database | Workflow for model context; application database for UI history |
256
+ | Deployment version | Latest deployment per turn | Deployment that started the run |
257
+ | Message injection | Start another run | Resume a Hook |
504
258
  | Workflow complexity | Lower | Higher |
505
- | Workflow time horizon | Minutes | Hours to indefinitely |
506
- | Observability scope | Per-turn traces | Full session traces |
259
+ | Workflow time horizon | One model turn | Hours or longer |
260
+ | Observability scope | Per turn | Full session |
507
261
 
508
- **Multi-turn is recommended for most production use-cases.** If you're starting fresh, go with multi-turn. It's more flexible and grows with your requirements. You don't need to maintain the chat history yourself and can offload all that to the workflow's built in persistence. It also enables native message injection and full session observability, which becomes increasingly valuable as your agent matures.
262
+ **Multi-turn works well for new durable sessions.** The workflow owns model context, accepts messages from users and backend systems through the same Hook, and provides one full-session trace.
509
263
 
510
- **Single-turn works well when adapting existing architectures.** If you already have a system for managing message state, and want to adopt durable agents incrementally, single-turn workflows slot in with minimal changes. Each turn maps cleanly to an independent workflow run.
264
+ **Single-turn works well when adapting an existing architecture.** If the application already manages message state and you want to adopt durable agents incrementally, one workflow run per turn requires fewer lifecycle changes and always uses the latest deployment.
511
265
 
512
- ## Multiplayer Chat Sessions
266
+ ## Multiplayer chat sessions
513
267
 
514
- The multi-turn pattern also easily enables multi-player chat sessions. New messages can come from system events, external services, and other users. Since a `hook` injects messages into workflow at any point, and the entire history is a single stream that clients can reconnect to, it doesn't matter where the injected messages come from. Here are different use-cases for multi-player chat sessions:
268
+ The multi-turn pattern also supports messages from system events, external services, and multiple users. Every source resumes the same Hook; the workflow queues those messages and processes them between model turns.
515
269
 
516
- <Tabs items={['System Event', 'External Service', 'Multiple Users']}>
270
+ <Tabs items={['System event', 'External service', 'Multiple users']}>
517
271
 
518
- <Tab value="System Event">
272
+ <Tab value="System event">
519
273
 
520
- Internal system events like scheduled tasks, background jobs, or database triggers can inject updates into an active conversation.
274
+ Scheduled tasks, background jobs, or database triggers can inject updates into an active conversation:
521
275
 
522
276
  ```typescript title="app/api/internal/flight-update/route.ts" lineNumbers
523
- import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
277
+ import { chatMessageHook } from "@/workflows/chat-session";
524
278
 
525
- // Called by your flight status monitoring system
526
- export async function POST(req: Request) {
527
- const { runId, flightNumber, newStatus } = await req.json();
279
+ export async function POST(request: Request) {
280
+ const { runId, flightNumber, newStatus } = await request.json();
528
281
 
529
- await chatMessageHook.resume(runId, { // [!code highlight]
530
- message: `[System] Flight ${flightNumber} status updated: ${newStatus}`, // [!code highlight]
531
- }); // [!code highlight]
282
+ await chatMessageHook.resume(runId, {
283
+ message: `[System] Flight ${flightNumber} status updated: ${newStatus}`,
284
+ });
532
285
 
533
286
  return Response.json({ success: true });
534
287
  }
@@ -536,20 +289,20 @@ export async function POST(req: Request) {
536
289
 
537
290
  </Tab>
538
291
 
539
- <Tab value="External Service">
292
+ <Tab value="External service">
540
293
 
541
- External webhooks from third-party services (Stripe, Twilio, etc.) can notify the conversation of events.
294
+ A third-party webhook can notify the conversation about an external event:
542
295
 
543
296
  ```typescript title="app/api/webhooks/payment/route.ts" lineNumbers
544
- import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
297
+ import { chatMessageHook } from "@/workflows/chat-session";
545
298
 
546
- export async function POST(req: Request) {
547
- const { runId, paymentStatus, amount } = await req.json();
299
+ export async function POST(request: Request) {
300
+ const { runId, paymentStatus, amount } = await request.json();
548
301
 
549
302
  if (paymentStatus === "succeeded") {
550
- await chatMessageHook.resume(runId, { // [!code highlight]
551
- message: `[Payment] Payment of $${amount.toFixed(2)} received. Your booking is confirmed!`, // [!code highlight]
552
- }); // [!code highlight]
303
+ await chatMessageHook.resume(runId, {
304
+ message: `[Payment] Payment of $${amount.toFixed(2)} received. Your booking is confirmed.`,
305
+ });
553
306
  }
554
307
 
555
308
  return Response.json({ received: true });
@@ -558,38 +311,39 @@ export async function POST(req: Request) {
558
311
 
559
312
  </Tab>
560
313
 
561
- <Tab value="Multiple Users">
314
+ <Tab value="Multiple users">
562
315
 
563
- Multiple human users can participate in the same conversation. Each user's client connects to the same workflow stream.
316
+ Multiple authenticated users can participate in the same workflow-owned session. Include attribution when resuming the Hook:
564
317
 
565
- ```typescript title="app/api/chat/[id]/route.ts" lineNumbers
566
- import { chatMessageHook } from "@/workflows/chat/hooks/chat-message";
318
+ ```typescript title="app/api/chat/[runId]/message/route.ts" lineNumbers
319
+ import { chatMessageHook } from "@/workflows/chat-session";
567
320
  import { getUser } from "@/lib/auth";
568
321
 
569
322
  export async function POST(
570
- req: Request,
571
- { params }: { params: Promise<{ id: string }> }
323
+ request: Request,
324
+ { params }: { params: Promise<{ runId: string }> }
572
325
  ) {
573
- const { id: runId } = await params;
574
- const { message } = await req.json();
575
- const user = await getUser(req); // [!code highlight]
326
+ const { runId } = await params;
327
+ const { message } = await request.json();
328
+ const user = await getUser(request);
576
329
 
577
- // Inject message with user attribution // [!code highlight]
578
- await chatMessageHook.resume(runId, { // [!code highlight]
579
- message: `[${user.name}] ${message}`, // [!code highlight]
580
- }); // [!code highlight]
330
+ await chatMessageHook.resume(runId, {
331
+ message: `[${user.name}] ${message}`,
332
+ });
581
333
 
582
334
  return Response.json({ success: true });
583
335
  }
584
336
  ```
585
337
 
338
+ To preserve structured attribution across refreshes, persist the corresponding `UIMessage` using the application-history approach described above.
339
+
586
340
  </Tab>
587
341
 
588
342
  </Tabs>
589
343
 
590
- ## Related Documentation
344
+ ## Related documentation
591
345
 
592
- - [Building Durable AI Agents](/docs/ai) - Foundation guide for durable agents
593
- - [Message Queueing](/docs/ai/message-queueing) - Queueing messages during tool execution
594
- - [`defineHook()` API Reference](/docs/api-reference/workflow/define-hook) - Hook configuration options
595
- - [`DurableAgent` API Reference](/docs/api-reference/workflow-ai/durable-agent) - Full API documentation
346
+ - [Building Durable AI Agents](/docs/ai): Foundation guide for WorkflowAgent
347
+ - [Message Queueing](/docs/ai/message-queueing): Inject messages between model-call steps
348
+ - [Resumable Streams](/docs/ai/resumable-streams): Reconnect to durable output
349
+ - [`WorkflowAgent`](https://ai-sdk.dev/v7/docs/agents/workflow-agent#workflowagent): AI SDK API for durable, resumable agents