Handling Events
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.
How routing works
When a message arrives, the SDK evaluates handlers in this order:
- Subscribed threads — if the thread is subscribed,
onSubscribedMessagefires and no other message handler runs. - Mentions — if the bot is @-mentioned in an unsubscribed thread,
onNewMentionfires. - 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.
Handling @-mentions
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}`);
});Handling subscribed messages
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.
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
bot.onSubscribedMessage(async (thread, message) => {
await thread.startTyping();
// Build conversation history from thread messages
const history = [];
for await (const msg of thread.allMessages) {
history.push({
role: msg.author.isMe ? "assistant" : "user",
content: msg.text,
});
}
const response = await generateAIResponse(history);
await thread.post(response);
});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;
}
});Handling pattern matches
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,
});
});Handling reactions
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}`
);
});Handling interactions
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.
Handling Slack-specific events
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 |