Looking for the chatbot template? It's now here.

Usage

Event handlers, threads, channels, messages, and the core patterns of Chat SDK.

Chat SDK uses an event-driven architecture. You register handlers for different event types, and the SDK routes incoming webhooks to the appropriate handler.

Event handlers

Mention handler

onNewMention fires when your bot is @-mentioned in a thread it hasn't subscribed to yet. This is the primary entry point for new conversations.

lib/bot.ts
bot.onNewMention(async (thread, message) => {
  await thread.subscribe();
  await thread.post("Hello! I'm now listening to this thread.");
});

Subscribed message handler

onSubscribedMessage fires for every new message in a thread your bot has subscribed to. Once subscribed, all messages (including @mentions) route here instead of onNewMention.

lib/bot.ts
bot.onSubscribedMessage(async (thread, message) => {
  // Check if the bot was @-mentioned specifically
  if (message.isMention) {
    await thread.post("You mentioned me!");
    return;
  }

  await thread.post(`Got your message: ${message.text}`);
});

Pattern handler

onNewMessage fires for messages matching a regex pattern in threads the bot is not subscribed to. Useful for keyword-triggered responses.

lib/bot.ts
bot.onNewMessage(/^help$/i, async (thread, message) => {
  await thread.post("Here's how I can help...");
});

Slash command handler

onSlashCommand fires when a user invokes a /command in the message composer. See Slash Commands for the full guide.

lib/bot.ts
bot.onSlashCommand("/status", async (event) => {
  await event.channel.post("All systems operational!");
});

Reaction handler

onReaction fires when users add or remove emoji reactions to messages.

lib/bot.ts
import { emoji } from "chat";

bot.onReaction(["thumbs_up", "heart"], async (event) => {
  if (!event.added) return;

  await event.adapter.addReaction(
    event.threadId,
    event.messageId,
    emoji.raised_hands
  );
});

The event object includes:

PropertyTypeDescription
emojiEmojiValueNormalized emoji for comparison
rawEmojistringPlatform-specific emoji string
addedbooleantrue if added, false if removed
userAuthorThe user who reacted
messageMessage (optional)The message that was reacted to
threadThreadThread for posting replies
adapterAdapterThe platform adapter

Assistant thread handler

onAssistantThreadStarted fires when a user opens a new assistant thread in Slack. Use it with the Slack Assistants API to set suggested prompts and status indicators. See Slack Assistants API for details.

lib/bot.ts
bot.onAssistantThreadStarted(async (event) => {
  const slack = bot.getAdapter("slack") as SlackAdapter;
  await slack.setSuggestedPrompts(event.channelId, event.threadTs, [
    { title: "Get started", message: "What can you help me with?" },
  ]);
});

App Home handler

onAppHomeOpened fires when a user opens your bot's Home tab in Slack. Use it to publish a dynamic view.

lib/bot.ts
bot.onAppHomeOpened(async (event) => {
  const slack = bot.getAdapter("slack") as SlackAdapter;
  await slack.publishHomeView(event.userId, {
    type: "home",
    blocks: [{ type: "section", text: { type: "mrkdwn", text: "Welcome!" } }],
  });
});

Threads

A Thread represents a conversation thread on any platform. It provides methods for posting messages, managing subscriptions, and accessing message history.

Post a message

lib/bot.ts
// Plain text
await thread.post("Hello world");

// Markdown (converted to each platform's format)
await thread.post("**Bold** and _italic_ text");

// Structured message with attachments
await thread.post({
  markdown: "Here's a file:",
  files: [{ data: buffer, filename: "report.pdf" }],
});

Subscribe and unsubscribe

Subscriptions persist across restarts (stored in your state adapter). When a thread is subscribed, all messages route to onSubscribedMessage.

lib/bot.ts
await thread.subscribe();
await thread.unsubscribe();

const subscribed = await thread.isSubscribed();

Typing indicator

lib/bot.ts
await thread.startTyping();

Not all platforms support typing indicators. The call is a no-op on unsupported platforms. See the adapter feature matrix for details.

Message history

