thread


Every handler (onMessage, onReaction, a tool, a JSX onClick) gets a thread. It is the conversation: post to it, run the agent, read history, persist state.

channel.onMessage(async ({ thread, message }) => {
  await thread.post("hi");
  await thread.runAgent({ prompt: message.text });
});

Capability-gated methods

Not every platform supports every method (reactions, ephemeral messages, titles, reading history). Those methods return { ok: false } or undefined on a surface that does not support them, rather than throwing. Check the result when it matters.

Properties#

PropertyTypeDescription
platformstringThe platform this thread is on: "slack", "teams", and so on.
conversationKeystringStable key identifying this conversation.
supportsBlockingChoiceboolean?true/undefined: awaitChoice can block for a click. false: use post-and-resume (the managed path).
if (thread.platform === "teams") {
  // teams-specific formatting
}
if (thread.supportsBlockingChoice === false) {
  // managed surface: post and resume instead of awaitChoice
}

Reply#

MethodSignatureDescription
postpost(ui: Renderable): Promise<MessageRef>Send a message. ui is a string or a JSX component. Returns a ref you can update.
updateupdate(ref: MessageRef, ui: Renderable): Promise<MessageRef>Replace a message you posted, in place.
deletedelete(ref: MessageRef): Promise<void>Remove a message you posted.
streamstream(src: string | AsyncIterable<string>): Promise<MessageRef>Post a message that fills in as the source yields.
postEphemeralpostEphemeral(user, ui, { fallbackToDM }): Promise<EphemeralResult | null>Post a message only user sees. fallbackToDM: true DMs the user where ephemeral is unsupported; false resolves null.
const ref = await thread.post("working on it...");
await thread.update(ref, "done.");
await thread.delete(ref);

// stream text in as it is produced
await thread.stream(tokenStream);

// a reply only the caller sees
await thread.postEphemeral(message.user, "psst, only you see this", {
  fallbackToDM: true,
});

// escape hatch: a raw, platform-native payload (e.g. Slack Block Kit)
await thread.post({ raw: [{ type: "divider" }] });

post and update accept a raw payload as an escape hatch when a component does not cover what a platform can do. See the UI library.

Run the agent#

MethodSignatureDescription
runAgentrunAgent(input?): Promise<MessageRef | undefined>Run the agent and stream its reply into the thread.
resumeresume(value: unknown): Promise<MessageRef | undefined>Resume a run waiting on a value (HITL).

runAgent input:

FieldTypeDescription
promptstring | AgentContentPart[]A user message to inject. Use the array form for files and images.
contextContextEntry[]Extra context for this run.
toolsChannelTool[]Extra tools for this run.
transcriptboolean | { limit?: number }Bridge cross-platform history for this run. See Transcripts.
// inject a prompt plus per-turn context
await thread.runAgent({
  prompt: message.text,
  context: [{ description: "Requesting user", value: message.user.name ?? "" }],
});

// resume a run that paused for a human answer
await thread.resume({ approved: true });

Files#

MethodSignatureDescription
postFilepostFile({ bytes, filename, title?, altText? }): Promise<{ ok, fileId?, error? }>Send a file. bytes is a Uint8Array.
await thread.postFile({ bytes: png, filename: "chart.png", title: "Revenue" });

Human-in-the-loop#

MethodSignatureDescription
awaitChoiceawaitChoice<T>(ui: Renderable): Promise<T>Post a picker and block until a click resolves to the button's value. Only on surfaces where supportsBlockingChoice is not false.
const { confirmed } = await thread.awaitChoice<{ confirmed: boolean }>(
  <ConfirmCard />,
);
if (confirmed) await doTheWrite();

See human-in-the-loop for the managed post-and-resume pattern.

Reactions#

MethodSignatureDescription
reactreact(ref: MessageRef, emoji: EmojiValue): Promise<{ ok, error? }>Add an emoji reaction to a message.
unreactunreact(ref: MessageRef, emoji: EmojiValue): Promise<{ ok, error? }>Remove the bot's reaction.
await thread.react(message.ref, "eyes"); // 👀 acknowledge
// ...work...
await thread.unreact(message.ref, "eyes");
await thread.react(message.ref, "white_check_mark"); // ✅ done

Read the conversation#

MethodSignatureDescription
getMessagesgetMessages(): Promise<ThreadMessage[]>The conversation's messages. [] when the adapter can't read history.
lookupUserlookupUser(query: string): Promise<PlatformUser | undefined>Resolve a user by free-form query. undefined when unsupported.
const history = await thread.getMessages();
const user = await thread.lookupUser("alex@acme.com");

Per-thread state#

MethodSignatureDescription
setStatesetState<T>(v: T): Promise<void>Persist arbitrary per-thread state (e.g. a workflow step). Typed and validated when store.state is set.
statestate<T>(): Promise<T | undefined>Read back what setState wrote.
await thread.setState({ step: "awaiting_approval" });
const { step } = (await thread.state<{ step: string }>()) ?? {};

Conversation surface#

MethodSignatureDescription
setSuggestedPromptssetSuggestedPrompts(prompts, opts?): Promise<{ ok, error? }>Pin suggested prompts (e.g. the Slack assistant pane).
setTitlesetTitle(title: string): Promise<{ ok, error? }>Name the conversation.
subscribe / unsubscribe(): Promise<void>Record or drop a subscription for this conversation.
isSubscribed(): Promise<boolean>Whether this conversation is subscribed.
await thread.setTitle("Incident #4213");
await thread.setSuggestedPrompts([
  { title: "Summarize", message: "Summarize this thread" },
  { title: "File it", message: "File a Linear issue for this" },
]);
await thread.subscribe();