Skip to main content

Basic Usage

This guide covers the core concepts and common usage patterns for Chat SDK.

Core Concepts

Chat Instance

The Chat class is the main entry point. It coordinates adapters, handles webhooks, and dispatches events to your handlers.
import { Chat } from "chat";
import { createSlackAdapter } from "@chat-adapter/slack";
import { createRedisState } from "@chat-adapter/state-redis";

const bot = new Chat({
  userName: "mybot",
  adapters: {
    slack: createSlackAdapter(),
    teams: createTeamsAdapter(),
  },
  state: createRedisState(),
});
Configuration options:
  • userName - Bot username (required)
  • adapters - Map of adapter instances (required)
  • state - State adapter for persistence (required)
  • logger - Logger instance or log level ("info", "debug", "silent")
  • streamingUpdateIntervalMs - Update interval for fallback streaming (default: 500ms)
  • fallbackStreamingPlaceholderText - Initial placeholder for streaming (default: "...")
  • dedupeTtlMs - Message deduplication TTL (default: 300000ms / 5 minutes)

Thread

A Thread represents a conversation. It’s the primary interface for posting messages, managing subscriptions, and accessing thread state.
// Post a message
await thread.post("Hello world!");

// Subscribe to future messages
await thread.subscribe();

// Check subscription status  
const subscribed = await thread.isSubscribed();

// Access thread state
const state = await thread.state;
await thread.setState({ aiMode: true });
Key properties:
  • id - Unique thread ID (format: adapter:channel:thread)
  • channelId - Channel/conversation ID
  • isDM - Whether this is a direct message
  • adapter - The adapter instance for this thread
  • state - Custom thread state (typed if you provide a type parameter)

Message

Messages contain text, formatting, metadata, and attachments:
bot.onNewMention(async (thread, message) => {
  console.log(message.text);           // Plain text
  console.log(message.formatted);      // mdast AST
  console.log(message.author.userId);  // User ID
  console.log(message.metadata.dateSent); // Timestamp
  console.log(message.isMention);      // true for @mentions
});
Message properties:
  • text - Plain text content
  • formatted - mdast AST representation
  • raw - Platform-specific raw message
  • author - Author information (userId, userName, fullName, isBot, isMe)
  • metadata - Timestamp, edited status
  • attachments - File/image attachments
  • isMention - Whether the bot was @mentioned

Event Handlers

Mentions

Handle @mentions of your bot in unsubscribed threads:
bot.onNewMention(async (thread, message) => {
  await thread.subscribe();
  await thread.post("Hello! I'll watch this thread.");
});
onNewMention only fires for mentions in unsubscribed threads. After calling thread.subscribe(), all subsequent messages (including mentions) go to onSubscribedMessage handlers.

Subscribed Messages

Handle all messages in threads you’ve subscribed to:
bot.onSubscribedMessage(async (thread, message) => {
  // Check if this specific message is a mention
  if (message.isMention) {
    await thread.post("You mentioned me!");
  } else {
    await thread.post(`Got: ${message.text}`);
  }
});
The initial message that triggered thread.subscribe() does not fire onSubscribedMessage. Only subsequent messages do.

Pattern Matching

Match messages against regex patterns:
bot.onNewMessage(/^help$/i, async (thread, message) => {
  await thread.post("Here's how to use this bot...");
});

bot.onNewMessage(/deploy (\w+)/, async (thread, message) => {
  const [, environment] = message.text.match(/deploy (\w+)/)!;
  await thread.post(`Deploying to ${environment}...`);
});

Reactions

Handle emoji reactions:
import { emoji } from "chat";

// Handle specific emoji
bot.onReaction([emoji.thumbs_up, emoji.rocket], async (event) => {
  if (event.added) {
    await event.thread.post(`Thanks for the ${event.emoji}!`);
  }
});

// Handle all reactions
bot.onReaction(async (event) => {
  console.log(`${event.user.userName} ${event.added ? "added" : "removed"} ${event.rawEmoji}`);
});
Emoji usage:
import { emoji } from "chat";

// In messages (toString() is called automatically)
await thread.post(`Great work ${emoji.thumbs_up}`);

// Add reactions to messages
const msg = await thread.post("Hello!");
await msg.addReaction(emoji.wave);

Button Clicks (Actions)

Handle button clicks in interactive cards:
bot.onAction("approve", async (event) => {
  await event.thread.post(`Order ${event.value} approved by ${event.user.userName}`);
});

bot.onAction(["approve", "reject"], async (event) => {
  if (event.actionId === "approve") {
    // Handle approval
  } else {
    // Handle rejection  
  }
});

Slash Commands

Handle slash commands:
bot.onSlashCommand("/status", async (event) => {
  await event.channel.post("All systems operational!");
});

bot.onSlashCommand("/feedback", async (event) => {
  // Open a modal form
  await event.openModal({
    type: "modal",
    callbackId: "feedback_modal",
    title: "Submit Feedback",
    children: [
      {
        type: "text_input",
        id: "message",
        label: "Your feedback",
        multiline: true,
      },
    ],
  });
});

Posting Messages

Simple Text

await thread.post("Hello world!");

Markdown

await thread.post({
  markdown: "**Bold** and _italic_ text\n\nWith a [link](https://example.com)"
});

With Emoji

