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

推荐订阅源

The GitHub Blog
The GitHub Blog
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Hacker News: Ask HN
Hacker News: Ask HN
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
Schneier on Security
Schneier on Security
H
Hackread – Cybersecurity News, Data Breaches, AI and More
F
Full Disclosure
S
Secure Thoughts
大猫的无限游戏
大猫的无限游戏
www.infosecurity-magazine.com
www.infosecurity-magazine.com
P
Proofpoint News Feed
Hacker News - Newest:
Hacker News - Newest: "LLM"
罗磊的独立博客
S
Schneier on Security
H
Hacker News: Front Page
H
Heimdal Security Blog
美团技术团队
Hugging Face - Blog
Hugging Face - Blog
Apple Machine Learning Research
Apple Machine Learning Research
P
Privacy International News Feed
博客园 - 三生石上(FineUI控件)
P
Palo Alto Networks Blog
PCI Perspectives
PCI Perspectives
NISL@THU
NISL@THU
T
Troy Hunt's Blog
Project Zero
Project Zero
Y
Y Combinator Blog
阮一峰的网络日志
阮一峰的网络日志
G
GRAHAM CLULEY
小众软件
小众软件
月光博客
月光博客
Google DeepMind News
Google DeepMind News
W
WeLiveSecurity
C
Cisco Blogs
腾讯CDC
Blog — PlanetScale
Blog — PlanetScale
I
Intezer
I
InfoQ
WordPress大学
WordPress大学
F
Fortinet All Blogs
T
Threat Research - Cisco Blogs
N
News and Events Feed by Topic
T
Tor Project blog
N
News | PayPal Newsroom
A
Arctic Wolf
有赞技术团队
有赞技术团队
博客园 - Franky
Vercel News
Vercel News
宝玉的分享
宝玉的分享
Application and Cybersecurity Blog
Application and Cybersecurity Blog

Chat SDK Documentation

Vercel Connect | Chat SDK Teams Low-Level APIs | Chat SDK CLI | Chat SDK Platform Adapters | Chat SDK State Adapters | Chat SDK Cards | Chat SDK Getting Started | Chat SDK Introduction | Chat SDK Modals | Chat SDK Slack Low-Level APIs | Chat SDK Streaming | Chat SDK Testing | Chat SDK Overview | Chat SDK toAiMessages | Chat SDK Cards | Chat SDK Overview | Chat SDK Markdown | Chat SDK Modals | Chat SDK AI SDK Tools | Chat SDK Types | Chat SDK Message Subject | Chat SDK Conversation History | Chat SDK Transcripts | Chat SDK Slack bot with Next.js and Redis Actions | Chat SDK Direct Messages | Chat SDK Emoji | Chat SDK Error Handling | Chat SDK File Uploads | Chat SDK Handling Events | Chat SDK Posting Messages | Chat SDK Slash Commands | Chat SDK State Adapters | Chat SDK Threads, Messages, and Channels | Chat SDK Creating a Chat Instance | Chat SDK WhatsApp Business Cloud Channel | Chat SDK Chat | Chat SDK Platform Adapters | Chat SDK Overlapping Messages | Chat SDK Ephemeral Messages | Chat SDK Message | Chat SDK Thread | Chat SDK Building a community adapter | Chat SDK Documenting your adapter | Chat SDK Publishing your adapter | Chat SDK Testing adapters | Chat SDK Code review GitHub bot with Hono and Redis Discord support bot with Nuxt and Redis Schedule Slack posts with Next.js, Workflow, and Neon PostableMessage | Chat SDK
Durable chat sessions with Next.js, Workflow, and Redis
Vercel · 2026-04-06 · via Chat SDK Documentation

This guide walks through combining Chat SDK and Workflow so a chat thread can survive restarts, wait for follow-up messages, and keep its session state in Redis.

Chat SDK and Workflow solve different parts of the same problem.

Chat SDK normalizes incoming platform events into thread and message objects and gives you a consistent way to reply. Workflow gives you durable execution so a session can wait for the next turn without holding a request open or losing state on restart.

