showcase/shell-docs/src/content/docs/channels/reference/thread.mdx
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 });
});
| Property | Type | Description |
|---|---|---|
platform | string | The platform this thread is on: "slack", "teams", and so on. |
conversationKey | string | Stable key identifying this conversation. |
supportsBlockingChoice | boolean? | 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
}
| Method | Signature | Description |
|---|---|---|
post | post(ui: Renderable): Promise<MessageRef> | Send a message. ui is a string or a JSX component. Returns a ref you can update. |
update | update(ref: MessageRef, ui: Renderable): Promise<MessageRef> | Replace a message you posted, in place. |
delete | delete(ref: MessageRef): Promise<void> | Remove a message you posted. |
stream | stream(src: string | AsyncIterable<string>): Promise<MessageRef> | Post a message that fills in as the source yields. |
postEphemeral | postEphemeral(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.
| Method | Signature | Description |
|---|---|---|
runAgent | runAgent(input?): Promise<MessageRef | undefined> | Run the agent and stream its reply into the thread. |
resume | resume(value: unknown): Promise<MessageRef | undefined> | Resume a run waiting on a value (HITL). |
runAgent input:
| Field | Type | Description |
|---|---|---|
prompt | string | AgentContentPart[] | A user message to inject. Use the array form for files and images. |
context | ContextEntry[] | Extra context for this run. |
tools | ChannelTool[] | Extra tools for this run. |
transcript | boolean | { 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 });
| Method | Signature | Description |
|---|---|---|
postFile | postFile({ 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" });
| Method | Signature | Description |
|---|---|---|
awaitChoice | awaitChoice<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.
| Method | Signature | Description |
|---|---|---|
react | react(ref: MessageRef, emoji: EmojiValue): Promise<{ ok, error? }> | Add an emoji reaction to a message. |
unreact | unreact(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
| Method | Signature | Description |
|---|---|---|
getMessages | getMessages(): Promise<ThreadMessage[]> | The conversation's messages. [] when the adapter can't read history. |
lookupUser | lookupUser(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("[email protected]");
| Method | Signature | Description |
|---|---|---|
setState | setState<T>(v: T): Promise<void> | Persist arbitrary per-thread state (e.g. a workflow step). Typed and validated when store.state is set. |
state | state<T>(): Promise<T | undefined> | Read back what setState wrote. |
await thread.setState({ step: "awaiting_approval" });
const { step } = (await thread.state<{ step: string }>()) ?? {};
| Method | Signature | Description |
|---|---|---|
setSuggestedPrompts | setSuggestedPrompts(prompts, opts?): Promise<{ ok, error? }> | Pin suggested prompts (e.g. the Slack assistant pane). |
setTitle | setTitle(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();