Skip to main content
The Chat SDK provides a unified API for posting messages across Slack, Teams, Google Chat, and Discord.

Basic Message Posting

Simple Text

chat.onNewMention(async (thread, message) => {
  // Simple string
  await thread.post("Hello world!");
  
  // With emoji
  await thread.post(`${emoji.thumbs_up} Great job!`);
});

Markdown

Use the markdown format for cross-platform formatting:
await thread.post({
  markdown: `
**Bold** and _italic_ text

- List item 1
- List item 2

[Link](https://example.com)
  `.trim()
});
Markdown is automatically converted to platform-specific formats (Slack mrkdwn, Teams/Discord markdown, Google Chat HTML).

Raw Platform Text

Skip markdown conversion and send platform-specific text:
// Slack mrkdwn format
await thread.post({
  raw: "*Bold* and _italic_ in Slack format"
});

AST (Advanced)

Use mdast AST for programmatic message construction:
import { root, paragraph, text, strong, link } from "chat";

await thread.post({
  ast: root([
    paragraph([
      strong([text("Bold text")]),
      text(" and "),
      link("https://example.com", [text("a link")])
    ])
  ])
});

Interactive Cards

Create rich interactive cards with buttons, inputs, and structured layouts:
import { Card, Text, Button, Divider } from "chat";

await thread.post(
  <Card title="Order Approval">
    <Text>Order #12345 requires approval</Text>
    <Divider />
    <Button id="approve" value="12345" style="primary">Approve</Button>
    <Button id="reject" value="12345" style="danger">Reject</Button>
  </Card>
);

// Handle button clicks
chat.onAction(["approve", "reject"], async (event) => {
  const orderId = event.value;
  if (event.actionId === "approve") {
    await event.thread.post(`✅ Order ${orderId} approved`);
  } else {
    await event.thread.post(`❌ Order ${orderId} rejected`);
  }
});
Cards are automatically converted to platform-specific formats: Slack Block Kit, Teams Adaptive Cards, and Google Chat Cards.

File Attachments

Uploading Files

import { readFile } from "node:fs/promises";

const fileData = await readFile("./report.pdf");

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

Multiple Files

await thread.post({
  markdown: "Quarterly reports:",
  files: [
    { filename: "q1.pdf", data: q1Data, mimeType: "application/pdf" },
    { filename: "q2.pdf", data: q2Data, mimeType: "application/pdf" },
    { filename: "summary.xlsx", data: xlsxData, mimeType: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" }
  ]
});

Image Attachments

// Link to existing images
await thread.post({
  markdown: "Check out this chart:",
  attachments: [
    {
      type: "image",
      url: "https://example.com/chart.png",
      name: "Sales Chart"
    }
  ]
});

Ephemeral Messages

Send messages visible only to a specific user:
chat.onSlashCommand("/secret", async (event) => {
  // Try native ephemeral (Slack/Google Chat)
  const result = await event.channel.postEphemeral(
    event.user,
    "This message is only visible to you!",
    { fallbackToDM: false }
  );
  
  if (!result) {
    // Platform doesn't support ephemeral - tell user
    await event.channel.post("Sorry, this platform doesn't support private messages in channels.");
  }
});

Fallback to DM

On platforms without ephemeral support (Teams, Discord), automatically fall back to DM:
// Always send (DM fallback on Teams/Discord)
const result = await thread.postEphemeral(
  user,
  "Secret information!",
  { fallbackToDM: true }
);

if (result.usedFallback) {
  console.log("Sent via DM instead of ephemeral");
}

Slack

Native ephemeral (session-dependent, disappears on reload)

Google Chat

Native private message (persists, only target user sees it)

Discord

No native support - requires fallbackToDM: true

Teams

No native support - requires fallbackToDM: true

Sent Message Operations

All posting methods return a SentMessage with edit/delete capabilities:

Editing Messages

const msg = await thread.post("Initial message");

// Edit with new text
await msg.edit("Updated message!");

// Edit with markdown
await msg.edit({
  markdown: "**Updated** with _formatting_"
});

// Edit with a card
await msg.edit(
  <Card title="Updated">
    <Text>This replaces the original message</Text>
  </Card>
);

Deleting Messages

const msg = await thread.post("Temporary message");

// Wait 5 seconds then delete
setTimeout(async () => {
  await msg.delete();
}, 5000);

Adding Reactions

import { emoji } from "chat";

const msg = await thread.post("Check this out!");

// Add reactions
await msg.addReaction(emoji.thumbs_up);
await msg.addReaction(emoji.eyes);

// Remove a reaction
await msg.removeReaction(emoji.thumbs_up);

Typing Indicators

Show that the bot is processing:
chat.onSubscribedMessage(async (thread, message) => {
  // Show typing indicator
  await thread.startTyping("Thinking...");
  
  // Do some work
  const response = await generateAIResponse(message.text);
  
  // Post response (typing indicator auto-clears)
  await thread.post(response);
});
Some platforms support persistent typing indicators with status text (Google Chat), others just send once (Slack).

Mentioning Users

Create platform-specific user mentions:
chat.onSubscribedMessage(async (thread, message) => {
  const mention = thread.mentionUser(message.author.userId);
  await thread.post(`Hey ${mention}, check this out!`);
});
// Output: <@U123456>
const mention = thread.mentionUser("U123456");

Posting to Channels vs Threads

Thread-Level Posting

chat.onNewMention(async (thread, message) => {
  // Posts to the thread (replies to the conversation)
  await thread.post("This is a reply in the thread");
});

Channel-Level Posting

chat.onSlashCommand("/announce", async (event) => {
  // Posts to the channel (top-level message)
  await event.channel.post("This is a channel announcement");
});

// Or access channel from thread
chat.onNewMention(async (thread, message) => {
  await thread.channel.post("Posted to the channel, not the thread");
});
On platforms without native threading, thread and channel posting are the same. Always test cross-platform behavior.

Opening Direct Messages

Start a DM conversation with a user:
chat.onNewMention(async (thread, message) => {
  // Open DM with the user
  const dmThread = await chat.openDM(message.author.userId);
  await dmThread.post("Hey! I sent you a private message.");
  
  // Also reply in the original thread
  await thread.post("I sent you a DM!");
});

// Or use Author object
chat.onSubscribedMessage(async (thread, message) => {
  const dmThread = await chat.openDM(message.author);
  await dmThread.post("Private reply");
});

Message Content Extraction

When you post a message, the SDK extracts content for the SentMessage object:
// From thread.ts
function extractMessageContent(message: AdapterPostableMessage): {
  plainText: string;
  formatted: Root;
  attachments: Attachment[];
} {
  if (typeof message === "string") {
    return {
      plainText: message,
      formatted: root([paragraph([textNode(message)])]),
      attachments: [],
    };
  }

  if ("markdown" in message) {
    const ast = parseMarkdown(message.markdown);
    return {
      plainText: toPlainText(ast),
      formatted: ast,
      attachments: message.attachments || [],
    };
  }

  if ("card" in message) {
    const fallbackText = message.fallbackText || cardToFallbackText(message.card);
    return {
      plainText: fallbackText,
      formatted: root([paragraph([textNode(fallbackText)])]),
      attachments: [],
    };
  }
  
  // ... other formats
}
The SDK automatically converts all message formats to both plain text and mdast AST for consistency.