import { emoji } from "chat";

await thread.post(`Deployment complete ${emoji.rocket}`);

With Attachments

await thread.post({
  markdown: "Here's the report:",
  files: [
    {
      filename: "report.pdf",
      data: pdfBuffer,
      mimeType: "application/pdf",
    },
  ],
});

Interactive Cards (JSX)

/** @jsxImportSource chat */
import { Card, Section, Button, Actions } from "chat";

await thread.post(
  <Card title="Order Approval">
    <Section>
      Order #{orderId} requires approval
    </Section>
    <Actions>
      <Button id="approve" value={orderId} style="primary">
        Approve
      </Button>
      <Button id="reject" value={orderId} style="danger">
        Reject  
      </Button>
    </Actions>
  </Card>
);

Streaming AI Responses

import { streamText } from "ai";

bot.onSubscribedMessage(async (thread, message) => {
  const result = streamText({
    model: openai("gpt-4"),
    prompt: message.text,
  });
  
  // Stream directly to chat (uses native streaming on Slack)
  await thread.post(result.textStream);
});
Streaming uses native Slack streaming APIs when available. On other platforms, it falls back to post + periodic edits.

Editing and Deleting Messages

// Edit a message
const msg = await thread.post("Processing...");
await msg.edit("Done!");

// Delete a message
await msg.delete();

// Add/remove reactions
await msg.addReaction(emoji.check);
await msg.removeReaction(emoji.check);

Thread State

Store custom data per thread:
interface MyThreadState {
  aiMode?: boolean;
  userName?: string;
}

const bot = new Chat<typeof adapters, MyThreadState>({
  userName: "mybot",
  adapters,
  state,
});

bot.onNewMention(async (thread, message) => {
  // Set state (merges by default)
  await thread.setState({ aiMode: true, userName: message.author.userName });
  
  // Get state (fully typed)
  const state = await thread.state;
  if (state?.aiMode) {
    // AI mode is enabled
  }
  
  // Replace entire state
  await thread.setState({ aiMode: false }, { replace: true });
});
State TTL: Thread state persists for 30 days by default.

Iterating Messages

Recent Messages (Newest First)

for await (const message of thread.messages) {
  console.log(message.text);
  // Automatically paginates backward from most recent
}

All Messages (Oldest First)

for await (const message of thread.allMessages) {
  console.log(message.text);
  // Automatically paginates forward from oldest
}

Manual Pagination

const result = await thread.adapter.fetchMessages(thread.id, {
  limit: 50,
  direction: "backward", // or "forward"
});

console.log(result.messages);

if (result.nextCursor) {
  const next = await thread.adapter.fetchMessages(thread.id, {
    cursor: result.nextCursor,
  });
}

Ephemeral Messages

Send messages visible only to a specific user:
// With native ephemeral (Slack, Google Chat)
await thread.postEphemeral(
  message.author,
  "This is just for you!",
  { fallbackToDM: false }
);

// With DM fallback (Discord, Teams)
const result = await thread.postEphemeral(
  message.author,
  "This is just for you!",
  { fallbackToDM: true }
);

if (result?.usedFallback) {
  console.log("Sent via DM instead");
}
Ephemeral messages on Slack are session-dependent and disappear on reload. On Google Chat they persist. Discord and Teams require fallbackToDM: true.

Typing Indicators

await thread.startTyping();
await thread.startTyping("Thinking..."); // Optional status text
Some platforms show persistent typing indicators, others send a single ping. The optional status parameter is only supported on select platforms.

Direct Messages

Open a DM conversation:
const dmThreadId = await adapter.openDM(userId);
const dmThread = new ThreadImpl({
  id: dmThreadId,
  adapter,
  stateAdapter: state,
  channelId: dmThreadId,
  isDM: true,
});

await dmThread.post("Hello via DM!");

Custom Thread State Types

Define custom state for type safety:
interface MyState {
  conversationMode: "ai" | "human";
  lastPrompt?: string;
}

const bot = new Chat<typeof adapters, MyState>({
  userName: "mybot",
  adapters,
  state,
});

bot.onSubscribedMessage(async (thread, message) => {
  const state = await thread.state; // Type: MyState | null
  
  if (state?.conversationMode === "ai") {
    // Handle AI mode
  }
});

Error Handling

import { ChatError, RateLimitError, LockError } from "chat";

bot.onSubscribedMessage(async (thread, message) => {
  try {
    await thread.post("Hello!");
  } catch (error) {
    if (error instanceof RateLimitError) {
      console.log("Rate limited, retrying...");
    } else if (error instanceof LockError) {
      console.log("Thread is locked by another process");
    } else if (error instanceof ChatError) {
      console.log("Chat SDK error:", error.message);
    }
  }
});

Logging

Configure logging level:
import { ConsoleLogger } from "chat";

const bot = new Chat({
  userName: "mybot",
  adapters,
  state,
  logger: "debug", // "silent" | "error" | "warn" | "info" | "debug"
});

// Or use a custom logger
const bot = new Chat({
  userName: "mybot",
  adapters,
  state,
  logger: new ConsoleLogger("debug"),
});

Next Steps

Event Handlers

Deep dive into all event types

AI Streaming

Stream LLM responses

Interactive Cards

Build rich UI with JSX

Adapters

Platform-specific configuration