Register handlers for mentions, messages, reactions, member joins, and platform-specific events.
Chat SDK uses an event-driven architecture. You register handlers for different event types, and the SDK routes incoming webhooks to the appropriate handler.
When a message arrives, the SDK evaluates handlers in this order:
- Direct messages — if the thread is a DM and any
onDirectMessagehandlers are registered, they fire beforeonSubscribedMessage,onNewMention, and pattern handlers. - Subscribed threads — if the thread is subscribed,
onSubscribedMessagefires and no other message handler runs. DMs only reach this step when noonDirectMessagehandlers are registered. - Mentions — if the bot is @-mentioned in an unsubscribed thread,
onNewMentionfires. Unsubscribed DMs without direct handlers are treated as mentions for backward compatibility. - Pattern matches — if the message text matches any
onNewMessageregex patterns, those handlers fire.
Reactions, slash commands, actions, and modals have their own dedicated routing and are not affected by subscription state.
onNewMention fires when your bot is @-mentioned in a thread it hasn't subscribed to. This is the primary entry point for new conversations.
bot.onNewMention(async (thread, message) => {
await thread.subscribe();
await thread.post("Hello! I'm now listening to this thread.");
});The handler receives a Thread and a Message. Once you call thread.subscribe(), future messages in that thread route to onSubscribedMessage instead.
When to use
- AI assistants — subscribe on first mention, then respond to all follow-up messages in the thread.
- Ticket bots — create a ticket when mentioned, then track the conversation.
- One-shot commands — respond to a mention without subscribing for bots that don't need ongoing context.
Example: AI assistant with context
bot.onNewMention(async (thread, message) => {
await thread.subscribe();
await thread.startTyping();
const response = await generateAIResponse(message.text);
await thread.post(response);
});Example: Triage bot
bot.onNewMention(async (thread, message) => {
const ticket = await createTicket({
title: message.text.slice(0, 100),
reporter: message.author.fullName,
});
await thread.post(`Ticket created: ${ticket.url}`);
});onSubscribedMessage fires for every new message in a non-DM thread your bot has subscribed to. Once subscribed, messages (including @-mentions) route here instead of onNewMention.
If an onDirectMessage handler is registered, DM messages route there before subscription routing. Without a direct handler, subscribed DMs route to onSubscribedMessage.
bot.onSubscribedMessage(async (thread, message) => {
if (message.isMention) {
await thread.post("You mentioned me!");
return;
}
await thread.post(`Got your message: ${message.text}`);
});Messages sent by the bot itself do not trigger this handler. You don't need to filter out your own messages.
When to use
- Conversational AI — maintain a back-and-forth conversation with message history.
- Thread monitoring — watch a thread for updates and react to specific keywords or patterns.
- Collaborative workflows — track all messages in a thread to update external systems.
Example: Conversational AI with history
import { toAiMessages } from "chat/ai";
bot.onSubscribedMessage(async (thread, message) => {
await thread.startTyping();
// Build conversation history from thread messages
const messages = [];
for await (const msg of thread.allMessages) {
messages.push(msg);
}
const history = await toAiMessages(messages);
const response = await generateAIResponse(history);
await thread.post(response);
});See toAiMessages for all options including multi-user name prefixing, message transforms, and attachment handling.
Example: Unsubscribe on keyword
bot.onSubscribedMessage(async (thread, message) => {
if (message.text.toLowerCase().includes("stop")) {
await thread.unsubscribe();
await thread.post("Got it, I'll stop watching this thread.");
return;
}
// Handle other messages...
});Example: Thread state for multi-step flows
interface ThreadState {
step: "awaiting_name" | "awaiting_email" | "done";
}
const bot = new Chat<typeof adapters, ThreadState>({ /* ...config */ });
bot.onNewMention(async (thread) => {
await thread.subscribe();
await thread.setState({ step: "awaiting_name" });
await thread.post("Let's get you set up. What's your name?");
});
bot.onSubscribedMessage(async (thread, message) => {
const state = await thread.state;
switch (state?.step) {
case "awaiting_name":
await thread.setState({ step: "awaiting_email" });
await thread.post(`Thanks, ${message.text}! What's your email?`);
break;
case "awaiting_email":
await thread.setState({ step: "done" });
await thread.post("All set! Your account has been created.");
await thread.unsubscribe();
break;
}
});onNewMessage fires for messages matching a regex pattern in threads the bot is not subscribed to. Use it for keyword-triggered responses without requiring an @-mention.
bot.onNewMessage(/^help$/i, async (thread, message) => {
await thread.post("Here's how I can help...");
});The first argument is a RegExp that's tested against the message text. Only messages in unsubscribed threads are evaluated.
When to use
- Keyword triggers — respond to specific words or phrases without requiring a mention.
- Auto-responders — detect common questions and provide instant answers.
- Escalation detection — watch for urgent language and alert the right people.
Example: FAQ auto-responder
bot.onNewMessage(/\b(deploy|deployment|ship)\b/i, async (thread, message) => {
await thread.post(
"Deployments run automatically on push to `main`. " +
"Check status at https://dashboard.example.com/deploys"
);
});Example: Incident detection
bot.onNewMessage(/\b(outage|down|incident|p[01])\b/i, async (thread, message) => {
await thread.subscribe();
await thread.post(
"I've flagged this as a potential incident and I'm monitoring this thread."
);
await notifyOnCallTeam({
channel: thread.channel.id,
reporter: message.author.fullName,
text: message.text,
});
});onReaction fires when users add or remove emoji reactions to messages. You can handle all reactions or filter by specific emoji.
import { emoji } from "chat";
// Handle specific emoji
bot.onReaction(["thumbs_up", "heart"], async (event) => {
if (!event.added) return;
await event.adapter.addReaction(
event.threadId,
event.messageId,
emoji.raised_hands
);
});
// Handle all reactions
bot.onReaction(async (event) => {
console.log(`${event.user.userName} ${event.added ? "added" : "removed"} ${event.emoji}`);
});ReactionEvent
| Property | Type | Description |
|---|---|---|
emoji | EmojiValue | Normalized emoji name for cross-platform comparison |
rawEmoji | string | Platform-specific emoji string |
added | boolean | true if added, false if removed |
user | Author | The user who reacted |
message | Message (optional) | The message that was reacted to |
thread | Thread | Thread for posting replies |
messageId | string | ID of the message that was reacted to |
threadId | string | Thread ID |
adapter | Adapter | The platform adapter |
raw | unknown | Platform-specific event payload |
When to use
- Approval workflows — use thumbs up/down as lightweight approve/reject signals.
- Bookmarking — save messages to an external system when a specific emoji is added.
- Polls and voting — count reactions as votes.
Example: Approval workflow
bot.onReaction(["thumbs_up", "thumbs_down"], async (event) => {
if (!event.added) return;
const approved = event.emoji === "thumbs_up";
const status = approved ? "approved" : "rejected";
await event.thread.post(
`Request ${status} by ${event.user.fullName}`
);
await updateRequestStatus(event.messageId, status, event.user.userId);
});Example: Save to external system
bot.onReaction(["bookmark"], async (event) => {
if (!event.added || !event.message) return;
await saveToDatabase({
text: event.message.text,
savedBy: event.user.userId,
source: event.threadId,
});
await event.thread.post(
`Bookmarked by ${event.user.fullName}`
);
});For button clicks, slash commands, and modal forms, see the dedicated guides:
- Slash Commands — handle
/commandinvocations from the message composer. - Actions — handle button clicks and interactive card events.
- Modals — collect structured input through modal dialogs with validation.
These handlers are specific to the Slack platform and require the Slack adapter.
Handling assistant threads
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.
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?" },
{ title: "Summarize", message: "Summarize the current channel" },
]);
});The event object includes:
| Property | Type | Description |
|---|---|---|
channelId | string | The assistant thread's channel |
threadTs | string | Thread timestamp |
threadId | string | Thread ID |
userId | string | User who started the thread |
context | object | Assistant context (channelId, teamId, enterpriseId, threadEntryPoint) |
adapter | Adapter | The Slack adapter |
Handling assistant context changes
onAssistantContextChanged fires when the assistant context changes, for example when a user navigates to a different channel while the assistant thread is open.
bot.onAssistantContextChanged(async (event) => {
const slack = bot.getAdapter("slack") as SlackAdapter;
await slack.setStatus(event.channelId, event.threadTs, "Updating context...");
// Update prompts based on new context
const channelName = event.context.channelId ?? "general";
await slack.setSuggestedPrompts(event.channelId, event.threadTs, [
{ title: "Summarize", message: `Summarize #${channelName}` },
]);
});Handling App Home opens
onAppHomeOpened fires when a user opens your bot's Home tab in Slack. Use it to publish a dynamic view.
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, <@${event.userId}>!` },
},
{
type: "actions",
elements: [
{
type: "button",
text: { type: "plain_text", text: "Open Dashboard" },
url: "https://dashboard.example.com",
},
],
},
],
});
});The event object includes:
| Property | Type | Description |
|---|---|---|
userId | string | User who opened the Home tab |
channelId | string | Channel context |
adapter | Adapter | The Slack adapter |
Handling member joined channel
onMemberJoinedChannel fires when a user joins a Slack channel. Use it to post welcome messages or onboard users automatically.
bot.onMemberJoinedChannel(async (event) => {
// Only post when the bot itself joins
if (event.userId !== event.adapter.botUserId) {
return;
}
await event.adapter.postMessage(
event.channelId,
"Hello! I'm now available in this channel. Mention me to get started."
);
});The event object includes:
| Property | Type | Description |
|---|---|---|
adapter | Adapter | The Slack adapter |
channelId | string | The channel that was joined |
userId | string | The user who joined |
inviterId | string (optional) | The user who invited them |