Access recent messages or iterate through full history:

lib/bot.ts
// Cached messages from the webhook payload
const recent = thread.recentMessages;

// Newest first (auto-paginates)
for await (const msg of thread.messages) {
  console.log(msg.text);
}

// Oldest first (auto-paginates)
for await (const msg of thread.allMessages) {
  console.log(msg.text);
}

Thread state

Store typed, per-thread state that persists across requests:

lib/bot.ts
interface ThreadState {
  aiMode?: boolean;
  context?: string;
}

const bot = new Chat<typeof adapters, ThreadState>({
  // ...config
});

bot.onNewMention(async (thread) => {
  await thread.setState({ aiMode: true });

  const state = await thread.state; // ThreadState | null
  if (state?.aiMode) {
    // AI mode is enabled
  }
});

State is stored in your state adapter with a 30-day TTL. Use { replace: true } to replace state entirely instead of merging:

lib/bot.ts
await thread.setState({ aiMode: false }, { replace: true });

Messages

Incoming messages are normalized across platforms into a consistent format:

PropertyTypeDescription
idstringPlatform message ID
threadIdstringThread ID in adapter:channel:thread format
textstringPlain text content
formattedRootmdast AST representation
rawunknownOriginal platform-specific payload
authorAuthorMessage author info
metadataMessageMetadataTimestamps and edit status
attachmentsAttachment[] (optional)File attachments
isMentionboolean (optional)Whether the bot was @-mentioned

Author

interface Author {
  userId: string;
  userName: string;
  fullName: string;
  isBot: boolean | "unknown";
  isMe: boolean; // true if message is from the bot itself
}

Sent messages

When you post a message, you get back a SentMessage with methods to edit, delete, and react:

lib/bot.ts
const sent = await thread.post("Processing...");
// Do some work...
await sent.edit("Done!");

// Or delete
await sent.delete();

// Add/remove reactions
await sent.addReaction(emoji.check);
await sent.removeReaction(emoji.check);

Channels

A Channel represents the container that holds threads (e.g., a Slack channel, a Teams conversation). Navigate to a channel from a thread or get one directly:

lib/bot.ts
// From a thread
const channel = thread.channel;

// Directly by ID
const channel = bot.channel("slack:C123ABC");

List threads

Iterate threads in a channel, most recently active first:

lib/bot.ts
for await (const thread of channel.threads()) {
  console.log(thread.rootMessage.text, thread.replyCount);
}

Channel messages

Iterate top-level messages (not thread replies):

lib/bot.ts
for await (const msg of channel.messages) {
  console.log(msg.text);
}

Post to a channel

Post a top-level message (not inside a thread):

lib/bot.ts
await channel.post("Hello channel!");

Channel metadata

lib/bot.ts
const info = await channel.fetchMetadata();
console.log(info.name, info.memberCount);

Thread ID format

All thread IDs follow the pattern {adapter}:{channel}:{thread}:

  • Slack: slack:C123ABC:1234567890.123456
  • Teams: teams:{base64(conversationId)}:{base64(serviceUrl)}
  • Google Chat: gchat:spaces/ABC123:{base64(threadName)}
  • Discord: discord:{guildId}:{channelId}/{messageId}

You typically don't need to construct these yourself — they're provided by the SDK in event handlers.

Logging

The logger option is optional — if omitted, Chat SDK uses ConsoleLogger("info") by default. Each adapter also creates its own child logger automatically.

lib/bot.ts
// Use defaults (ConsoleLogger at "info" level)
const bot = new Chat({
  // ...
});

// Or set a specific log level
const bot = new Chat({
  // ...
  logger: "debug", // "debug" | "info" | "warn" | "error" | "silent"
});

// Or use a custom ConsoleLogger for child loggers
import { ConsoleLogger } from "chat";

const logger = new ConsoleLogger("info");
const bot = new Chat({
  // ...
  logger,
});

You can pass child loggers to adapters for prefixed log output, but adapters create their own child loggers by default:

lib/bot.ts
createSlackAdapter({
  logger: logger.child("slack"), // optional — auto-created if omitted
});