This guide uses Slack and Next.js for a concrete example, but the same pattern works with any Chat SDK adapter.

  • Node.js 18+
  • pnpm (or npm/yarn)
  • A Next.js App Router project
  • A Slack workspace where you can install apps
  • A Redis instance for Chat SDK state

Install Chat SDK, the Slack adapter, Redis state, and Workflow:

pnpm add chat @chat-adapter/slack @chat-adapter/state-redis workflow

Wrap your Next.js config with withWorkflow() so "use workflow" and "use step" directives are compiled correctly:

import { withWorkflow } from "workflow/next";
import type { NextConfig } from "next";

const nextConfig: NextConfig = {
  // ...your existing config
};

export default withWorkflow(nextConfig);

If your app uses proxy.ts, exclude .well-known/workflow/ from the matcher so Workflow's internal routes are not intercepted.

Create a bot instance exactly as you would for a normal Chat SDK app, but keep the bot definition separate from the workflow code:

import { createRedisState } from "@chat-adapter/state-redis";
import { createSlackAdapter } from "@chat-adapter/slack";
import { Chat } from "chat";

const adapters = {
  slack: createSlackAdapter(),
};

export interface ThreadState {
  runId?: string;
}

export const bot = new Chat<typeof adapters, ThreadState>({
  userName: "durable-bot",
  adapters,
  state: createRedisState(),
}).registerSingleton();

runId will store the active workflow run for each subscribed thread.

registerSingleton() matters here because Workflow may deserialize Thread objects again inside "use step" functions, and Chat SDK needs a registered singleton to resolve the adapter and state layer for those thread instances.

Workflow hooks are how follow-up messages get injected back into a running session. Define the payload type once so both the workflow and the webhook side stay in sync:

import type { SerializedMessage } from "chat";

export type ChatTurnPayload = {
  message: SerializedMessage;
};

The workflow receives the serialized thread and first message, restores them with reviver, and then keeps waiting for more turns through the hook.

The important detail is that the workflow only orchestrates. Chat SDK side effects such as post(), unsubscribe(), and setState() stay inside step helpers:

import { Message, reviver, type Thread } from "chat";
import { createHook, getWorkflowMetadata } from "workflow";
import type { ThreadState } from "@/lib/bot";
import type { ChatTurnPayload } from "@/workflows/chat-turn-hook";

async function postAssistantMessage(
  thread: Thread<ThreadState>,
  text: string
) {
  "use step";

  const { bot } = await import("@/lib/bot");
  await bot.initialize();
  await thread.post(text);
}

async function closeSession(thread: Thread<ThreadState>) {
  "use step";

  const { bot } = await import("@/lib/bot");
  await bot.initialize();
  await thread.post("Session closed.");
  await thread.unsubscribe();
  await thread.setState({}, { replace: true });
}

async function runTurn(text: string) {
  "use step";

  // Replace this with AI SDK calls, database work, or other business logic.
  return `You said: ${text}`;
}

async function processMessage(
  thread: Thread<ThreadState>,
  message: Message
) {
  const text = message.text.trim();

  if (text.toLowerCase() === "done") {
    await closeSession(thread);
    return false;
  }

  const reply = await runTurn(text);
  await postAssistantMessage(thread, reply);
  return true;
}

export async function durableChatSession(payload: string) {
  "use workflow";

  const { workflowRunId } = getWorkflowMetadata();
  const { thread, message } = JSON.parse(payload, reviver) as {
    thread: Thread<ThreadState>;
    message: Message;
  };

  using hook = createHook<ChatTurnPayload>({ token: workflowRunId });

  await postAssistantMessage(
    thread,
    "Durable session started. Reply in this thread and send `done` when you want to stop."
  );

  const shouldContinue = await processMessage(thread, message);
  if (!shouldContinue) {
    return;
  }

  for await (const event of hook) {
    const nextMessage = Message.fromJSON(event.message);

    const keepRunning = await processMessage(thread, nextMessage);
    if (!keepRunning) {
      return;
    }
  }
}

