Chat
The Chat class coordinates adapters, state, and event handlers. Create one instance and register handlers for different event types.
Constructor
| Prop | Type | Default | Description |
|---|---|---|---|
userName? | string | Default bot username across all adapters. | |
adapters? | Record<string, Adapter> | Map of adapter name to adapter instance. | |
dedupeTtlMs? | number | 600000 | TTL for message deduplication entries in milliseconds. The default outlives the last Slack event retry, which arrives about 5 minutes after the original delivery. Increase it if your platform redelivers later than that. |
state? | StateAdapter | State adapter for subscriptions, locking, and caching. | |
logger? | Logger | "debug" | "info" | "warn" | "error" | "silent" | ConsoleLogger("info") | Logger instance or log level. |
streamingUpdateIntervalMs? | number | 500 | Throttle interval for fallback streaming (post + edit) in milliseconds. |
fallbackStreamingPlaceholderText? | string | null | "..." | Placeholder text posted when fallback streaming (post + edit) starts. Set to `null` to wait for the first streamed text before posting. |
concurrency? | "drop" | "queue" | "debounce" | "burst" | "concurrent" | ConcurrencyConfig | "drop" | What happens when a message arrives on a thread while a handler is still running. See [Concurrency](/docs/concurrency). |
lockScope? | "thread" | "channel" | ((context: LockScopeContext) => LockScope | Promise<LockScope>) | "thread" | Which messages share a lock: one per thread or one per channel. A function can choose per message. When unset, the adapter's default is used, which is `"channel"` for WhatsApp and Telegram. See [Lock scope](/docs/concurrency#lock-scope). |
history? | HistoryConfig | undefined | Configuration for the History API's user, thread, and channel scopes. `history.user` includes the identity resolver. See `bot.history` below. |
These options are deprecated but still read, so existing bots keep working:
| Option | Replacement |
|---|---|
onLockConflict | concurrency. It only applies under the drop strategy, where 'force' releases the held lock instead of throwing LockError. |
identity | history.user.identity |
transcripts | history.user |
threadHistory | history.thread |
messageHistory | history.thread. It was renamed to threadHistory first, which takes precedence when both are set. |
Event handlers
onNewMention
Fires when the bot is @-mentioned in a thread it has not subscribed to. Most bots start new conversations from this handler.
| Prop | Type | Description |
|---|---|---|
thread? | Thread | The thread where the mention occurred. |
message? | Message | The message that contains the @-mention. |
onDirectMessage
Fires for every direct message when registered. Direct message handlers run before onSubscribedMessage, onNewMention, and pattern handlers. If no direct message handler is registered, unsubscribed DMs fall through to onNewMention for backward compatibility.
| Prop | Type | Description |
|---|---|---|
thread? | Thread | The DM thread where the message occurred. |
message? | Message | The direct message. |
channel? | Channel | The DM channel. |
onSubscribedMessage
Fires for every new message in a subscribed non-DM thread. Once subscribed, messages (including @-mentions) route here instead of onNewMention. DM threads route to onDirectMessage first when a direct message handler is registered.
onNewMessage
Fires for messages in unsubscribed threads that aren't DMs and don't mention the bot. Pass a regex to filter by message text, or pass only a handler to receive every such message, including attachment-only messages with empty text. All onNewMessage handlers run one after another in registration order.
| Prop | Type | Description |
|---|---|---|
pattern? | RegExp | Optional regular expression tested against the message text. Omit it to receive every message that reaches pattern routing. |
handler? | (thread: Thread, message: Message, context?: MessageContext) => Promise<void> | Handler called for each matching message. `context` is set by the `queue` and `burst` concurrency strategies. See [MessageContext](/docs/concurrency#messagecontext). |
The handler-only form doesn't receive DMs or mentions, even when no onDirectMessage or onNewMention handler is registered. Passing arguments in the wrong order, a pattern that isn't a RegExp, or no handler throws a ChatError when the handler is registered.
onReaction
Fires when a user adds or removes an emoji reaction.
| Prop | Type | Description |
|---|---|---|
event.emoji? | EmojiValue | Normalized emoji value. Supports `===` comparison. |
event.rawEmoji? | string | Platform-specific emoji string. |
event.added? | boolean | `true` if added, `false` if removed. |
event.user? | Author | The user who reacted. |
event.thread? | Thread | The thread where the reaction occurred. |
event.message? | Message | undefined | The message that was reacted to, when available. |
event.messageId? | string | The message ID that was reacted to. |
onAction
Fires when a user clicks a button or selects an option in a card.
| Prop | Type | Description |
|---|---|---|
event.actionId? | string | Action ID from the button or select. |
event.value? | string | undefined | Optional payload value from the button. |
event.values? | Record<string, string> | undefined | The card's own input values, keyed by input ID. Absent where the platform has no card inputs. |
event.user? | Author | User who triggered the action. |
event.thread? | Thread | null | The thread containing the card, or null for view-based actions. |
event.triggerId? | string | undefined | Trigger ID for opening modals. Platform-specific and may expire quickly. |
event.openModal? | (modal: ModalElement | ChatElement) => Promise<{ viewId: string } | undefined> | Open a modal form in response to this action. |
onModalSubmit
Fires when a user submits a modal form.
| Prop | Type | Description |
|---|---|---|
event.callbackId? | string | The callback ID specified when the modal was created. |
event.values? | Record<string, string> | Form field values keyed by input ID. |
event.user? | Author | User who submitted the modal. |
event.relatedThread? | Thread | undefined | The thread the modal was opened from, when available. |
event.relatedMessage? | SentMessage | undefined | The message containing the action that opened the modal. |
event.relatedChannel? | Channel | undefined | The channel the modal was opened from. Available when the modal was opened from a slash command. |
event.privateMetadata? | string | undefined | Arbitrary string passed through the modal lifecycle. |
Returns ModalResponse | undefined to control the modal after submission:
| Response | Effect |
|---|---|
{ action: "close" } | Closes the current view and goes back one level in the stack |
{ action: "clear" } | Closes all views and dismisses the modal |
{ action: "errors", errors: { fieldId: "message" } } | Shows validation errors |
{ action: "update", modal: ModalElement } | Replaces the modal content |
{ action: "push", modal: ModalElement } | Pushes a new modal view onto the stack |
onOptionsLoad
Fires when an ExternalSelect requests options dynamically. Slack only. The handler is keyed on the select's id and must return options within Slack's 3-second budget. The adapter caps the loader at about 2.5 seconds and returns an empty result on timeout.
Return an array of OptionsLoadGroup ({ label, options }[]) instead of a flat array to render grouped headers, such as "Recent" and "All". Slack allows at most 100 groups and 100 options per group.
| Prop | Type | Description |
|---|---|---|
event.actionId? | string | The `id` of the select requesting options. Matches the id passed to `bot.onOptionsLoad`. |
event.query? | string | The text the user has typed so far. |
event.user? | Author | The user requesting options. |
event.adapter? | Adapter | The adapter that received this event. |
event.raw? | unknown | Raw platform-specific payload. |
onSlashCommand
Fires when a user invokes a /command in the message composer. Supported on Slack and Discord.
| Prop | Type | Description |
|---|---|---|
event.command? | string | The command name, such as "/status". |
event.text? | string | Arguments after the command. |
event.user? | Author | The user who invoked the command. |
event.channel? | Channel | The channel where the command was invoked. |
event.triggerId? | string | undefined | Trigger ID for opening modals. Expires after a short time. |
event.openModal? | (modal: ModalElement | ChatElement) => Promise<{ viewId: string } | undefined> | Open a modal form in response to this command. |
event.adapter? | Adapter | The platform adapter. |
event.raw? | unknown | Platform-specific raw payload. |
onModalClose
Fires when a user closes a modal. Requires notifyOnClose: true on the modal.
onAssistantThreadStarted
Fires when a user opens a new assistant thread through the Slack Assistants API. Use it to set suggested prompts, show a status indicator, or send an initial greeting.
| Prop | Type | Description |
|---|---|---|
event.threadId? | string | Encoded thread ID. |
event.userId? | string | The user who opened the thread. |
event.channelId? | string | The DM channel ID. |
event.threadTs? | string | Thread timestamp. |
event.context? | AssistantThreadContext | Where the thread was opened: channel, team, enterprise, and entry point. |
event.adapter? | Adapter | The platform adapter. |
onAssistantContextChanged
Fires when a user navigates to a different channel while the Slack Assistants API panel is open. Use it to update suggested prompts or context for the new channel.
The event shape is identical to onAssistantThreadStarted.
onAgentSessionStopped
Fires after a user clicks Slack's native agent-session stop button. Chat SDK
aborts the active turn and moves the session out of processing before this
handler runs.
The event contains threadId, channelId, threadTs,
streamingMessageTs, userId, and adapter. streamingMessageTs lists the
streaming messages Slack stopped and can be empty when no stream was active.
onAgentSessionTitleChanged
Fires when a user renames a Slack agent session.
The event contains title, previousTitle, threadId, channelId,
threadTs, userId, and adapter. previousTitle is omitted when the
session did not have a title before the change.
onAppHomeOpened
Fires when a user opens the bot's Home tab in Slack. Use it to publish a dynamic Home tab view.
| Prop | Type | Description |
|---|---|---|
event.userId? | string | The user who opened the Home tab. |
event.channelId? | string | The channel ID associated with the Home tab. |
event.adapter? | Adapter | The platform adapter. |
onInstalled
Fires when the bot is installed, including when an app upgrade adds the bot to the manifest. Routine upgrades do not emit this event. Only the Teams adapter emits it, for personal, group chat, and team installs.
| Prop | Type | Description |
|---|---|---|
event.action? | "add" | "add-upgrade" | Install action: "add" or "add-upgrade". |
event.channelId? | string | undefined | Normalized destination for the installation, safe to persist for later proactive posts. |
event.conversationId? | string | Platform conversation ID of the installation location. |
event.id? | string | Platform activity ID, useful for idempotency. |
event.userId? | string | undefined | The user who installed the bot. |
event.tenantId? | string | undefined | Tenant the installation belongs to. |
event.adapter? | Adapter | The platform adapter. |
onUninstalled
Fires when the bot is removed, including when an app upgrade removes it from the manifest. Same event shape as onInstalled, with action set to "remove" or "remove-upgrade". Clean up durable records for both actions, and do not post to the conversation after removal.
removeTeamsInstallation and removeSlackInstallation are application-owned.
For Teams team installs, associate records with event.raw.channelData.team.id
as well as conversationId, which can differ between installation and removal.
See the Teams persistence example.
The Slack adapter emits onUninstalled, with action set to "remove", when a
workspace or org uninstalls the app. conversationId is the Slack installation
key: the team_id, or the enterprise_id for org-wide installs. tenantId is
the Enterprise Grid enterprise_id when the event carries one. See
Slack uninstalls.
Utility methods
abortTurn
Abort the active handler turn for a thread. Chat SDK signals work in this process immediately and publishes the cancellation through the configured state adapter for another serverless instance to observe.
Platform adapters normally invoke this for native cancellation events. Model
and tool calls must receive thread.signal to stop their own upstream work.
webhooks
Typed webhook handlers keyed by adapter name. Call them from your HTTP route handler.
getAdapter
Get a typed adapter instance by name.
Direct client access
Each adapter exposes its platform's typed native API client through a getter named after the SDK: .webClient on Slack, .linearClient on Linear, and .octokit on GitHub.
The client uses the credentials from your adapter config. For multi-tenant or multi-workspace adapters (Slack, Linear, GitHub), it returns the client bound to the credentials for the current webhook request context.
The previous .client getter still works on all three adapters as a deprecated alias for .webClient, .linearClient, and .octokit.
Multi-tenant adapters (GitHub App without a fixed installation ID, Linear with per-org OAuth, Slack in multi-workspace mode) require a webhook handler context to resolve credentials when the native client getter is accessed. Calling it outside a handler throws.
For Slack, you can also bind a token explicitly outside a webhook with adapter.withBotToken(token, () => adapter.webClient.…), for example in cron jobs or workflows. The same pattern is required when botToken is configured as an async resolver function, since .webClient resolves the token synchronously.
Single-tenant adapters (PAT, API key, static botToken string, or a synchronous botToken resolver) work anywhere.
| Adapter | Getter | Type |
|---|---|---|
| Slack | .webClient | WebClient from @slack/web-api |
| Linear | .linearClient | LinearClient from @linear/sdk |
| GitHub | .octokit | Octokit from @octokit/rest |
openDM
Open a direct message thread with a user.
getUser
Look up user information by user ID. Returns a UserInfo object with name, email, avatar, and bot status, or null if the user was not found.
bot.getUser picks the adapter from the format of the user ID, so it only works for platforms whose IDs it can recognize: Slack (U... or W...), Microsoft Teams (29:...), Google Chat (users/...), Linear (UUID), and Discord, Telegram, and GitHub (numeric). For other adapters that implement getUser, such as X, Twilio, and Twitch, call the adapter directly with bot.getAdapter("x").getUser(userId).
| Prop | Type | Description |
|---|---|---|
userId? | string | Platform-specific user ID. |
userName? | string | Username or handle. |
fullName? | string | Display name or full name. |
isBot? | boolean | Whether the user is a bot. |
email? | string | undefined | Email address. Requires extra scopes on some platforms. |
avatarUrl? | string | undefined | Profile image URL. |
| Platform | Constraints |
|---|---|
| Slack | Requires the users:read and users:read.email scopes. The email scope must be granted at OAuth install time. |
| Discord | Bot tokens never see email. The email OAuth scope only applies to user-context auth. |
| Telegram | Bots can only look up users who have previously messaged them. |
| Microsoft Teams | Only works for users who previously interacted with the bot, because profiles are cached from webhook activity. avatarUrl is not returned because the Graph API requires a separate photo call. |
| Google Chat | Same caching constraint as Teams: only users seen in prior webhooks. |
| GitHub | email is only returned if the user made it public or you authenticated with the user:email scope. |
| Linear | Returns the full profile, including email and avatar, for any active workspace member. |
| X | Returns the name, username, and avatar. email is not returned. Call through the adapter, because X's numeric IDs aren't inferred. |
| Twitch | Returns the display name, login, and avatar from Get Users. email is not returned. Call through the adapter, because Twitch's numeric IDs aren't inferred. |
| Twilio | Doesn't call Twilio. Returns a profile built from the phone number, with the number as userId, userName, and fullName. |
Fields that aren't available return undefined.
Numeric user IDs from Discord, Telegram, and GitHub are ambiguous when more than one of those adapters is registered. In that case bot.getUser throws a ChatError with code AMBIGUOUS_USER_ID, even when you pass an Author, because the adapter is inferred from the ID alone. Call the adapter directly instead, for example thread.adapter.getUser?.(message.author.userId) inside a handler or bot.getAdapter("telegram").getUser(userId) elsewhere.
bot.getUser throws a ChatError in three cases. Handle them if your bot runs on multiple platforms.
| Code | When |
|---|---|
NOT_SUPPORTED | The inferred adapter doesn't implement getUser |
AMBIGUOUS_USER_ID | A numeric user ID could belong to more than one registered adapter (Discord, Telegram, or GitHub) |
UNKNOWN_USER_ID_FORMAT | The userId string doesn't match any registered platform's ID format |
thread
Get a Thread handle by its thread ID. Use it to post to threads outside a webhook, such as from a cron job or an external trigger.
channel
Get a Channel by its channel ID.
initialize / shutdown
Manage the lifecycle manually. Initialization happens automatically on the first webhook. Call initialize() yourself when the bot does work outside a webhook.
During shutdown, the SDK calls the optional disconnect() method on each adapter before disconnecting the state adapter. Adapters use it to clean up platform connections, close WebSockets, or tear down subscriptions. If one adapter's disconnect() fails, the remaining adapters and the state adapter still disconnect.
reviver
Get a JSON.parse reviver that deserializes Thread and Message objects from workflow payloads.
Threads and channels restored by bot.reviver() stay bound to that bot's adapters and state, and restored threads use that bot's streaming defaults, even if another Chat instance is registered later. When using multiple bots, parse each payload with the receiving bot's reviver. This does not isolate state keys if the bots share the same state store.
The binding is not serialized. If a restored object crosses a Workflow step boundary, automatic deserialization uses the registered singleton again. For multiple bots, pass the serialized data and explicitly restore it with the receiving bot's reviver inside the step.
There is also a workflow-safe serialization entrypoint that works without importing the Chat runtime. Use it in Vercel Workflow files so serializer registration does not load Node-only runtime dependencies:
Message, ThreadImpl, and ChannelImpl retain their automatic Workflow serialization when imported from either chat or chat/serialization. The dedicated entrypoint guarantees their serializer module graph does not load the Node-only Chat conversation context.
The standalone reviver resolves adapters lazily, looking them up from the Chat singleton on first access. Call chat.registerSingleton() before using thread methods like post() (typically inside a "use step" function). Once a restored thread or channel resolves its runtime, it retains that Chat instance for its adapter, state, and streaming defaults, even if the singleton changes. Registering the correct bot before first use remains the caller's responsibility; use bot.reviver() when the owner must be explicit.
history
Namespaced access to the History API: cross-platform user history, per-thread reads and cache, and per-channel reads.
See History API reference for full details and the History guide for setup and patterns.
bot.transcripts is a deprecated alias for bot.history.user. Accessing either when user history is not configured throws.
Read more
Creating a Chat Instance
Initialize the Chat class with adapters, state, and configuration options.
Handling Events
Register handlers for mentions, messages, reactions, member joins, and platform-specific events.
Thread
Represents a conversation thread with methods for posting, subscribing, and state management.
Channel
Channel container that holds threads, with methods for listing, posting, and iteration.