---
title: Posting Messages
description: Different ways to render and send messages with thread.post().
type: guide
prerequisites:
  - /docs/usage
related:
  - /docs/cards
  - /docs/streaming
  - /docs/files
  - /docs/api/postable-message
  - /docs/threads-messages-channels
---

# Posting Messages



`thread.post()` accepts several message formats: plain strings, markdown, an mdast AST, interactive cards, and streams. [Choosing a format](#choosing-a-format) compares them.

## Plain text

Pass a string to send it to the platform as-is, without any formatting conversion.

```typescript title="lib/bot.ts" lineNumbers
await thread.post("Hello world");
```

## Markdown

Pass a `{ markdown }` object to have the SDK render standard markdown on each platform. Slack receives it in its native `markdown_text` field, and other adapters convert it to their platform's format.

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

The SDK parses the markdown into an mdast AST, and each adapter either handles it natively or converts it to the platform's format.

## Reply to a message

Use `thread.reply()` when the platform should preserve a native reference to a specific message:

```typescript title="lib/bot.ts" lineNumbers
bot.onNewMessage(/help/i, async (thread, message) => {
  await thread.reply(message, {
    markdown: "Thanks, I can help with that.",
  });
});
```

The target can be a `Message` from the same thread or its message ID. Replies accept the same plain text, markdown, AST, card, file, and stream formats as regular messages. Streams are buffered before the reply is sent.

Adapters without native message replies throw `NotImplementedError`. See the [adapter feature matrix](/docs/platform-adapters) for support.

## AST builders

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

```typescript title="lib/bot.ts" lineNumbers
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

| Builder                       | Description                  | Example                                  |
| ----------------------------- | ---------------------------- | ---------------------------------------- |
| `root(children)`              | Root node (required wrapper) | `root([paragraph([...])])`               |
| `paragraph(children)`         | Paragraph block              | `paragraph([text("Hello")])`             |
| `text(value)`                 | Plain text                   | `text("Hello")`                          |
| `strong(children)`            | **Bold** text                | `strong([text("bold")])`                 |
| `emphasis(children)`          | *Italic* text                | `emphasis([text("italic")])`             |
| `strikethrough(children)`     | ~~Strikethrough~~ text       | `strikethrough([text("done")])`          |
| `inlineCode(value)`           | `Inline code`                | `inlineCode("const x = 1")`              |
| `codeBlock(value, lang?)`     | Fenced code block            | `codeBlock("const x = 1", "ts")`         |
| `link(url, children, title?)` | Hyperlink                    | `link("https://...", [text("click")])`   |
| `blockquote(children)`        | Block quote                  | `blockquote([paragraph([text("...")])])` |

### Parsing markdown to AST

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

```typescript title="lib/bot.ts" lineNumbers
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 on Google Chat.

### Function syntax

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

```typescript title="lib/bot.ts" lineNumbers
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:

```json title="tsconfig.json"
{
  "compilerOptions": {
    "jsx": "react-jsx",
    "jsxImportSource": "chat"
  }
}
```

```tsx title="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>
);
```

<Callout type="warn">
  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.
</Callout>

See the [Cards](/docs/cards) page for the full list of card components.

## Streaming

Pass an AI SDK or TanStack AI stream to `thread.post()` to stream a message in real time. The SDK uses platform-native streaming where available and falls back to post-then-edit or buffered delivery depending on the platform.

```typescript title="lib/bot.ts" lineNumbers
import { ToolLoopAgent } from "ai";

const agent = new ToolLoopAgent({ model, instructions: "You are a helpful assistant." });
const result = await agent.stream({ prompt: message.text });
await thread.post(result.fullStream);
```

Both `fullStream` and `textStream` are supported. Use `fullStream` with multi-step agents, because it preserves paragraph breaks between steps. Streams returned by TanStack AI's `chat()` are also auto-detected, with a paragraph break inserted between tool-loop turns. Any `AsyncIterable<string>` also works for custom streams.

For multi-turn conversations, use [`toAiMessages()`](/docs/ai/to-ai-messages) to convert thread history into the `{ role, content }[]` format expected by AI SDKs.

To pass platform-specific streaming options, such as Slack task grouping or stop blocks, wrap the stream in a [`StreamingPlan`](/docs/streaming#streaming-with-options) and post that.

See the [Streaming](/docs/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:

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

Use `attachments` on `{ raw }`, `{ markdown }`, or `{ ast }` when an adapter supports typed media uploads, such as Telegram's image/audio/video/file uploads and media groups.

See the [Files](/docs/files) page for more on attachments.

## Choosing a format

| Format                                                    | Use when                                          | Example                                           |
| --------------------------------------------------------- | ------------------------------------------------- | ------------------------------------------------- |
| Plain string                                              | Simple, unformatted text                          | Status updates, acknowledgements                  |
| `{ markdown }`                                            | You have a markdown string (e.g. from a template) | Notifications with links and formatting           |
| `{ ast }`                                                 | You need programmatic formatting control          | Dynamic messages built from data                  |
| Card (function)                                           | You need buttons, fields, or structured layouts   | Approval flows, dashboards                        |
| Card (JSX)                                                | Same as above, with JSX syntax preference         | Same use cases as function cards                  |
| `AsyncIterable`                                           | Streaming AI responses                            | Chat with LLMs                                    |
| [`Plan`](/docs/streaming#plan-api)                        | Step-by-step tasks that mutate after posting      | Multi-step agents, deploy progress                |
| [`StreamingPlan`](/docs/streaming#streaming-with-options) | Streaming with platform-specific options          | Slack streaming with grouped tasks or stop blocks |

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


---

For a semantic overview of all documentation, see [/sitemap.md](/sitemap.md)

For an index of all available documentation, see [/llms.txt](/llms.txt)

For agent-facing discovery, including API and MCP surfaces, see [/agents.md](/agents.md)