The using keyword requires TypeScript 5.2+ with "lib": ["esnext.disposable"] in your tsconfig.json. If you are on an older version, call hook.dispose() manually when the session ends.

Do not import bot at the top level of a workflow file. Adapter packages depend on Node.js modules that are not available in the workflow sandbox. Use the standalone reviver for deserialization and import bot dynamically inside "use step" functions where Node.js modules are available.

This is the core integration:

  • thread.toJSON() and message.toJSON() cross the workflow boundary safely
  • reviver restores real Chat SDK objects inside the workflow without pulling in adapter dependencies
  • registerSingleton() is called in lib/bot.ts and the singleton is available inside step functions when bot is dynamically imported
  • createHook<ChatTurnPayload>({ token: workflowRunId }) makes the workflow run itself the session identifier
  • runTurn(), postAssistantMessage(), and closeSession() are steps, so adapter and state side effects stay outside the workflow sandbox

Create a small side-effect module that decides whether to start a new workflow or resume the existing one:

import { type Message, type Thread } from "chat";
import { resumeHook, start } from "workflow/api";
import { bot, type ThreadState } from "@/lib/bot";
import { durableChatSession } from "@/workflows/durable-chat-session";
import type { ChatTurnPayload } from "@/workflows/chat-turn-hook";

async function startSession(
  thread: Thread<ThreadState>,
  message: Message
) {
  const run = await start(durableChatSession, [
    JSON.stringify({
      thread: thread.toJSON(),
      message: message.toJSON(),
    }),
  ]);

  await thread.setState({ runId: run.runId });
}

async function routeTurn(
  thread: Thread<ThreadState>,
  message: Message
) {
  const state = await thread.state;

  if (!state?.runId) {
    await startSession(thread, message);
    return;
  }

  await resumeHook<ChatTurnPayload>(state.runId, {
    message: message.toJSON(),
  });
}

bot.onNewMention(async (thread, message) => {
  await thread.subscribe();
  await routeTurn(thread, message);
});

bot.onSubscribedMessage(async (thread, message) => {
  await routeTurn(thread, message);
});

On the first mention, the handler subscribes the thread and starts a workflow. Every later message resumes the existing run by sending the serialized message to the hook.

In production, catch resumeHook() failures, clear stale runId values, and start a new session if the old workflow has already ended.

Import the side-effect module once so the handlers are registered before the webhook runs:

import "@/lib/chat-session-handlers";
import { after } from "next/server";
import { bot } from "@/lib/bot";

type Platform = keyof typeof bot.webhooks;

export async function POST(
  request: Request,
  context: RouteContext<"/api/webhooks/[platform]">
) {
  const { platform } = await context.params;

  const handler = bot.webhooks[platform as Platform];
  if (!handler) {
    return new Response(`Unknown platform: ${platform}`, { status: 404 });
  }

  return handler(request, {
    waitUntil: (task) => after(() => task),
  });
}

The workflow pattern stays the same if you want AI responses. Replace runTurn() with a step that calls AI SDK:

import { anthropic } from "@ai-sdk/anthropic";
import { generateText } from "ai";

async function runTurn(text: string) {
  "use step";

  const { text: reply } = await generateText({
    model: anthropic("claude-sonnet-4-5"),
    system: "You are a helpful assistant in a chat thread.",
    prompt: text,
  });

  return reply;
}

Install the extra packages if you use this version:

pnpm add ai @ai-sdk/anthropic
  1. A user @mentions the bot in a thread.
  2. Chat SDK subscribes the thread and starts durableChatSession().
  3. The handler stores the workflow runId in Chat SDK thread state.
  4. Follow-up messages call resumeHook(runId, ...) instead of starting a new run.
  5. The workflow keeps ownership of the session until the user sends done or you end it some other way.

This gives you a durable session boundary without moving platform-specific webhook code into your workflow layer.

From here you can add:

  • inactivity timeouts with Workflow sleep()
  • escalation or approval pauses with additional hooks
  • AI-generated replies, tool calls, or human handoffs inside "use step" functions