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.
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.
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.
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.
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.
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:
| Property | Type | Description |
|---|---|---|
emoji | EmojiValue | Normalized emoji for 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 |
adapter | Adapter | The 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.
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.
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
// 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.
await thread.subscribe();
await thread.unsubscribe();
const subscribed = await thread.isSubscribed();Typing indicator
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:
// 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:
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:
await thread.setState({ aiMode: false }, { replace: true });Messages
Incoming messages are normalized across platforms into a consistent format:
| Property | Type | Description |
|---|---|---|
id | string | Platform message ID |
threadId | string | Thread ID in adapter:channel:thread format |
text | string | Plain text content |
formatted | Root | mdast AST representation |
raw | unknown | Original platform-specific payload |
author | Author | Message author info |
metadata | MessageMetadata | Timestamps and edit status |
attachments | Attachment[] (optional) | File attachments |
isMention | boolean (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:
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:
// 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:
for await (const thread of channel.threads()) {
console.log(thread.rootMessage.text, thread.replyCount);
}Channel messages
Iterate top-level messages (not thread replies):
for await (const msg of channel.messages) {
console.log(msg.text);
}Post to a channel
Post a top-level message (not inside a thread):
await channel.post("Hello channel!");Channel metadata
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.
// 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:
createSlackAdapter({
logger: logger.child("slack"), // optional — auto-created if omitted
});