---
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, 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.

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

This sends the string directly without any formatting conversion.

## Markdown

Pass a `{ markdown }` object to have the SDK render standard markdown on each platform — passed through to Slack's native `markdown_text` field, converted to HTML for Teams, and so on.

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

Under the hood, the SDK parses the markdown into an mdast AST, then each adapter 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(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 use cases — 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.

### 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 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 — it preserves paragraph breaks between steps. 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 (e.g. 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 cases, **AST builders** give the best balance of control and simplicity. Reach for **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)