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

推荐订阅源

A
About on SuperTechFans
博客园 - 聂微东
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 司徒正美
宝玉的分享
宝玉的分享
美团技术团队
量子位
The Cloudflare Blog
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
IT之家
IT之家
爱范儿
爱范儿
J
Java Code Geeks
博客园 - Franky
Last Week in AI
Last Week in AI
B
Blog
H
Hackread – Cybersecurity News, Data Breaches, AI and More
I
InfoQ
GbyAI
GbyAI
Recent Announcements
Recent Announcements
小众软件
小众软件
H
Help Net Security
Microsoft Azure Blog
Microsoft Azure Blog
MyScale Blog
MyScale Blog

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.