workflow 5.0.0-beta.2 → 5.0.0-beta.20

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 (169) hide show
  1. package/dist/api-workflow.d.ts +1 -1
  2. package/dist/api-workflow.d.ts.map +1 -1
  3. package/dist/api-workflow.js +2 -2
  4. package/dist/api.d.ts +5 -1
  5. package/dist/api.d.ts.map +1 -1
  6. package/dist/api.js +14 -2
  7. package/dist/internal/builtins.d.ts +17 -0
  8. package/dist/internal/builtins.d.ts.map +1 -1
  9. package/dist/internal/builtins.js +65 -1
  10. package/dist/observability.d.ts +1 -1
  11. package/dist/observability.js +2 -2
  12. package/dist/runtime.d.ts +1 -1
  13. package/dist/runtime.d.ts.map +1 -1
  14. package/dist/runtime.js +2 -2
  15. package/docs/ai/index.mdx +27 -23
  16. package/docs/api-reference/index.mdx +24 -0
  17. package/docs/api-reference/meta.json +8 -0
  18. package/docs/api-reference/vitest/index.mdx +28 -7
  19. package/docs/api-reference/workflow/create-hook.mdx +38 -0
  20. package/docs/api-reference/workflow/create-webhook.mdx +1 -0
  21. package/docs/api-reference/workflow/experimental-set-attributes.mdx +65 -0
  22. package/docs/api-reference/workflow/fetch.mdx +5 -0
  23. package/docs/api-reference/workflow/index.mdx +3 -0
  24. package/docs/api-reference/workflow-ai/durable-agent.mdx +7 -45
  25. package/docs/api-reference/workflow-api/get-hook-by-token.mdx +7 -0
  26. package/docs/api-reference/workflow-api/get-run.mdx +6 -0
  27. package/docs/api-reference/workflow-api/index.mdx +6 -8
  28. package/docs/api-reference/workflow-api/resume-hook.mdx +57 -0
  29. package/docs/api-reference/workflow-api/start.mdx +13 -5
  30. package/docs/api-reference/workflow-astro/index.mdx +18 -0
  31. package/docs/api-reference/workflow-astro/meta.json +4 -0
  32. package/docs/api-reference/workflow-astro/workflow.mdx +45 -0
  33. package/docs/api-reference/workflow-errors/hook-conflict-error.mdx +60 -0
  34. package/docs/api-reference/workflow-errors/index.mdx +85 -0
  35. package/docs/api-reference/workflow-errors/meta.json +5 -0
  36. package/docs/api-reference/workflow-errors/run-not-supported-error.mdx +58 -0
  37. package/docs/api-reference/workflow-errors/workflow-error.mdx +52 -0
  38. package/docs/api-reference/workflow-errors/workflow-run-failed-error.mdx +16 -6
  39. package/docs/api-reference/workflow-errors/workflow-run-not-completed-error.mdx +58 -0
  40. package/docs/api-reference/workflow-errors/workflow-runtime-error.mdx +58 -0
  41. package/docs/api-reference/workflow-nest/configure-workflow-controller.mdx +33 -0
  42. package/docs/api-reference/workflow-nest/index.mdx +31 -0
  43. package/docs/api-reference/workflow-nest/meta.json +9 -0
  44. package/docs/api-reference/workflow-nest/nest-local-builder.mdx +64 -0
  45. package/docs/api-reference/workflow-nest/workflow-controller.mdx +40 -0
  46. package/docs/api-reference/workflow-nest/workflow-module.mdx +74 -0
  47. package/docs/api-reference/workflow-next/with-workflow.mdx +34 -2
  48. package/docs/api-reference/workflow-nitro/index.mdx +59 -0
  49. package/docs/api-reference/workflow-nuxt/index.mdx +47 -0
  50. package/docs/api-reference/workflow-observability/hydrate-data.mdx +35 -0
  51. package/docs/api-reference/workflow-observability/hydrate-resource-io.mdx +62 -0
  52. package/docs/api-reference/workflow-observability/index.mdx +64 -0
  53. package/docs/api-reference/workflow-observability/meta.json +11 -0
  54. package/docs/api-reference/workflow-observability/observability-revivers.mdx +50 -0
  55. package/docs/api-reference/workflow-observability/parse-class-name.mdx +41 -0
  56. package/docs/api-reference/workflow-observability/parse-step-name.mdx +40 -0
  57. package/docs/api-reference/workflow-observability/parse-workflow-name.mdx +55 -0
  58. package/docs/api-reference/workflow-runtime/create-world.mdx +39 -0
  59. package/docs/api-reference/workflow-runtime/get-world-handlers.mdx +44 -0
  60. package/docs/api-reference/{workflow-api → workflow-runtime}/get-world.mdx +7 -10
  61. package/docs/api-reference/workflow-runtime/health-check.mdx +50 -0
  62. package/docs/api-reference/workflow-runtime/index.mdx +43 -0
  63. package/docs/api-reference/workflow-runtime/meta.json +12 -0
  64. package/docs/api-reference/workflow-runtime/set-world.mdx +49 -0
  65. package/docs/api-reference/workflow-runtime/workflow-entrypoint.mdx +42 -0
  66. package/docs/api-reference/{workflow-api → workflow-runtime}/world/index.mdx +5 -8
  67. package/docs/api-reference/workflow-runtime/world/meta.json +4 -0
  68. package/docs/api-reference/{workflow-api → workflow-runtime}/world/queue.mdx +2 -2
  69. package/docs/api-reference/{workflow-api → workflow-runtime}/world/storage.mdx +11 -4
  70. package/docs/api-reference/{workflow-api → workflow-runtime}/world/streams.mdx +2 -2
  71. package/docs/api-reference/workflow-serde/index.mdx +0 -1
  72. package/docs/api-reference/workflow-serde/workflow-deserialize.mdx +1 -2
  73. package/docs/api-reference/workflow-serde/workflow-serialize.mdx +1 -2
  74. package/docs/api-reference/workflow-sveltekit/index.mdx +18 -0
  75. package/docs/api-reference/workflow-sveltekit/meta.json +4 -0
  76. package/docs/api-reference/workflow-sveltekit/workflow-plugin.mdx +42 -0
  77. package/docs/api-reference/workflow-vite/index.mdx +18 -0
  78. package/docs/api-reference/workflow-vite/meta.json +4 -0
  79. package/docs/api-reference/workflow-vite/workflow.mdx +48 -0
  80. package/docs/changelog/attributes-mvp.mdx +380 -0
  81. package/docs/changelog/eager-processing.mdx +269 -0
  82. package/docs/changelog/index.mdx +2 -1
  83. package/docs/changelog/lazy-event-creation.md +127 -0
  84. package/docs/changelog/meta.json +6 -1
  85. package/docs/changelog/resilient-start.mdx +31 -283
  86. package/docs/cookbook/advanced/child-workflows.mdx +315 -0
  87. package/docs/cookbook/advanced/meta.json +2 -3
  88. package/docs/cookbook/advanced/publishing-libraries.mdx +87 -29
  89. package/docs/cookbook/advanced/serializable-steps.mdx +17 -5
  90. package/docs/cookbook/advanced/upgrading-workflows.mdx +195 -0
  91. package/docs/cookbook/agent-patterns/agent-cancellation.mdx +156 -0
  92. package/docs/cookbook/agent-patterns/durable-agent.mdx +11 -184
  93. package/docs/cookbook/agent-patterns/human-in-the-loop.mdx +150 -173
  94. package/docs/cookbook/agent-patterns/meta.json +1 -7
  95. package/docs/cookbook/common-patterns/batching.mdx +44 -118
  96. package/docs/cookbook/common-patterns/idempotency.mdx +36 -52
  97. package/docs/cookbook/common-patterns/meta.json +4 -4
  98. package/docs/cookbook/common-patterns/rate-limiting.mdx +1 -1
  99. package/docs/cookbook/common-patterns/saga.mdx +128 -33
  100. package/docs/cookbook/common-patterns/scheduling.mdx +77 -193
  101. package/docs/cookbook/common-patterns/sequential-and-parallel.mdx +155 -0
  102. package/docs/cookbook/common-patterns/timeouts.mdx +100 -0
  103. package/docs/cookbook/common-patterns/workflow-composition.mdx +117 -0
  104. package/docs/cookbook/index.mdx +14 -17
  105. package/docs/cookbook/integrations/ai-sdk.mdx +330 -142
  106. package/docs/cookbook/integrations/chat-sdk.mdx +264 -151
  107. package/docs/cookbook/integrations/sandbox.mdx +482 -81
  108. package/docs/cookbook/meta.json +1 -1
  109. package/docs/deploying/building-a-world.mdx +1 -1
  110. package/docs/deploying/world/postgres-world.mdx +5 -3
  111. package/docs/deploying/world/vercel-world.mdx +2 -0
  112. package/docs/errors/abort-signal-timeout-in-workflow.mdx +80 -0
  113. package/docs/errors/corrupted-event-log.mdx +5 -5
  114. package/docs/errors/hook-conflict.mdx +56 -4
  115. package/docs/errors/index.mdx +9 -0
  116. package/docs/errors/replay-divergence.mdx +27 -0
  117. package/docs/errors/runtime-decryption-failed.mdx +77 -0
  118. package/docs/errors/step-executed-multiple-times.mdx +23 -0
  119. package/docs/errors/step-not-registered.mdx +1 -1
  120. package/docs/foundations/cancellation.mdx +459 -0
  121. package/docs/foundations/errors-and-retries.mdx +7 -3
  122. package/docs/foundations/hooks.mdx +29 -0
  123. package/docs/foundations/idempotency.mdx +236 -11
  124. package/docs/foundations/index.mdx +3 -3
  125. package/docs/foundations/meta.json +3 -2
  126. package/docs/foundations/serialization.mdx +78 -42
  127. package/docs/foundations/starting-workflows.mdx +6 -2
  128. package/docs/foundations/streaming.mdx +14 -23
  129. package/docs/foundations/versioning.mdx +263 -0
  130. package/docs/getting-started/astro.mdx +6 -0
  131. package/docs/getting-started/index.mdx +6 -7
  132. package/docs/getting-started/meta.json +1 -0
  133. package/docs/getting-started/nestjs.mdx +9 -0
  134. package/docs/getting-started/next.mdx +5 -3
  135. package/docs/getting-started/nitro.mdx +22 -0
  136. package/docs/getting-started/sveltekit.mdx +6 -0
  137. package/docs/getting-started/tanstack-start.mdx +241 -0
  138. package/docs/how-it-works/cancellation.mdx +287 -0
  139. package/docs/how-it-works/code-transform.mdx +2 -2
  140. package/docs/how-it-works/encryption.mdx +2 -2
  141. package/docs/how-it-works/event-sourcing.mdx +2 -2
  142. package/docs/how-it-works/meta.json +2 -1
  143. package/docs/internal/index.mdx +21 -0
  144. package/docs/internal/meta.json +10 -0
  145. package/docs/internal/nitro-native-build.mdx +38 -0
  146. package/docs/internal/nitro-web-ui.mdx +24 -0
  147. package/docs/internal/serializable-abort-controller.mdx +148 -0
  148. package/docs/migration-guides/migrating-from-aws-step-functions.mdx +63 -16
  149. package/docs/migration-guides/migrating-from-inngest.mdx +44 -22
  150. package/docs/migration-guides/migrating-from-temporal.mdx +43 -14
  151. package/docs/migration-guides/migrating-from-trigger-dev.mdx +59 -27
  152. package/docs/observability/attributes.mdx +87 -0
  153. package/docs/observability/index.mdx +25 -1
  154. package/docs/observability/meta.json +1 -1
  155. package/docs/observability/tracing.mdx +106 -0
  156. package/docs/testing/index.mdx +2 -2
  157. package/package.json +14 -13
  158. package/docs/api-reference/workflow-api/world/meta.json +0 -4
  159. package/docs/api-reference/workflow-api/world/observability.mdx +0 -164
  160. package/docs/cookbook/advanced/custom-serialization.mdx +0 -168
  161. package/docs/cookbook/advanced/durable-objects.mdx +0 -148
  162. package/docs/cookbook/advanced/isomorphic-packages.mdx +0 -145
  163. package/docs/cookbook/agent-patterns/stop-workflow.mdx +0 -216
  164. package/docs/cookbook/agent-patterns/tool-orchestration.mdx +0 -255
  165. package/docs/cookbook/agent-patterns/tool-streaming.mdx +0 -181
  166. package/docs/cookbook/common-patterns/child-workflows.mdx +0 -372
  167. package/docs/cookbook/common-patterns/content-router.mdx +0 -207
  168. package/docs/cookbook/common-patterns/fan-out.mdx +0 -208
  169. package/docs/foundations/common-patterns.mdx +0 -265
