Skip to main content
The Chat class is the central orchestrator that manages adapters, event handlers, and webhook processing.

Constructor

new Chat<TAdapters, TState>(config: ChatConfig<TAdapters>)
config
ChatConfig<TAdapters>
required
Configuration object for the Chat instance
chat
Chat<TAdapters, TState>
Initialized Chat instance with type-safe adapter access and webhook handlers

Example

import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { createRedisState } from "@chat-state/redis";

interface MyThreadState {
  aiMode?: boolean;
  userName?: string;
}

const chat = new Chat<typeof adapters, MyThreadState>({
  userName: "mybot",
  adapters: {
    slack: createSlackAdapter({
      botToken: process.env.SLACK_BOT_TOKEN,
      signingSecret: process.env.SLACK_SIGNING_SECRET,
    }),
  },
  state: createRedisState({ url: process.env.REDIS_URL }),
  logger: "info",
});

Properties

webhooks

readonly webhooks: Webhooks<TAdapters>
Type-safe webhook handlers keyed by adapter name. Each webhook handler processes incoming requests from the platform.
// Type: (request: Request, options?: WebhookOptions) => Promise<Response>
export async function POST(request: Request) {
  return chat.webhooks.slack(request, { 
    waitUntil: (p) => after(() => p) 
  });
}

Event Handler Methods

onNewMention()

onNewMention(handler: MentionHandler<TState>): void
Register a handler for new @-mentions of the bot in unsubscribed threads only.
handler
(thread: Thread<TState>, message: Message) => void | Promise<void>
required
Handler called when the bot is @-mentioned in an unsubscribed thread
This handler is ONLY called for mentions in unsubscribed threads. Once a thread is subscribed via thread.subscribe(), subsequent messages (including @-mentions) go to onSubscribedMessage handlers instead.
chat.onNewMention(async (thread, message) => {
  await thread.subscribe();
  await thread.post("Hello! I'll be watching this thread.");
});

onNewMessage()

onNewMessage(pattern: RegExp, handler: MessageHandler<TState>): void
Register a handler for messages matching a regex pattern in unsubscribed threads.
pattern
RegExp
required
Regular expression to match against message text
handler
(thread: Thread<TState>, message: Message) => void | Promise<void>
required
Handler called when pattern matches
// Match messages starting with "!help"
chat.onNewMessage(/^!help/, async (thread, message) => {
  await thread.post("Available commands: !help, !status, !ping");
});

onSubscribedMessage()

onSubscribedMessage(handler: SubscribedMessageHandler<TState>): void
Register a handler for all messages in subscribed threads.
handler
(thread: Thread<TState>, message: Message) => void | Promise<void>
required
Handler called for all messages in subscribed threads
Does NOT fire for:
  • The message that triggered the subscription (e.g., the initial @mention)
  • Messages sent by the bot itself
chat.onSubscribedMessage(async (thread, message) => {
  if (message.isMention) {
    await thread.post("You mentioned me again!");
  }
  await thread.post(`Got your message: ${message.text}`);
});

onReaction()

onReaction(handler: ReactionHandler): void
onReaction(emoji: EmojiFilter[], handler: ReactionHandler): void
Register a handler for reaction events (emoji added/removed).
emoji
Array<EmojiValue | string>
Optional array of emoji to filter. Empty or omitted means all emoji.
handler
(event: ReactionEvent) => void | Promise<void>
required
Handler called when reaction is added/removed
import { emoji } from "chat";

// Handle specific emoji using EmojiValue objects (recommended)
chat.onReaction([emoji.thumbs_up, emoji.heart], async (event) => {
  if (event.emoji === emoji.thumbs_up) {
    await event.thread.post("Thanks for the thumbs up!");
  }
});

// Handle all reactions
chat.onReaction(async (event) => {
  console.log(`${event.added ? "Added" : "Removed"} ${event.emoji.name}`);
});

onAction()

onAction(handler: ActionHandler): void
onAction(actionIds: string[] | string, handler: ActionHandler): void
Register a handler for action events (button clicks in cards).
actionIds
string | string[]
Optional action ID(s) to filter. Empty or omitted means all actions.
handler
(event: ActionEvent) => void | Promise<void>
required
Handler called when button is clicked
// 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!");
  }
});

// Catch-all handler
chat.onAction(async (event) => {
  console.log(`Action: ${event.actionId}`);
});

onSlashCommand()

onSlashCommand(handler: SlashCommandHandler<TState>): void
onSlashCommand(commands: string[] | string, handler: SlashCommandHandler<TState>): void
Register a handler for slash command events.
commands
string | string[]
Optional command(s) to filter (e.g., “/help” or [“help”, “/help”]). Empty or omitted means all commands.
handler
(event: SlashCommandEvent<TState>) => void | Promise<void>
required
Handler called when command is invoked
// Handle specific command
chat.onSlashCommand("/help", async (event) => {
  await event.channel.post("Here are the available commands...");
});

