chat
Version:
Unified chat abstraction for Slack, Teams, Google Chat, and Discord
297 lines (225 loc) • 8.16 kB
text/mdx
---
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 can also create thread handles directly using `chat.thread()` 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, streams, and `PostableObject` instances (`Plan`, `StreamingPlan`).
```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);
// Plan (mutable task list)
const plan = new Plan({ initialMessage: "Working..." });
await thread.post(plan);
await plan.addTask({ title: "Step 1" });
// Streaming with platform options
await thread.post(new StreamingPlan(stream, { groupTasks: "plan" }));
```
**Parameters:** `message: string | PostableMessage | CardJSXElement`
**Returns:** `Promise<SentMessage | PostableObject>` — for plain messages and streams, a `SentMessage` with `edit()`, `delete()`, `addReaction()`, and `removeReaction()` methods; for `Plan` / `StreamingPlan` inputs, the same object is returned so you can keep mutating it.
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>
## getParticipants
Get the unique human participants in a thread. Returns deduplicated authors, excluding all bots. Useful for subscribing only to 1:1 conversations and unsubscribing when others join.
```typescript
const participants = await thread.getParticipants();
// Subscribe only when one person is talking to the bot
if (participants.length === 1) {
await thread.subscribe();
}
// Unsubscribe when the thread becomes a group conversation
if (participants.length > 1) {
await thread.unsubscribe();
}
```
<Callout type="warn">
Each call fetches the full message history to find all participants. On threads with long history this makes multiple API calls to the platform. Consider checking `message.author` against a known set before calling `getParticipants()` on every incoming message.
</Callout>
## subscribe / unsubscribe
Manage thread subscriptions. Subscribed non-DM threads route all messages to `onSubscribedMessage` handlers. DM threads route to `onDirectMessage` first when a direct message handler is registered.
```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>',
},
}}
/>