The Chat SDK provides a comprehensive event system for responding to messages, reactions, button clicks, slash commands, and more.
Event Handler Types
The SDK distinguishes between different types of events based on subscription state and message patterns:
Message Events
onNewMention Triggered when the bot is @-mentioned in an unsubscribed thread
onSubscribedMessage Triggered for all messages in subscribed threads
onNewMessage Triggered when a message matches a regex pattern
onNewMention is ONLY called for mentions in unsubscribed threads. Once subscribed, all messages (including @-mentions) go to onSubscribedMessage handlers.
Interactive Events
onReaction Triggered when a user adds/removes a reaction emoji
onAction Triggered when a user clicks a button in a card
onSlashCommand Triggered when a user invokes a slash command
onModalSubmit Triggered when a user submits a modal form
Mention Handlers
Basic Mention Handling
chat . onNewMention ( async ( thread , message ) => {
await thread . post ( `Hello ${ message . author . userName } !` );
});
Subscribing to Threads
The typical pattern is to subscribe when first mentioned:
chat . onNewMention ( async ( thread , message ) => {
// Subscribe to follow-up messages
await thread . subscribe ();
await thread . post ( "I'll be watching this thread!" );
});
chat . onSubscribedMessage ( async ( thread , message ) => {
// Handle all messages in subscribed threads
if ( message . isMention ) {
await thread . post ( "You mentioned me again!" );
} else {
await thread . post ( `Got your message: ${ message . text } ` );
}
});
The initial message that triggered subscription does NOT fire onSubscribedMessage. Only subsequent messages trigger subscribed handlers.
Pattern-Based Message Handlers
Match messages using regular expressions:
// Match messages starting with "!help"
chat . onNewMessage ( / ^ !help/ , async ( thread , message ) => {
await thread . post ( "Available commands: !help, !status, !ping" );
});
// Match "hello" (case-insensitive)
chat . onNewMessage ( /hello/ i , async ( thread , message ) => {
await thread . post ( "Hi there!" );
});
// Extract data from patterns
chat . onNewMessage ( / ^ !remind ( . + ) / , async ( thread , message ) => {
const match = message . text . match ( / ^ !remind ( . + ) / );
const reminder = match ?.[ 1 ];
await thread . post ( `I'll remind you: ${ reminder } ` );
});
Reaction Handlers
React to emoji reactions on messages:
import { emoji } from "chat" ;
// Handle specific emoji (recommended)
chat . onReaction ([ emoji . thumbs_up , emoji . heart ], async ( event ) => {
if ( event . added ) {
await event . thread . post ( `Thanks for the ${ event . emoji } !` );
}
});
// Handle all reactions (catch-all)
chat . onReaction ( async ( event ) => {
console . log ( ` ${ event . user . userName } ${ event . added ? "added" : "removed" } ${ event . emoji . name } ` );
});
Reaction Event Structure
interface ReactionEvent {
adapter : Adapter ; // Platform adapter
added : boolean ; // true = added, false = removed
emoji : EmojiValue ; // Normalized emoji object
rawEmoji : string ; // Platform-specific emoji string
message ?: Message ; // The message that was reacted to
messageId : string ; // Message ID
thread : Thread ; // Thread where reaction occurred
threadId : string ; // Thread ID
user : Author ; // User who reacted
raw : unknown ; // Platform-specific event data
}
Use emoji constants for cross-platform emoji support. They work consistently across Slack, Google Chat, Teams, and Discord.
Handle button clicks from interactive cards:
import { Card , Button } from "chat" ;
// Post a card with buttons
chat . onNewMention ( async ( thread , message ) => {
await thread . post (
< Card title = "Choose an option" >
< Button id = "approve" value = "order-123" > Approve </ Button >
< Button id = "reject" value = "order-123" > Reject </ Button >
</ Card >
);
});
// Handle specific action
chat . onAction ( "approve" , async ( event ) => {
await event . thread . post ( `Order ${ event . value } approved by ${ event . user . userName } ` );
});
// Handle multiple actions
chat . onAction ([ "approve" , "reject" ], async ( event ) => {
if ( event . actionId === "approve" ) {
await event . thread . post ( "Approved!" );
} else {
await event . thread . post ( "Rejected!" );
}
});
// Handle all actions (catch-all)
chat . onAction ( async ( event ) => {
console . log ( `Action: ${ event . actionId } , Value: ${ event . value } ` );
});
Action Event Structure
interface ActionEvent {
actionId : string ; // The action ID from the button
adapter : Adapter ; // Platform adapter
messageId : string ; // Message containing the card
thread : Thread ; // Thread where action occurred
threadId : string ; // Thread ID
triggerId ?: string ; // For opening modals (time-limited)
user : Author ; // User who clicked
value ?: string ; // Optional value/payload from button
raw : unknown ; // Platform-specific event data
// Open a modal in response to this action
openModal ( modal : ModalElement | CardJSXElement ) : Promise <{ viewId : string } | undefined >;
}
Slash Command Handlers
Handle slash commands like /help or /status:
// Handle a specific command
chat . onSlashCommand ( "/help" , async ( event ) => {
await event . channel . post ( "Available commands: /help, /status, /ping" );
});
// Handle multiple commands
chat . onSlashCommand ([ "/status" , "/health" ], async ( event ) => {
await event . channel . post ( "All systems operational!" );
});
// Handle with arguments
chat . onSlashCommand ( "/search" , async ( event ) => {
const query = event . text ; // Text after the command
await event . channel . post ( `Searching for: ${ query } ` );
});
// Ephemeral response (only user sees it)
chat . onSlashCommand ( "/secret" , async ( event ) => {
await event . channel . postEphemeral (
event . user ,
"This is just for you!" ,
{ fallbackToDM: false }
);
});
// Catch-all handler
chat . onSlashCommand ( async ( event ) => {
console . log ( `Command: ${ event . command } , Args: ${ event . text } ` );
});
Slash Command Event Structure
interface SlashCommandEvent {
adapter : Adapter ; // Platform adapter
channel : Channel ; // Channel where command was invoked
command : string ; // Command name (e.g., "/help")
text : string ; // Arguments after the command
triggerId ?: string ; // For opening modals (time-limited)
user : Author ; // User who invoked command
raw : unknown ; // Platform-specific event data
// Open a modal in response to this command
openModal ( modal : ModalElement | CardJSXElement ) : Promise <{ viewId : string } | undefined >;
}
Slash commands are invoked at the channel level , so you get a Channel object instead of a Thread.
Modal Handlers
Handle modal form submissions and closures:
import { Modal , TextInput } from "chat" ;
// Open a modal from a slash command
chat . onSlashCommand ( "/feedback" , async ( event ) => {
await event . openModal (
< Modal callbackId = "feedback_modal" title = "Submit Feedback" >
< TextInput id = "feedback" label = "Your feedback" required />
</ Modal >
);
});
// Handle modal submission
chat . onModalSubmit ( "feedback_modal" , async ( event ) => {
const feedback = event . values . feedback ;
// Access the related thread/channel (if modal was opened from a message)
if ( event . relatedThread ) {
await event . relatedThread . post ( `Thanks for the feedback: ${ feedback } ` );
}
// Return validation errors
if ( feedback . length < 10 ) {
return {
action: "errors" ,
errors: { feedback: "Please provide more detail" }
};
}
// Close the modal
return { action: "close" };
});
// Handle modal close (requires notifyOnClose: true)
chat . onModalClose ( "feedback_modal" , async ( event ) => {
console . log ( `User ${ event . user . userName } closed the feedback modal` );
});
Modal Response Types
// Close the modal
return { action: "close" };
// Show validation errors
return {
action: "errors" ,
errors: {
fieldId: "Error message"
}
};
// Update the modal with new content
return {
action: "update" ,
modal: newModalElement
};
// Push a new modal (stack)
return {
action: "push" ,
modal: newModalElement
};
Event Flow and Deduplication
The SDK handles common concerns automatically:
Deduplication
Messages can arrive via multiple paths (e.g., Slack sends both message and app_mention events):
// From chat.ts
const dedupeKey = `dedupe: ${ adapter . name } : ${ message . id } ` ;
const alreadyProcessed = await this . _stateAdapter . get < boolean >( dedupeKey );
if ( alreadyProcessed ) {
this . logger . debug ( "Skipping duplicate message" );
return ;
}
await this . _stateAdapter . set ( dedupeKey , true , this . _dedupeTtlMs );
Default deduplication TTL is 5 minutes . Adjust with dedupeTtlMs in ChatConfig.
Bot Message Filtering
Messages from the bot itself are automatically skipped:
// From chat.ts
if ( message . author . isMe ) {
this . logger . debug ( "Skipping message from self (isMe=true)" );
return ;
}
Thread Locking
Only one instance processes a thread at a time:
// From chat.ts
const lock = await this . _stateAdapter . acquireLock ( threadId , DEFAULT_LOCK_TTL_MS );
if ( ! lock ) {
throw new LockError ( `Could not acquire lock on thread ${ threadId } ` );
}
try {
// Process message...
} finally {
await this . _stateAdapter . releaseLock ( lock );
}
Background Processing
Use waitUntil for fast webhook responses:
import { after } from "next/server" ;
// Next.js App Router
export async function POST ( request : Request ) {
return await chat . webhooks . slack ( request , {
waitUntil : ( p ) => after (() => p )
});
}
import { waitUntil } from "@vercel/functions" ;
// Vercel Functions
export async function POST ( request : Request ) {
return await chat . webhooks . slack ( request , { waitUntil });
}
Without waitUntil, webhook processing blocks the response. Platforms may retry if the response takes too long.
Error Handling
Handlers are wrapped with automatic error catching:
// From chat.ts
processMessage (
adapter : Adapter ,
threadId : string ,
message : Message ,
options ?: WebhookOptions
): void {
const task = ( async () => {
await this . handleIncomingMessage ( adapter , threadId , message );
})(). catch (( err ) => {
this . logger . error ( "Message processing error" , { error: err , threadId });
});
if ( options ?. waitUntil ) {
options . waitUntil ( task );
}
}
Errors in handlers are logged but don’t crash the application. Use structured logging for debugging.