// Open a modal from a slash command
chat.onSlashCommand("/feedback", async (event) => {
  await event.openModal({
    type: "modal",
    callbackId: "feedback_modal",
    title: "Submit Feedback",
    children: [
      { type: "text_input", id: "feedback", label: "Your feedback" }
    ],
  });
});

onModalSubmit()

onModalSubmit(handler: ModalSubmitHandler): void
onModalSubmit(callbackIds: string[] | string, handler: ModalSubmitHandler): void
Register a handler for modal/dialog form submissions.
callbackIds
string | string[]
Optional callback ID(s) to filter. Empty or omitted means all modals.
handler
(event: ModalSubmitEvent) => void | Promise<ModalResponse | undefined>
required
Handler called when modal is submitted. Can return ModalResponse to update/close the modal.
chat.onModalSubmit("feedback_modal", async (event) => {
  const feedback = event.values.feedback;
  // Process feedback...
  
  // Optionally return response to close modal
  return { action: "close" };
});

onModalClose()

onModalClose(handler: ModalCloseHandler): void
onModalClose(callbackIds: string[] | string, handler: ModalCloseHandler): void
Register a handler for modal close/cancel events.
callbackIds
string | string[]
Optional callback ID(s) to filter. Empty or omitted means all modals.
handler
(event: ModalCloseEvent) => void | Promise<void>
required
Handler called when modal is closed/cancelled

Utility Methods

initialize()

async initialize(): Promise<void>
Manually initialize the chat instance and all adapters. This is called automatically when handling webhooks, but can be called manually for non-webhook use cases (e.g., Gateway listeners).
await chat.initialize();

shutdown()

async shutdown(): Promise<void>
Gracefully shut down the chat instance and disconnect from the state backend.
await chat.shutdown();

getAdapter()

getAdapter<K extends keyof TAdapters>(name: K): TAdapters[K]
Get an adapter by name with type safety.
name
K extends keyof TAdapters
required
Adapter name (key from the adapters config)
adapter
TAdapters[K]
The adapter instance
const slackAdapter = chat.getAdapter("slack");

openDM()

async openDM(user: string | Author): Promise<Thread<TState>>
Open a direct message conversation with a user. The adapter is automatically inferred from the userId format.
user
string | Author
required
Platform-specific user ID string, or an Author object from message.author or event.user
thread
Thread<TState>
A Thread that can be used to post messages
User ID Formats:
  • Slack: U... (e.g., “U00FAKEUSER1”)
  • Teams: 29:... (e.g., “29:198PbJuw…”)
  • Google Chat: users/... (e.g., “users/100000000000000000001”)
  • Discord: numeric snowflake (e.g., “1033044521375764530”)
// Using user ID directly
const dmThread = await chat.openDM("U123456");
await dmThread.post("Hello via DM!");

// Using Author object from a message
chat.onSubscribedMessage(async (thread, message) => {
  const dmThread = await chat.openDM(message.author);
  await dmThread.post("Hello via DM!");
});

channel()

channel(channelId: string): Channel<TState>
Get a Channel by its channel ID. The adapter is automatically inferred from the channel ID prefix.
channelId
string
required
Channel ID (e.g., “slack:C123ABC”, “gchat:spaces/ABC123”)
channel
Channel<TState>
A Channel that can be used to list threads, post messages, etc.
const channel = chat.channel("slack:C123ABC");

// Iterate messages newest first
for await (const msg of channel.messages) {
  console.log(msg.text);
}

// Post to channel
await channel.post("Hello channel!");

reviver()

reviver(): (key: string, value: unknown) => unknown
Get a JSON.parse reviver function that automatically deserializes Thread, Channel, and Message objects.
reviver
(key: string, value: unknown) => unknown
Reviver function for JSON.parse
// Parse workflow payload with automatic deserialization
const data = JSON.parse(payload, chat.reviver());

// data.thread is now a ThreadImpl instance
await data.thread.post("Hello from workflow!");

registerSingleton()

registerSingleton(): this
Register this Chat instance as the global singleton. Required for Thread deserialization via @workflow/serde.
this
Chat<TAdapters, TState>
Returns the Chat instance for chaining
const chat = new Chat({ ... }).registerSingleton();

// Now threads can be deserialized without passing chat explicitly
const thread = ThreadImpl.fromJSON(serializedThread);

Static Methods

getSingleton()

static getSingleton(): Chat
Get the registered singleton Chat instance. Throws if no singleton has been registered.
const chat = Chat.getSingleton();

hasSingleton()

static hasSingleton(): boolean
Check if a singleton has been registered.
if (Chat.hasSingleton()) {
  const chat = Chat.getSingleton();
}

Type Parameters

TAdapters
Record<string, Adapter>
default:"Record<string, Adapter>"
Map of adapter names to Adapter instances for type-safe webhook access
TState
object
default:"Record<string, unknown>"
Custom state type stored per-thread and per-channel

See Also