The Thread interface represents a conversation thread where users exchange messages. It provides methods for posting messages, managing subscriptions, and storing per-thread state.
Properties
Unique thread ID. Format: {adapter}:{channel}:{thread}
Examples:
Slack: slack:C123ABC:1234567890.123456
Teams: teams:{base64(conversationId)}:{base64(serviceUrl)}
Google Chat: gchat:spaces/ABC123:{base64(threadName)}
channelId
readonly channelId : string
Channel/conversation ID containing this thread.
adapter
readonly adapter : Adapter
The adapter this thread belongs to.
isDM
Whether this is a direct message conversation.
channel
readonly channel : Channel < TState >
Get the Channel containing this thread. Lazy-created and cached.
const channelName = await thread . channel . fetchMetadata ();
console . log ( channelName . name );
state
readonly state : Promise < TState | null >
Get the current thread state. Returns null if no state has been set.
interface MyState {
aiMode ?: boolean ;
userName ?: string ;
}
const state = await thread . state ; // Type: MyState | null
if ( state ?. aiMode ) {
// AI mode is enabled
}
recentMessages
recentMessages : Message []
Recently fetched messages (cached). Updated by refresh().
messages
readonly messages : AsyncIterable < Message >
Iterate messages newest first (backward from most recent). Auto-paginates lazily.
// Get the 10 most recent messages
const recent : Message [] = [];
for await ( const msg of thread . messages ) {
recent . push ( msg );
if ( recent . length >= 10 ) break ;
}
allMessages
readonly allMessages : AsyncIterable < Message >
Iterate ALL messages in chronological order (oldest first). Automatically handles pagination.
// Process all messages from the beginning
for await ( const message of thread . allMessages ) {
console . log ( message . text );
}
Methods
post()
post (
message : string | PostableMessage | CardJSXElement
): Promise < SentMessage >
Post a message to this thread. Supports text, markdown, cards, and streaming from async iterables.
message
string | PostableMessage | CardJSXElement
required
Message content to post Show PostableMessage types
string - Raw text
{ raw: string } - Explicit raw text
{ markdown: string } - Markdown text
{ ast: Root } - mdast AST
{ card: CardElement } - Rich card
CardElement - Direct card element
AsyncIterable<string> - Streaming text (e.g., from AI SDK)
A SentMessage with methods to edit, delete, or add reactions
Streaming Behavior:
When posting a stream (e.g., from AI SDK), uses platform-native streaming APIs when available (Slack), or falls back to post + edit with throttling.
// Simple string
await thread . post ( "Hello!" );
// Markdown
await thread . post ({ markdown: "**Bold** and _italic_" });
// With emoji
import { emoji } from "chat" ;
await thread . post ( ` ${ emoji . thumbs_up } Great job!` );
// JSX Card (with @jsxImportSource chat)
await thread . post (
< Card title = "Welcome!" >
< Text > Hello world </ Text >
</ Card >
);
// Stream from AI SDK
import { generateText } from "ai" ;
const result = await generateText ({ prompt: message . text });
await thread . post ( result . textStream );
postEphemeral()
postEphemeral (
user : string | Author ,
message : AdapterPostableMessage | CardJSXElement ,
options : PostEphemeralOptions
): Promise < EphemeralMessage | null >
Post an ephemeral message visible only to a specific user.
User ID string or Author object (from message.author or event.user)
message
AdapterPostableMessage | CardJSXElement
required
Message content (string, markdown, card, etc.). Streaming is NOT supported.
options
PostEphemeralOptions
required
If true, falls back to DM when native ephemeral is not supported. If false, returns null when unsupported.
EphemeralMessage with usedFallback: true if DM was used, or null if native ephemeral not supported and fallbackToDM is false
Platform Behavior:
Slack : Native ephemeral (session-dependent, disappears on reload)
Google Chat : Native private message (persists, only target user sees it)
Discord : No native support - requires fallbackToDM: true
Teams : No native support - requires fallbackToDM: true
// Always send (DM fallback on Discord/Teams)
await thread . postEphemeral (
user ,
'Only you can see this!' ,
{ fallbackToDM: true }
);
// Only send if native ephemeral supported (Slack/GChat)
const result = await thread . postEphemeral (
user ,
'Secret!' ,
{ fallbackToDM: false }
);
if ( ! result ) {
// Platform doesn't support native ephemeral
}
setState()
setState (
state : Partial < TState > ,
options ?: { replace? : boolean }
): Promise < void >
Set the thread state. Merges with existing state by default.
State object to set (will be merged with existing state unless replace: true)
If true, replace entire state instead of merging
interface MyState {
aiMode ?: boolean ;
userName ?: string ;
count ?: number ;
}
// Merge with existing state
await thread . setState ({ aiMode: true });
// Replace entire state
await thread . setState ({ userName: "Alice" }, { replace: true });
subscribe()
async subscribe (): Promise < void >
Subscribe to future messages in this thread. Once subscribed, all messages in this thread will trigger onSubscribedMessage handlers.
The initial message that triggered subscription will NOT fire the handler. Only subsequent messages will.
chat . onNewMention ( async ( thread , message ) => {
await thread . subscribe ();
await thread . post ( "I'm now watching this thread!" );
});
unsubscribe()
async unsubscribe (): Promise < void >
Unsubscribe from this thread. Future messages will no longer trigger onSubscribedMessage handlers.
await thread . unsubscribe ();
isSubscribed()
async isSubscribed (): Promise < boolean >
Check if this thread is currently subscribed.
In subscribed message handlers, this is optimized to return true immediately without a state lookup, since we already know we’re in a subscribed context.
if ( await thread . isSubscribed ()) {
console . log ( "Already subscribed" );
} else {
await thread . subscribe ();
}
startTyping()
async startTyping ( status ?: string ): Promise < void >
Show typing indicator in the thread.
Optional status text (e.g., “Typing…”, “Searching documents…”) shown where supported
Some platforms support persistent typing indicators, others just send once. The optional status parameter is shown where supported.
await thread . startTyping ( "Thinking..." );
// Perform long-running operation
await thread . post ( "Here's the answer!" );
refresh()
async refresh (): Promise < void >
Refresh recentMessages from the API. Fetches the latest 50 messages and updates the cache.
await thread . refresh ();
const latest = thread . recentMessages [ thread . recentMessages . length - 1 ];
mentionUser()
mentionUser ( userId : string ): string
Get a platform-specific mention string for a user. Use this to @-mention a user in a message.
Platform-specific user ID
Formatted mention string (e.g., <@U123>)
const mention = thread . mentionUser ( message . author . userId );
await thread . post ( `Hey ${ mention } , check this out!` );
createSentMessageFromMessage()
createSentMessageFromMessage (
message : Message
): SentMessage
Wrap a Message object as a SentMessage with edit/delete capabilities. Used internally for reconstructing messages from serialized data.
SentMessage with edit/delete/reaction methods
Type Parameters
TState
object
default: "Record<string, unknown>"
Custom state type stored per-thread
Platform-specific raw message type
Usage Examples
Basic Message Posting
chat . onNewMention ( async ( thread , message ) => {
await thread . post ( "Hello! How can I help?" );
});
Streaming from AI
import { generateText } from "ai" ;
import { openai } from "@ai-sdk/openai" ;
chat . onSubscribedMessage ( async ( thread , message ) => {
const result = await generateText ({
model: openai ( "gpt-4" ),
prompt: message . text ,
});
// Stream the response
await thread . post ( result . textStream );
});
Thread State Management
interface ConversationState {
mode : "help" | "chat" | "search" ;
history : string [];
}
chat . onNewMention ( async ( thread , message ) => {
await thread . setState ({
mode: "chat" ,
history: [ message . text ]
});
await thread . subscribe ();
});
chat . onSubscribedMessage ( async ( thread , message ) => {
const state = await thread . state ;
const history = state ?. history || [];
// Update history
await thread . setState ({
history: [ ... history , message . text ]
});
});
Message Iteration
// Get recent messages for context
const recentMessages : string [] = [];
for await ( const msg of thread . messages ) {
recentMessages . push ( msg . text );
if ( recentMessages . length >= 5 ) break ;
}
const context = recentMessages . reverse (). join ( " \n " );
See Also