惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

GbyAI
GbyAI
人人都是产品经理
人人都是产品经理
Hugging Face - Blog
Hugging Face - Blog
罗磊的独立博客
博客园 - 【当耐特】
D
Docker
Y
Y Combinator Blog
L
LangChain Blog
博客园 - 三生石上(FineUI控件)
I
InfoQ
阮一峰的网络日志
阮一峰的网络日志
F
Fortinet All Blogs
J
Java Code Geeks
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
V2EX
B
Blog
The GitHub Blog
The GitHub Blog
腾讯CDC
MongoDB | Blog
MongoDB | Blog
博客园 - Franky
爱范儿
爱范儿
A
About on SuperTechFans
量子位
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC

Workflow SDK Documentation

Patterns for Defining Tools Human-in-the-Loop Building Durable AI Agents Queueing User Messages Resumable Streams Sleep, Suspense, and Scheduling Streaming Updates from Tools API Reference Workflow Globals Changelog Resilient run start Cookbook Building a World Deploying Astro Express Fastify Hono Getting Started NestJS Next.js Nitro Nuxt Python SvelteKit Vite corrupted-event-log fetch-in-workflow hook-conflict Errors
Chat SDK
2026-05-31 · via Workflow SDK Documentation

Make Chat SDK bot sessions durable — one workflow run per conversation thread, with hooks bridging inbound platform events into long-running agent logic.

Chat SDK 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.

Workflow SDK complements it by making bot sessions durable. Each conversation thread maps to a long-running workflow run that:

  • Owns multi-turn state in the durable event log instead of Redis-by-hand bookkeeping
  • Can sleep() for hours or days waiting for a user reply, an approval, or a scheduled follow-up
  • Survives deploys, cold starts, and crashes — the session picks up from the last step on replay
  • Receives follow-up messages via hooks, so the bot stays responsive while the workflow is still running

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 for explicit child-run and handoff patterns using deploymentId: "latest".

The rest of this page covers the integration pattern. For a full Slack + Next.js + Redis walkthrough, see the Durable chat sessions guide on chat-sdk.dev.

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:

  • 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).
  • 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.

Without Workflow, a long-running bot session usually means one of:

  • Holding a webhook request open while the agent runs (doesn't survive restarts, blows past platform timeouts)
  • Writing session state to Redis manually, plus a scheduler for timeouts and retries, plus custom reconnection logic

Workflow replaces all of that with a single durable function. The bot can:

  • Run a tool loop for minutes while the user watches typing indicators
  • Wait for a human approval in another thread before continuing
  • Schedule a follow-up message 24 hours later via sleep("24h")
  • Pause on sandbox snapshot, resume when the user sends the next command (see the Sandbox integration)

Because the session is a workflow run, its history is recoverable from the event log — no separate message store to keep in sync.

Three files. The bot definition is separate from the workflow so adapter packages stay out of the workflow sandbox.

  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.
  2. First mention → start(). Handler serializes thread + message with toJSON(), passes them through start(durableChatSession, [payload]), stashes the returned runId in thread state.
  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.
  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.
  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().

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.

Because the session is just a workflow, everything else from the cookbook composes naturally:

  • Stream AI SDK responses into the thread. Use the AI SDK integration 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.).
  • Give the bot a sandbox. Combine with the Sandbox integration: each thread gets its own persistent sandbox session, snapshots on idle, resumes on the next message. That's effectively a coding-agent bot.
  • Human-in-the-loop approvals. Promise.race([hook, approvalHook]) inside the workflow, post buttons in the thread via cards, resume approvalHook from bot.onAction(...).
  • Scheduled follow-ups. sleep("24h") before a proactive check-in. Surviving restarts is free.

Don't import the bot at the top of workflow files

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.

Register the bot as a singleton

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.

Hook payloads must be JSON-serializable

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.

Handle stale runIds

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.

Keep the hook outside the loop

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 and Sandbox session patterns.

Platform timeouts are separate from workflow timeouts

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.

  • Chat / Thread / Message — Chat SDK primitives. toJSON() / fromJSON() / reviver are the serialization layer.
  • start() — start a new session workflow. Store the returned runId in thread state.
  • resumeHook() — forward a new platform message to the running workflow.
  • getRun()run.exists before resuming, to detect stale runIds.
  • defineHook() — per-turn suspension point inside the workflow.
  • registerSingleton() — makes the bot resolvable from inside step functions.