Looking for the chatbot template? It's now here.

Posting Messages

Different ways to render and send messages with thread.post().

thread.post() accepts several message formats, each suited to different use cases. Choose the format that best fits your content — from plain strings to structured AST to rich interactive cards.

Plain text

The simplest option. Pass a string and it goes through as-is to the platform.

lib/bot.ts
await thread.post("Hello world");

This sends the string directly without any formatting conversion.

Markdown

Pass a { markdown } object to have the SDK convert standard markdown to each platform's native format — mrkdwn for Slack, HTML for Teams, and so on.

lib/bot.ts
await thread.post({
  markdown: "**Bold**, _italic_, and `code`",
});

Under the hood, the SDK parses the markdown into an mdast AST, then each adapter converts it to the platform's format.

AST builders

For programmatic control over message formatting, use the mdast AST builder functions exported from chat. This is the recommended approach for most use cases — it gives you fine-grained control without the overhead of card rendering.

lib/bot.ts
import { root, paragraph, text, strong, link } from "chat";

await thread.post({
  ast: root([
    paragraph([
      strong([text("Deployment complete")]),
      text(" — "),
      link("https://example.com", [text("View site")]),
    ]),
  ]),
});

Available builders

BuilderDescriptionExample
root(children)Root node (required wrapper)root([paragraph([...])])
paragraph(children)Paragraph blockparagraph([text("Hello")])
text(value)Plain texttext("Hello")
strong(children)Bold textstrong([text("bold")])
emphasis(children)Italic textemphasis([text("italic")])
strikethrough(children)Strikethrough textstrikethrough([text("done")])
inlineCode(value)Inline codeinlineCode("const x = 1")
codeBlock(value, lang?)Fenced code blockcodeBlock("const x = 1", "ts")
link(url, children, title?)Hyperlinklink("https://...", [text("click")])
blockquote(children)Block quoteblockquote([paragraph([text("...")])])

Parsing markdown to AST

You can also parse a markdown string into an AST, manipulate it, then send it:

lib/bot.ts
import { parseMarkdown, stringifyMarkdown } from "chat";

const ast = parseMarkdown("**Hello** world");
// Manipulate the AST...
await thread.post({ ast });

Cards

When you need interactive elements like buttons, dropdowns, or structured layouts, use cards. Cards render natively on each platform — Block Kit on Slack, Adaptive Cards on Teams, and Google Chat Cards.

Function syntax

Use the function-call API for type-safe card construction:

lib/bot.ts
import { Card, Text, Actions, Button } from "chat";

await thread.post(
  Card({
    title: "Order #1234",
    children: [
      Text("Your order has been received!"),
      Actions([
        Button({ id: "approve", label: "Approve", style: "primary" }),
        Button({ id: "reject", label: "Reject", style: "danger" }),
      ]),
    ],
  })
);

JSX syntax

You can also use JSX if you configure the chat JSX runtime:

tsconfig.json
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "chat"
  }
}
lib/bot.tsx
import { Card, CardText, Actions, Button } from "chat";

await thread.post(
  <Card title="Order #1234">
    <CardText>Your order has been received!</CardText>
    <Actions>
      <Button id="approve" style="primary">Approve</Button>
      <Button id="reject" style="danger">Reject</Button>
    </Actions>
  </Card>
);

The JSX syntax requires jsxImportSource: "chat" in your tsconfig.json (or a per-file /** @jsxImportSource chat */ pragma). Without this, TypeScript won't recognize the card JSX types. If you run into type issues with JSX, use the function-call syntax instead — it produces the same output with better type inference.

See the Cards page for the full list of card components.

Streaming

Pass an AsyncIterable<string> (like the AI SDK's textStream) to stream a message in real time. The SDK uses platform-native streaming where available and falls back to post-then-edit on other platforms.

lib/bot.ts
import { streamText } from "ai";

const result = streamText({ model, prompt: message.text });
await thread.post(result.textStream);

See the Streaming page for details on platform behavior and configuration.

Attachments and files

Any structured message format (markdown, ast, or card) supports files for uploading attachments alongside the message:

lib/bot.ts
await thread.post({
  markdown: "Here's the report:",
  files: [{ data: buffer, filename: "report.pdf" }],
});

See the Files page for more on attachments.

Choosing a format

FormatUse whenExample
Plain stringSimple, unformatted textStatus updates, acknowledgements
{ markdown }You have a markdown string (e.g. from a template)Notifications with links and formatting
{ ast }You need programmatic formatting controlDynamic messages built from data
Card (function)You need buttons, fields, or structured layoutsApproval flows, dashboards
Card (JSX)Same as above, with JSX syntax preferenceSame use cases as function cards
AsyncIterableStreaming AI responsesChat with LLMs

For most cases, AST builders give the best balance of control and simplicity. Reach for cards when you need interactive elements like buttons or dropdowns.