chat
Version:
Unified chat abstraction for Slack, Teams, Google Chat, and Discord
408 lines (312 loc) • 13.7 kB
text/mdx
---
title: Handling Events
description: Register handlers for mentions, messages, reactions, member joins, and platform-specific events.
type: guide
prerequisites:
- /docs/getting-started
related:
- /docs/slash-commands
- /docs/actions
- /docs/modals
---
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:
1. **Direct messages** — if the thread is a DM and any `onDirectMessage` handlers are registered, they fire before `onSubscribedMessage`, `onNewMention`, and pattern handlers.
2. **Subscribed threads** — if the thread is subscribed, `onSubscribedMessage` fires and no other message handler runs. DMs only reach this step when no `onDirectMessage` handlers are registered.
3. **Mentions** — if the bot is @-mentioned in an unsubscribed thread, `onNewMention` fires. Unsubscribed DMs without direct handlers are treated as mentions for backward compatibility.
4. **Pattern matches** — if the message text matches any `onNewMessage` regex 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.
```typescript title="lib/bot.ts" lineNumbers
bot.onNewMention(async (thread, message) => {
await thread.subscribe();
await thread.post("Hello! I'm now listening to this thread.");
});
```
The handler receives a [`Thread`](/docs/api/thread) and a [`Message`](/docs/api/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
```typescript title="lib/bot.ts" lineNumbers
bot.onNewMention(async (thread, message) => {
await thread.subscribe();
await thread.startTyping();
const response = await generateAIResponse(message.text);
await thread.post(response);
});
```
### Example: Triage bot
```typescript title="lib/bot.ts" lineNumbers
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 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`.
```typescript title="lib/bot.ts" lineNumbers
bot.onSubscribedMessage(async (thread, message) => {
if (message.isMention) {
await thread.post("You mentioned me!");
return;
}
await thread.post(`Got your message: ${message.text}`);
});
```
<Callout type="info">
Messages sent by the bot itself do not trigger this handler. You don't need to filter out your own messages.
</Callout>
### 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
```typescript title="lib/bot.ts" lineNumbers
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`](/docs/ai/to-ai-messages) for all options including multi-user name prefixing, message transforms, and attachment handling.
### Example: Unsubscribe on keyword
```typescript title="lib/bot.ts" lineNumbers
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
```typescript title="lib/bot.ts" lineNumbers
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.
```typescript title="lib/bot.ts" lineNumbers
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
```typescript title="lib/bot.ts" lineNumbers
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
```typescript title="lib/bot.ts" lineNumbers
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.
```typescript title="lib/bot.ts" lineNumbers
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
```typescript title="lib/bot.ts" lineNumbers
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
```typescript title="lib/bot.ts" lineNumbers
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](/docs/slash-commands)** — handle `/command` invocations from the message composer.
- **[Actions](/docs/actions)** — handle button clicks and interactive card events.
- **[Modals](/docs/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](/adapters/official/slack#slack-assistants-api) to set suggested prompts and status indicators.
```typescript title="lib/bot.ts" lineNumbers
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.
```typescript title="lib/bot.ts" lineNumbers
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.
```typescript title="lib/bot.ts" lineNumbers
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.
```typescript title="lib/bot.ts" lineNumbers
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 |