---
title: Thread
description: Represents a conversation thread with methods for posting, subscribing, and state management.
type: reference
---

A `Thread` is provided to your event handlers and represents a conversation thread on any platform. You don't create threads directly — they come from handler callbacks or `chat.openDM()`.

## Properties

<TypeTable
  type={{
    id: {
      description: 'Full thread ID in adapter:channel:thread format.',
      type: 'string',
    },
    channelId: {
      description: 'Channel/conversation ID.',
      type: 'string',
    },
    adapter: {
      description: 'The platform adapter this thread belongs to.',
      type: 'Adapter',
    },
    isDM: {
      description: 'Whether this is a direct message conversation.',
      type: 'boolean',
    },
    channel: {
      description: 'The Channel containing this thread.',
      type: 'Channel',
    },
    recentMessages: {
      description: 'Cached messages from the webhook payload.',
      type: 'Message[]',
    },
  }}
/>

## post

Post a message to the thread. Accepts strings, structured messages, cards, and streams.

```typescript
// Plain text
await thread.post("Hello!");

// Markdown
await thread.post({ markdown: "**Bold** text" });

// AST
await thread.post({ ast: root([paragraph([text("Hello")])]) });

// Card
await thread.post(Card({ title: "Hi", children: [Text("Hello")] }));

// Stream (fullStream recommended for multi-step agents)
await thread.post(result.fullStream);
```

**Parameters:** `message: string | PostableMessage | CardJSXElement`

**Returns:** `Promise<SentMessage>` — the sent message with `edit()`, `delete()`, `addReaction()`, and `removeReaction()` methods.

See [Posting Messages](/docs/posting-messages) for details on each format.

## postEphemeral

Post a message visible only to a specific user.

```typescript
await thread.postEphemeral(userId, "Only you can see this", {
  fallbackToDM: true,
});
```

<TypeTable
  type={{
    user: {
      description: 'User ID string or Author object.',
      type: 'string | Author',
    },
    message: {
      description: 'Message content (streaming not supported).',
      type: 'AdapterPostableMessage | CardJSXElement',
    },
    'options.fallbackToDM': {
      description: 'If true, falls back to DM when native ephemeral is not supported. If false, returns null.',
      type: 'boolean',
    },
  }}
/>

**Returns:** `Promise<EphemeralMessage | null>`

## schedule

Schedule a message for future delivery. Currently only supported by the Slack adapter — other adapters throw `NotImplementedError`.

```typescript
const scheduled = await thread.schedule("Reminder: standup in 5 minutes!", {
  postAt: new Date("2026-03-09T09:00:00Z"),
});

// Cancel before it's sent
await scheduled.cancel();
```

**Parameters:** `message: string | PostableMessage | CardJSXElement`, `options: { postAt: Date }`

**Returns:** `Promise<ScheduledMessage>`

<Callout type="warn">
Streaming and file uploads are not supported in scheduled messages.
</Callout>

## subscribe / unsubscribe

Manage thread subscriptions. Subscribed threads route all messages to `onSubscribedMessage` handlers.

```typescript
await thread.subscribe();
await thread.unsubscribe();
const subscribed = await thread.isSubscribed();
```

Subscriptions persist across restarts via your state adapter.

## state

Store typed, per-thread state that persists across requests. State has a 30-day TTL.

```typescript
// Read state
const state = await thread.state; // TState | null

// Merge into existing state
await thread.setState({ aiMode: true });

// Replace state entirely
await thread.setState({ aiMode: false }, { replace: true });
```

## startTyping

Show a typing indicator in the thread. No-op on platforms that don't support it. On Slack, you can pass an optional `status` string to show a custom loading message (requires `assistant:write` scope).

```typescript
await thread.startTyping();

// With custom status (Slack only)
await thread.startTyping("Searching documents...");
```

## messages / allMessages

Iterate through message history.

```typescript
// Newest first (auto-paginates)
for await (const msg of thread.messages) {
  console.log(msg.text);
}

// Oldest first (auto-paginates)
for await (const msg of thread.allMessages) {
  console.log(msg.text);
}
```

## refresh

Re-fetch messages from the API and update `recentMessages`.

```typescript
await thread.refresh();
```

## mentionUser

Get a platform-specific @-mention string for a user.

```typescript
await thread.post(`Hey ${thread.mentionUser(userId)}, check this out!`);
```

## Serialization

Threads can be serialized for workflow engines and external systems. The serialized thread includes the current message if one is available.

```typescript
// Serialize
const json = thread.toJSON();

// Pass to a workflow
await workflow.start("my-workflow", {
  thread: thread.toJSON(),
});
```

The serialized format includes the thread ID, channel ID, adapter name, DM status, and the current message (if present).

### Deserialization

Use `bot.reviver()` as a `JSON.parse` reviver to automatically restore `Thread` and `Message` objects from serialized payloads:

```typescript
const data = JSON.parse(payload, bot.reviver());
await data.thread.post("Hello from workflow!");
```

Under the hood, the reviver calls `ThreadImpl.fromJSON()` and `Message.fromJSON()` for any serialized objects it encounters.

## ScheduledMessage

Returned by `thread.schedule()` and `channel.schedule()`.

<TypeTable
  type={{
    scheduledMessageId: {
      description: 'Platform-specific scheduled message ID.',
      type: 'string',
    },
    channelId: {
      description: 'Channel ID where the message will be posted.',
      type: 'string',
    },
    postAt: {
      description: 'When the message will be sent.',
      type: 'Date',
    },
    raw: {
      description: 'Platform-specific raw response.',
      type: 'unknown',
    },
    'cancel()': {
      description: 'Cancel the scheduled message before it is sent.',
      type: '() => Promise<void>',
    },
  }}
/>

## SentMessage

Returned by `thread.post()`. Extends `Message` with mutation methods.

<TypeTable
  type={{
    'edit(newContent)': {
      description: 'Edit this message.',
      type: '(content: string | PostableMessage | CardJSXElement) => Promise<SentMessage>',
    },
    'delete()': {
      description: 'Delete this message.',
      type: '() => Promise<void>',
    },
    'addReaction(emoji)': {
      description: 'Add a reaction to this message.',
      type: '(emoji: EmojiValue | string) => Promise<void>',
    },
    'removeReaction(emoji)': {
      description: 'Remove a reaction from this message.',
      type: '(emoji: EmojiValue | string) => Promise<void>',
    },
  }}
/>
