Loading...
Loading...
Vercel Chat SDK expert guidance. Use when building multi-platform chat bots — Slack, Telegram, Microsoft Teams, Discord, Google Chat, GitHub, Linear — with a single codebase. Covers the Chat class, adapters, threads, messages, cards, modals, streaming, state management, and webhook setup.
npx skill4agent add vercel-labs/vercel-plugin chat-sdkCRITICAL — Your training data is outdated for this library. Chat SDK is new (v4.18+) and not in most training data. Before writing Chat SDK code, fetch the docs at https://chat-sdk.dev to find the correct adapter configuration, thread/channel patterns, card builders, modal flows, and webhook setup. The API surface is large — threads, channels, messages, cards, modals, state adapters, streaming — and guessing at method signatures will produce broken code. Check the GitHub repo at https://github.com/vercel/chat for working examples.
chat@^4.18.0@chat-adapter/slack@^4.18.0@chat-adapter/telegram@^4.18.0@chat-adapter/teams@^4.18.0@chat-adapter/discord@^4.18.0@chat-adapter/gchat@^4.18.0@chat-adapter/github@^4.18.0@chat-adapter/linear@^4.18.0@chat-adapter/state-redis@^4.18.0@chat-adapter/state-ioredis@^4.18.0@chat-adapter/state-memory@^4.18.0# Core SDK
npm install chat@^4.18.0
# Platform adapters (install only what you need)
npm install @chat-adapter/slack@^4.18.0
npm install @chat-adapter/telegram@^4.18.0
npm install @chat-adapter/teams@^4.18.0
npm install @chat-adapter/discord@^4.18.0
npm install @chat-adapter/gchat@^4.18.0
npm install @chat-adapter/github@^4.18.0
npm install @chat-adapter/linear@^4.18.0
# State adapters (pick one)
npm install @chat-adapter/state-redis@^4.18.0
npm install @chat-adapter/state-ioredis@^4.18.0
npm install @chat-adapter/state-memory@^4.18.0Fieldoptions{ label, value }Thread<TState>Channel<TState>Record<string, unknown>signingSecretimport { createSlackAdapter } from "@chat-adapter/slack";
let slackAdapter: ReturnType<typeof createSlackAdapter> | undefined;
export function getSlackAdapter() {
if (!slackAdapter) {
slackAdapter = createSlackAdapter({
signingSecret: process.env.SLACK_SIGNING_SECRET!,
});
}
return slackAdapter;
}import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { createTelegramAdapter } from "@chat-adapter/telegram";
import { createRedisState } from "@chat-adapter/state-redis";
const bot = new Chat({
userName: "my-bot",
adapters: {
slack: createSlackAdapter(),
telegram: createTelegramAdapter(),
},
state: createRedisState(),
streamingUpdateIntervalMs: 1000,
dedupeTtlMs: 10_000,
fallbackStreamingPlaceholderText: "Thinking...",
});
bot.onNewMention(async (thread, message) => {
await thread.subscribe();
await thread.post(`Received: ${message.text}`);
});
bot.onSubscribedMessage(async (thread, message) => {
await thread.post(`Echo: ${message.text}`);
});interface ChatConfig<TAdapters> {
userName: string;
adapters: TAdapters;
state: StateAdapter;
logger?: Logger | LogLevel;
streamingUpdateIntervalMs?: number;
dedupeTtlMs?: number;
fallbackStreamingPlaceholderText?: string | null;
}dedupeTtlMsfallbackStreamingPlaceholderTextnullclass Chat {
openDM(userId: string): Promise<Channel>;
channel(channelId: string): Channel;
}openDM()channel()ThreadChannelPostableinterface Postable<TState extends Record<string, unknown> = Record<string, unknown>> {
post(content: PostableContent): Promise<SentMessage>;
postEphemeral(
userId: string,
content: PostableContent,
): Promise<SentMessage | null>;
mentionUser(userId: string): string;
startTyping(): Promise<void>;
messages: AsyncIterable<Message>;
state: Promise<TState | null>;
setState(
partial: Partial<TState>,
opts?: { replace?: boolean },
): Promise<void>;
}interface Thread<TState extends Record<string, unknown> = Record<string, unknown>> extends Postable<TState> {
id: string;
channelId: string;
subscribe(): Promise<void>;
unsubscribe(): Promise<void>;
isSubscribed(): Promise<boolean>;
refresh(): Promise<void>;
createSentMessageFromMessage(message: Message): SentMessage;
}class Message<TRaw = unknown> {
id: string;
threadId: string;
text: string;
isMention: boolean;
raw: TRaw;
toJSON(): SerializedMessage;
static fromJSON(data: SerializedMessage): Message;
}interface SentMessage extends Message {
edit(content: PostableContent): Promise<void>;
delete(): Promise<void>;
addReaction(emoji: string): Promise<void>;
removeReaction(emoji: string): Promise<void>;
}SentMessageMessageconst sent = await thread.post('Done'); await sent.addReaction('thumbsup');const sent = await thread.post("Done");
await sent.addReaction("thumbsup");interface Channel<TState extends Record<string, unknown> = Record<string, unknown>> extends Postable<TState> {
id: string;
name?: string;
}onNewMention(handler)onSubscribedMessage(handler)onNewMessage(pattern, handler)onReaction(filter?, handler)onAction(filter?, handler)onModalSubmit(filter?, handler)onModalClose(filter?, handler)onSlashCommand(filter?, handler)onMemberJoinedChannel(handler)bot.onMemberJoinedChannel(async (event) => {
await event.thread.post(`Welcome ${event.user.fullName}!`);
});onActiononModalSubmitonModalCloseonReactionbot.onAction(async (event) => { ... })bot.onAction("approve", async (event) => { ... })bot.onAction(["approve", "reject"], async (event) => { ... })interface ActionEvent {
actionId: string;
value?: string;
triggerId?: string;
privateMetadata?: string;
thread: Thread;
relatedThread?: Thread;
relatedMessage?: Message;
openModal: (modal: JSX.Element) => Promise<void>;
}
interface ModalEvent {
callbackId: string;
values: Record<string, string>;
triggerId?: string;
privateMetadata?: string;
relatedThread?: Thread;
relatedMessage?: Message;
}onModalSubmitModalResponseawait thread.post(
<Card
title="Build Status"
subtitle="Production"
imageUrl="https://example.com/preview.png"
>
<Text style="success">Deployment succeeded.</Text>
<Text style="muted">Commit: a1b2c3d</Text>
<Field
id="deploy-target"
label="Target"
options={[
{ label: "Staging", value: "staging" },
{ label: "Production", value: "prod" },
]}
value="prod"
/>
<Table>
<TableRow>
<TableCell>Region</TableCell>
<TableCell>us-east-1</TableCell>
</TableRow>
<TableRow>
<TableCell>Latency</TableCell>
<TableCell>128ms</TableCell>
</TableRow>
</Table>
<Actions>
<Button id="rollback" style="danger">
Rollback
</Button>
<CardLink url="https://vercel.com/dashboard">Open Dashboard</CardLink>
</Actions>
</Card>,
);Card.subtitleCard.imageUrlCardLinkFieldoptions{ label, value }[]TableTableRowTableCellTextdefaultmutedsuccesswarningdangercodeTable| Platform | Rendering |
|---|---|
| Slack | Block Kit table blocks |
| Teams / Discord | GFM markdown tables |
| Google Chat | Monospace text widgets |
| Telegram | Code blocks |
| GitHub / Linear | Markdown tables (existing pipeline) |
Table()<Table>
<TableRow>
<TableCell>Region</TableCell>
<TableCell>us-east-1</TableCell>
</TableRow>
<TableRow>
<TableCell>Latency</TableCell>
<TableCell>128ms</TableCell>
</TableRow>
</Table>await event.openModal(
<Modal
callbackId="deploy-form"
title="Deploy"
submitLabel="Deploy"
closeLabel="Cancel"
notifyOnClose
privateMetadata={JSON.stringify({ releaseId: "rel_123" })}
>
<TextInput id="reason" label="Reason" multiline />
</Modal>,
);privateMetadataimport { createSlackAdapter } from "@chat-adapter/slack";
const slack = createSlackAdapter();
// Env: SLACK_BOT_TOKEN, SLACK_SIGNING_SECRET
const oauthSlack = createSlackAdapter({
clientId: process.env.SLACK_CLIENT_ID!,
clientSecret: process.env.SLACK_CLIENT_SECRET!,
encryptionKey: process.env.SLACK_ENCRYPTION_KEY,
});import { createTelegramAdapter } from "@chat-adapter/telegram";
const telegram = createTelegramAdapter();
// Env: TELEGRAM_BOT_TOKEN, TELEGRAM_WEBHOOK_SECRETimport { createTeamsAdapter } from "@chat-adapter/teams";
const teams = createTeamsAdapter({
appType: "singleTenant",
});
// Env: TEAMS_APP_ID, TEAMS_APP_PASSWORD, TEAMS_APP_TENANT_IDimport { createDiscordAdapter } from "@chat-adapter/discord";
const discord = createDiscordAdapter();
// Env: DISCORD_BOT_TOKEN, DISCORD_PUBLIC_KEY, DISCORD_APPLICATION_ID, CRON_SECRETimport { createGoogleChatAdapter } from "@chat-adapter/gchat";
const gchat = createGoogleChatAdapter();
// Env: GOOGLE_CHAT_CREDENTIALS, GOOGLE_CHAT_USE_ADCimport { createGitHubAdapter } from "@chat-adapter/github";
const github = createGitHubAdapter({
botUserId: process.env.GITHUB_BOT_USER_ID,
});
// Env: GITHUB_TOKEN or (GITHUB_APP_ID + GITHUB_PRIVATE_KEY),
// GITHUB_WEBHOOK_SECRET, GITHUB_INSTALLATION_IDimport { createLinearAdapter } from "@chat-adapter/linear";
const linear = createLinearAdapter({
clientId: process.env.LINEAR_CLIENT_ID,
clientSecret: process.env.LINEAR_CLIENT_SECRET,
accessToken: process.env.LINEAR_ACCESS_TOKEN,
});import { createRedisState } from "@chat-adapter/state-redis";
const state = createRedisState();
// Env: REDIS_URL (or REDIS_HOST/REDIS_PORT/REDIS_PASSWORD)import { createIoRedisState } from "@chat-adapter/state-ioredis";
const state = createIoRedisState({
// cluster/sentinel options
});import { MemoryState } from "@chat-adapter/state-memory";
const state = new MemoryState();// app/api/webhooks/slack/route.ts
import { bot } from "@/lib/bot";
import { after } from "next/server";
export async function POST(req: Request) {
return bot.webhooks.slack(req, {
waitUntil: (p) => after(() => p),
});
}// app/api/webhooks/telegram/route.ts
import { bot } from "@/lib/bot";
export async function POST(req: Request) {
return bot.webhooks.telegram(req);
}// pages/api/bot.ts
export default async function handler(req, res) {
const response = await bot.webhooks.slack(req);
res.status(response.status).send(await response.text());
}openDM()channel()bot.onAction("handoff", async (event) => {
const dm = await bot.openDM(event.user.id);
await dm.post("A human will follow up shortly.");
const ops = bot.channel("ops-alerts");
await ops.post(`Escalated by ${event.user.fullName}`);
});registerSingleton()reviver()bot.registerSingleton();
const serialized = JSON.stringify({ thread });
const revived = JSON.parse(serialized, bot.reviver());
await revived.thread.post("Resumed workflow step");// app/api/webhooks/slack/oauth/callback/route.ts
import { bot } from "@/lib/bot";
export async function GET(req: Request) {
return bot.oauth.slack.callback(req);
}onNewMentionthread.subscribe()message.isMention = trueonNewMessage(pattern, handler)onSubscribedMessageopenDM()channel()**bold**fallbackStreamingPlaceholderText: nullstreamingUpdateIntervalMsdedupeTtlMsstartTyping()GOOGLE_CHAT_CREDENTIALSGOOGLE_CHAT_USE_ADCappTypeTEAMS_APP_TENANT_IDGITHUB_INSTALLATION_IDbotUserIdclientIdclientSecretLINEAR_ACCESS_TOKEN