@@ -1,203 +1,316 @@
1
1
  ---
2
2
  title: Chat SDK
3
- description: Build durable chat sessions by combining workflow persistence with AI SDK's chat primitives.
3
+ description: Make Chat SDK bot sessions durable one workflow run per conversation thread, with hooks bridging inbound platform events into long-running agent logic.
4
4
  type: guide
5
- summary: Use workflow hooks and streaming to create chat sessions that survive disconnects and server restarts.
5
+ summary: Chat SDK normalizes Slack, Teams, Discord, Telegram and friends into one thread/message model. Workflow SDK gives each thread a durable run that owns multi-turn state, can sleep for hours, and survives restarts.
6
6
  related:
7
- - /docs/ai/chat-session-modeling
8
- - /docs/ai/resumable-streams
9
- - /docs/ai/message-queueing
10
- - /docs/api-reference/workflow-ai/durable-agent
7
+ - /docs/cookbook/integrations/ai-sdk
8
+ - /docs/cookbook/integrations/sandbox
11
9
  - /docs/api-reference/workflow/define-hook
10
+ - /docs/api-reference/workflow-api/start
11
+ - /docs/api-reference/workflow-api/get-run
12
12
  ---
13
13
 
14
- AI SDK provides chat primitives (`useChat`, message types, streaming utilities) for building chat interfaces. Workflow SDK makes those chat sessions durable -- surviving disconnects, cold starts, and server restarts -- by persisting every message and LLM response as workflow events.
14
+ [Chat SDK](https://chat-sdk.dev/) is a unified TypeScript SDK for building bots across Slack, Microsoft Teams, Google Chat, Discord, Telegram, GitHub, Linear, and WhatsApp. Write the bot once, deploy to every platform. It handles webhook verification, event normalization, subscriptions, and cross-platform features like cards and modals.
15
15
 
16
- ## What It Enables
16
+ Workflow SDK complements it by making bot **sessions** durable. Each conversation thread maps to a long-running workflow run that:
17
17
 
18
- - **Durable chat history** -- Messages and responses are persisted in the workflow event log, not just client state
19
- - **Resumable sessions** -- Users reconnect and pick up where they left off, even after server restarts
20
- - **Multi-turn conversations** -- A single workflow manages an entire chat session with hook-based message injection
21
- - **Server-side message queueing** -- Inject follow-up messages while the agent is still processing
18
+ - Owns multi-turn state in the durable event log instead of Redis-by-hand bookkeeping
19
+ - Can `sleep()` for hours or days waiting for a user reply, an approval, or a scheduled follow-up
20
+ - Survives deploys, cold starts, and crashes the session picks up from the last step on replay
21
+ - Receives follow-up messages via hooks, so the bot stays responsive while the workflow is still running
22
22
 
23
- ## When to Use
23
+ <Callout type="info">
24
+ One thread mapped to one workflow run also means the thread stays on the deployment that started it. For channels where each message should use newer code, see [Versioning](/docs/foundations/versioning) for explicit child-run and handoff patterns using `deploymentId: "latest"`.
25
+ </Callout>
24
26
 
25
- Use this pattern when your chat application needs:
27
+ The rest of this page covers the integration pattern. For a full Slack + Next.js + Redis walkthrough, see the [Durable chat sessions guide](https://chat-sdk.dev/docs/guides/durable-chat-sessions-nextjs) on chat-sdk.dev.
26
28
 
27
- - Persistence beyond the browser session
28
- - Recovery from server failures mid-conversation
29
- - Long-running agent sessions (minutes to hours)
30
- - Server-driven message injection (system messages, external events)
29
+ ## How It Fits Together
31
30
 
32
- ## Single-Turn: Stateless Sessions
31
+ Chat SDK owns the edge — webhook verification, event routing, `thread.post()` / `thread.stream()`. Workflow owns the session — state, loops, sleeps, retries. They meet at exactly two points:
33
32
 
34
- Each user message starts a new workflow run. The client owns the message history and sends the full array with each request. This is the simplest pattern.
33
+ ```mermaid
34
+ flowchart TD
35
+ A["Platform webhook"] --> B["Chat SDK event handler<br/>(onNewMention, onSubscribedMessage, …)"]
36
+ B -->|"no runId in thread state"| C["start(durableChatSession, …)"]
37
+ B -->|"runId in thread state"| D["resumeHook(runId, { message })"]
38
+ C --> E["Workflow run (durable)<br/>one per thread; suspends between turns"]
39
+ D --> E
40
+ E --> F["&quot;use step&quot; helpers<br/>thread.post(), thread.subscribe(), thread.setState(), …"]
41
+ ```
35
42
 
36
- ```typescript title="workflows/chat.ts" lineNumbers
37
- import { DurableAgent } from "@workflow/ai/agent";
38
- import { convertToModelMessages, type UIMessage, type UIMessageChunk } from "ai";
39
- import { getWritable } from "workflow";
43
+ - **Inbound** — Chat SDK handlers decide whether to `start(workflow, [thread, message])` or `resumeHook(runId, { message })`. The `runId` lives in Chat SDK's thread state (Redis, Postgres, or any state adapter).
44
+ - **Outbound** the workflow calls Chat SDK APIs (`thread.post()`, `thread.subscribe()`, `thread.setState()`) from inside step functions. Never from the top level of a workflow file — adapter packages use Node-only modules that aren't available in the workflow sandbox.
40
45
 
41
- export async function chat(messages: UIMessage[]) {
42
- "use workflow";
46
+ ## Why Workflow + Chat SDK
43
47
 
44
- const agent = new DurableAgent({
45
- model: "anthropic/claude-sonnet-4-20250514",
46
- instructions: "You are a helpful assistant.",
47
- tools: { /* your tools here */ },
48
- });
48
+ Without Workflow, a long-running bot session usually means one of:
49
+ - Holding a webhook request open while the agent runs (doesn't survive restarts, blows past platform timeouts)
50
+ - Writing session state to Redis manually, plus a scheduler for timeouts and retries, plus custom reconnection logic
49
51
 
50
- const result = await agent.stream({ // [!code highlight]
51
- messages: await convertToModelMessages(messages),
52
- writable: getWritable<UIMessageChunk>(),
53
- });
52
+ Workflow replaces all of that with a single durable function. The bot can:
54
53
 
55
- return { messages: result.messages };
56
- }
57
- ```
54
+ - Run a tool loop for minutes while the user watches typing indicators
55
+ - Wait for a human approval in another thread before continuing
56
+ - Schedule a follow-up message 24 hours later via `sleep("24h")`
57
+ - Pause on sandbox snapshot, resume when the user sends the next command (see the [Sandbox integration](/docs/cookbook/integrations/sandbox))
58
58
 
59
- ```typescript title="app/api/chat/route.ts" lineNumbers
60
- import { createUIMessageStreamResponse } from "ai";
61
- import { start } from "workflow/api";
62
- import { chat } from "@/workflows/chat";
59
+ Because the session *is* a workflow run, its history is recoverable from the event log — no separate message store to keep in sync.
63
60
 
64
- export async function POST(request: Request) {
65
- const { messages } = await request.json();
66
- const run = await start(chat, [messages]); // [!code highlight]
61
+ ## The Pattern: One Thread = One Workflow Run
67
62
 
68
- return createUIMessageStreamResponse({
69
- stream: run.readable,
70
- headers: { "x-workflow-run-id": run.runId },
71
- });
72
- }
73
- ```
63
+ Three files. The bot definition is separate from the workflow so adapter packages stay out of the workflow sandbox.
64
+
65
+ <Tabs items={['Bot Setup', 'Workflow', 'Event Handlers']}>
66
+
67
+ <Tab value="Bot Setup">
68
+
69
+ Register the `Chat` instance as a singleton so step functions can dynamically import it and resolve adapters + state:
74
70
 
75
- The client uses `WorkflowChatTransport` for automatic stream resumption.
76
-
77
- ```typescript title="components/chat.tsx" lineNumbers
78
- "use client";
79
-
80
- import { useChat } from "@ai-sdk/react";
81
- import { WorkflowChatTransport } from "@workflow/ai";
82
-
83
- export function Chat() {
84
- const chat = useChat({
85
- transport: new WorkflowChatTransport({ api: "/api/chat" }), // [!code highlight]
86
- });
87
-
88
- return (
89
- <div>
90
- {chat.messages.map((m) => (
91
- <div key={m.id}>{m.content}</div>
92
- ))}
93
- <form onSubmit={chat.handleSubmit}>
94
- <input value={chat.input} onChange={chat.handleInputChange} />
95
- </form>
96
- </div>
97
- );
71
+ ```typescript title="lib/bot.ts" lineNumbers
72
+ import { Chat } from "chat";
73
+ import { createSlackAdapter } from "@chat-adapter/slack";
74
+ import { createRedisState } from "@chat-adapter/state-redis";
75
+
76
+ const adapters = {
77
+ slack: createSlackAdapter(),
78
+ };
79
+
80
+ export interface ThreadState {
81
+ runId?: string; // [!code highlight]
98
82
  }
83
+
84
+ export const bot = new Chat<typeof adapters, ThreadState>({
85
+ userName: "durable-bot",
86
+ adapters,
87
+ state: createRedisState(),
88
+ dedupeTtlMs: 600_000,
89
+ }).registerSingleton(); // [!code highlight]
99
90
  ```
100
91
 
101
- ## Multi-Turn: Durable Sessions
92
+ `registerSingleton()` is important: Chat SDK re-hydrates `Thread` objects inside step functions, and it needs a registered singleton to resolve adapters and state for those rehydrated instances.
102
93
 
103
- A single workflow manages the entire conversation. The workflow loops, waiting for new messages via a hook. This gives you server-side ownership of the full chat history.
94
+ </Tab>
104
95
 
105
- ```typescript title="workflows/durable-chat.ts" lineNumbers
106
- import { DurableAgent } from "@workflow/ai/agent";
107
- import {
108
- convertToModelMessages,
109
- type UIMessage,
110
- type UIMessageChunk,
111
- } from "ai";
112
- import { defineHook, getWritable, getWorkflowMetadata } from "workflow";
113
- import { z } from "zod";
96
+ <Tab value="Workflow">
114
97
 
115
- const chatMessageHook = defineHook({ // [!code highlight]
116
- schema: z.object({
117
- messages: z.array(z.any()),
118
- }),
119
- });
98
+ The workflow is a plain loop over a hook. It receives the serialized thread + first message from the handler, revives them via Chat SDK's standalone `reviver`, and every platform-side effect goes inside a `"use step"` helper:
99
+
100
+ ```typescript title="workflows/durable-chat-session.ts" lineNumbers
101
+ import { Message, reviver, type Thread } from "chat";
102
+ import { defineHook, getWorkflowMetadata } from "workflow";
103
+ import type { ThreadState } from "@/lib/bot";
104
+
105
+ // Hook payload lives in its own file so the webhook side can import it without
106
+ // pulling in the workflow module.
107
+ import type { ChatTurnPayload } from "@/workflows/chat-turn-hook";
108
+
109
+ const chatTurnHook = defineHook<ChatTurnPayload>(); // [!code highlight]
110
+
111
+ async function postAssistantMessage(
112
+ thread: Thread<ThreadState>,
113
+ text: string
114
+ ) {
115
+ "use step";
116
+ // Dynamic import keeps adapter packages out of the workflow sandbox.
117
+ const { bot } = await import("@/lib/bot"); // [!code highlight]
118
+ await bot.initialize();
119
+ await thread.post(text);
120
+ }
121
+
122
+ async function runTurn(text: string) {
123
+ "use step";
124
+ // Your AI SDK call, database lookup, tool loop, etc.
125
+ return `You said: ${text}`;
126
+ }
120
127
 
121
- export async function durableChat(initialMessages: UIMessage[]) {
128
+ async function handleMessage(
129
+ thread: Thread<ThreadState>,
130
+ message: Message
131
+ ) {
132
+ const text = message.text.trim();
133
+ if (text.toLowerCase() === "done") return false;
134
+
135
+ const reply = await runTurn(text);
136
+ await postAssistantMessage(thread, reply);
137
+ return true;
138
+ }
139
+
140
+ export async function durableChatSession(payload: string) {
122
141
  "use workflow";
123
142
 
124
143
  const { workflowRunId } = getWorkflowMetadata();
125
- let allMessages = await convertToModelMessages(initialMessages);
126
-
127
- const agent = new DurableAgent({
128
- model: "anthropic/claude-sonnet-4-20250514",
129
- instructions: "You are a helpful assistant.",
130
- tools: { /* your tools here */ },
131
- });
132
-
133
- // First turn
134
- const firstResult = await agent.stream({
135
- messages: allMessages,
136
- writable: getWritable<UIMessageChunk>(),
137
- preventClose: true,
138
- });
139
- allMessages = firstResult.messages;
140
-
141
- // Subsequent turns -- wait for new messages via hook
144
+ const { thread, message } = JSON.parse(payload, reviver) as { // [!code highlight]
145
+ thread: Thread<ThreadState>;
146
+ message: Message;
147
+ };
148
+
149
+ const hook = chatTurnHook.create({ token: workflowRunId });
150
+
151
+ await postAssistantMessage(thread, "Session started. Reply here; send `done` to stop.");
152
+
153
+ if (!(await handleMessage(thread, message))) return;
154
+
155
+ // Each hook resumption is one turn. The workflow stays suspended between
156
+ // messages — zero compute cost while idle.
142
157
  while (true) {
143
- const hook = chatMessageHook.create({ token: workflowRunId });
144
- const { messages: newMessages } = await hook; // [!code highlight]
145
-
146
- allMessages = [
147
- ...allMessages,
148
- ...await convertToModelMessages(newMessages),
149
- ];
150
-
151
- const result = await agent.stream({
152
- messages: allMessages,
153
- writable: getWritable<UIMessageChunk>(),
154
- preventClose: true,
155
- });
156
- allMessages = result.messages;
158
+ const { message: nextRaw } = await hook; // [!code highlight]
159
+ const next = Message.fromJSON(nextRaw);
160
+ if (!(await handleMessage(thread, next))) return;
157
161
  }
158
162
  }
159
163
  ```
160
164
 
161
- ### Multi-Turn API Routes
165
+ ```typescript title="workflows/chat-turn-hook.ts" lineNumbers
166
+ import type { SerializedMessage } from "chat";
167
+
168
+ export type ChatTurnPayload = {
169
+ message: SerializedMessage;
170
+ };
171
+ ```
172
+
173
+ </Tab>
174
+
175
+ <Tab value="Event Handlers">
162
176
 
163
- You need two routes: one to start the session, another to send follow-up messages.
177
+ Handlers live outside the workflow file so adapter dependencies don't leak in. They decide whether to start a new workflow or resume an existing one, then store the `runId` in thread state:
164
178
 
165
- ```typescript title="app/api/chat/route.ts" lineNumbers
166
- import { createUIMessageStreamResponse } from "ai";
167
- import { start } from "workflow/api";
168
- import { durableChat } from "@/workflows/durable-chat";
179
+ <Callout type="info">
180
+ If the platform can deliver the same first message concurrently, use a deterministic hook token derived from the thread ID so duplicate handlers route to the active chat session hook. See [Run idempotency](/docs/foundations/idempotency#run-idempotency).
181
+ </Callout>
169
182
 
170
- export async function POST(request: Request) {
171
- const { messages } = await request.json();
172
- const run = await start(durableChat, [messages]); // [!code highlight]
183
+ ```typescript title="lib/chat-session-handlers.ts" lineNumbers
184
+ import type { Message, Thread } from "chat";
185
+ import { getRun, resumeHook, start } from "workflow/api";
186
+ import { bot, type ThreadState } from "@/lib/bot";
187
+ import { durableChatSession } from "@/workflows/durable-chat-session";
188
+ import type { ChatTurnPayload } from "@/workflows/chat-turn-hook";
173
189
 
174
- return createUIMessageStreamResponse({
175
- stream: run.readable,
176
- headers: { "x-workflow-run-id": run.runId },
177
- });
190
+ async function startSession(thread: Thread<ThreadState>, message: Message) {
191
+ const run = await start(durableChatSession, [ // [!code highlight]
192
+ JSON.stringify({
193
+ thread: thread.toJSON(),
194
+ message: message.toJSON(),
195
+ }),
196
+ ]);
197
+ await thread.setState({ runId: run.runId });
178
198
  }
199
+
200
+ async function routeTurn(thread: Thread<ThreadState>, message: Message) {
201
+ const state = await thread.state;
202
+
203
+ // No run yet, or the previous run finished — start fresh.
204
+ if (!state?.runId || !(await getRun(state.runId).exists)) {
205
+ await startSession(thread, message);
206
+ return;
207
+ }
208
+
209
+ try {
210
+ await resumeHook<ChatTurnPayload>(state.runId, { // [!code highlight]
211
+ message: message.toJSON(),
212
+ });
213
+ } catch (err) {
214
+ const msg = err instanceof Error ? err.message.toLowerCase() : "";
215
+ if (msg.includes("not found") || msg.includes("expired")) {
216
+ // Stale runId — start a new session rather than dropping the message.
217
+ await startSession(thread, message);
218
+ return;
219
+ }
220
+ throw err;
221
+ }
222
+ }
223
+
224
+ bot.onNewMention(async (thread, message) => {
225
+ await thread.subscribe();
226
+ await routeTurn(thread, message);
227
+ });
228
+
229
+ bot.onSubscribedMessage(async (thread, message) => {
230
+ await routeTurn(thread, message);
231
+ });
179
232
  ```
180
233
 
181
- ```typescript title="app/api/chat/follow-up/route.ts" lineNumbers
182
- import { resumeHook } from "workflow/api";
234
+ Wire Chat SDK's webhook handler into a catch-all route. Importing `chat-session-handlers` for side effects registers the event handlers before the first webhook arrives:
183
235
 
184
- export async function POST(request: Request) {
185
- const { runId, messages } = await request.json();
186
- await resumeHook(runId, { messages }); // [!code highlight]
187
- return new Response("OK");
236
+ ```typescript title="app/api/webhooks/[platform]/route.ts" lineNumbers
237
+ import "@/lib/chat-session-handlers";
238
+ import { after } from "next/server";
239
+ import { bot } from "@/lib/bot";
240
+
241
+ type Platform = keyof typeof bot.webhooks;
242
+
243
+ export async function POST(
244
+ req: Request,
245
+ { params }: { params: Promise<{ platform: string }> }
246
+ ) {
247
+ const { platform } = await params;
248
+ const handler = bot.webhooks[platform as Platform];
249
+ if (!handler) return new Response(`Unknown platform: ${platform}`, { status: 404 });
250
+
251
+ return handler(req, { waitUntil: (task) => after(() => task) }); // [!code highlight]
188
252
  }
189
253
  ```
190
254
 
191
- ## Choosing a Pattern
255
+ </Tab>
256
+
257
+ </Tabs>
258
+
259
+ ## How It Works
260
+
261
+ 1. **Thread state stores the `runId`.** Chat SDK's state adapter (Redis, Postgres, memory) holds `{ runId }` per thread. That's the only piece of glue between the two SDKs.
262
+ 2. **First mention → `start()`.** Handler serializes `thread` + `message` with `toJSON()`, passes them through `start(durableChatSession, [payload])`, stashes the returned `runId` in thread state.
263
+ 3. **Subsequent messages → `resumeHook()`.** Handler looks up the `runId`, serializes the new message, and resumes the workflow's hook. The workflow picks up on the next `await hook` iteration.
264
+ 4. **Workflow posts back via steps.** All Chat SDK side effects (`thread.post`, `thread.subscribe`, `thread.setState`) happen inside `"use step"` helpers that dynamically import the bot. This keeps adapter packages outside the workflow sandbox.
265
+ 5. **Session ends — two ways.** The workflow returns normally (user said `done`, approval granted, etc.), or the workflow throws. Either way the run completes; the next inbound message with the stale `runId` falls through to `startSession()`.
266
+
267
+ The workflow is fully durable between turns: `await hook` suspends with zero compute cost, and platform webhooks can fire from anywhere without concern for which server instance handled the previous turn.
268
+
269
+ ## Extending the Pattern
270
+
271
+ Because the session is just a workflow, everything else from the cookbook composes naturally:
272
+
273
+ - **Stream AI SDK responses into the thread.** Use the [AI SDK integration](/docs/cookbook/integrations/ai-sdk) pattern inside a step, then pass `result.fullStream` to `thread.post()` — Chat SDK handles platform-specific streaming (Slack edit-in-place, Telegram message-per-chunk, etc.).
274
+ - **Give the bot a sandbox.** Combine with the [Sandbox integration](/docs/cookbook/integrations/sandbox): each thread gets its own persistent sandbox session, snapshots on idle, resumes on the next message. That's effectively a coding-agent bot.
275
+ - **Human-in-the-loop approvals.** `Promise.race([hook, approvalHook])` inside the workflow, post buttons in the thread via [cards](https://chat-sdk.dev/docs/cards), resume `approvalHook` from `bot.onAction(...)`.
276
+ - **Scheduled follow-ups.** `sleep("24h")` before a proactive check-in. Surviving restarts is free.
277
+
278
+ ## Pitfalls
279
+
280
+ ### Don't import the bot at the top of workflow files
281
+
282
+ Adapter packages (`@chat-adapter/slack`, `@chat-adapter/telegram`, etc.) depend on Node-only modules that aren't available in the workflow bundler's sandbox. Keep `import { bot } from "@/lib/bot"` inside `"use step"` functions with `await import(...)`. Use `reviver` from `chat` for deserialization inside the workflow — it's standalone and has no adapter dependencies.
283
+
284
+ ### Register the bot as a singleton
285
+
286
+ `new Chat({...}).registerSingleton()`. Chat SDK rehydrates `Thread` objects inside step functions via `reviver`, and it looks up adapters + state from the registered singleton. Without it, thread methods throw when called from step contexts.
287
+
288
+ ### Hook payloads must be JSON-serializable
289
+
290
+ `Message` and `Thread` have methods, so pass them through `.toJSON()` / `Message.fromJSON()` across the hook boundary. Define a `ChatTurnPayload` type in its own file so both the webhook handler (in the Node bundle) and the workflow (in the workflow sandbox) can share it without dragging in adapter code.
291
+
292
+ ### Handle stale `runId`s
293
+
294
+ A workflow run ends but its `runId` is still cached in thread state. The next message calls `resumeHook` on a dead run and throws `not found` / `expired`. Gate on `getRun(runId).exists` before resuming, or catch the error and fall through to `startSession`. Either way the user's message must not be dropped.
295
+
296
+ ### Make first-message routing atomic
297
+
298
+ Thread state is also the idempotency boundary for starting sessions. Back it with a state adapter or database operation that can atomically claim the thread before `startSession()` runs when duplicate sessions would be harmful.
299
+
300
+ ### Keep the hook outside the loop
301
+
302
+ One `chatTurnHook.create({ token: workflowRunId })` per workflow run, reused every iteration. Creating a new hook with the same token throws `HookConflictError`. This is the same rule as the [AI SDK](/docs/cookbook/integrations/ai-sdk) and [Sandbox](/docs/cookbook/integrations/sandbox) session patterns.
303
+
304
+ ### Platform timeouts are separate from workflow timeouts
192
305
 
193
- | | Single-Turn | Multi-Turn |
194
- |---|---|---|
195
- | **State ownership** | Client | Server (workflow event log) |
196
- | **Message injection** | Not needed | Via hooks |
197
- | **Complexity** | Low | Medium |
198
- | **Session duration** | Per-request | Minutes to hours |
199
- | **Crash recovery** | Client resends full history | Workflow replays from event log |
306
+ Slack wants a 200 within 3 seconds. The webhook handler returns immediately after `resumeHook` (which is fast) — the workflow then runs in the background and posts back via `thread.post`. Don't try to `await` the whole turn inside the webhook handler; that's what breaks in the naive integration.
200
307
 
201
- Start with single-turn. Move to multi-turn when you need server-owned state, message injection from external sources, or sessions that outlive the browser tab.
308
+ ## Key APIs
202
309
 
203
- See [Chat Session Modeling](/docs/ai/chat-session-modeling) for the full guide including multiplayer patterns and message queueing.
310
+ - [`Chat`](https://chat-sdk.dev/docs/api/chat) / [`Thread`](https://chat-sdk.dev/docs/api/thread) / [`Message`](https://chat-sdk.dev/docs/api/message) Chat SDK primitives. `toJSON()` / `fromJSON()` / `reviver` are the serialization layer.
311
+ - [`start()`](/docs/api-reference/workflow-api/start) — start a new session workflow. Store the returned `runId` in thread state.
312
+ - [`resumeHook()`](/docs/api-reference/workflow-api/resume-hook) — forward a new platform message to the running workflow.
313
+ - [`getRun()`](/docs/api-reference/workflow-api/get-run) — `run.exists` before resuming, to detect stale `runId`s.
314
+ - [`defineHook()`](/docs/api-reference/workflow/define-hook) — per-turn suspension point inside the workflow.
315
+ - [`registerSingleton()`](https://chat-sdk.dev/docs/api/chat) — makes the bot resolvable from inside step functions.
316
+ - [Idempotency](/docs/foundations/idempotency) — protect duplicate-sensitive first messages and side effects.