Streaming
Chat SDK accepts any AsyncIterable<string> as a message, so you can stream AI responses and other incremental content to chat platforms as it's generated. Streams from the AI SDK and TanStack AI are detected automatically. On platforms with native or structured streaming support, you can also stream StreamChunk objects for rich content such as task progress cards and plan updates.
AI SDK integration
Pass an AI SDK fullStream or textStream directly to thread.post():
Passing thread.signal lets platform cancellation stop model generation. In
Slack Agent messaging, clicking the native stop button aborts this signal even
when the stop webhook is handled by another serverless instance using the same
state adapter.
Why fullStream over textStream?
When AI SDK agents make tool calls between text steps, textStream concatenates all text without separators, producing "hello.how are you?" instead of "hello.\n\nhow are you?". The fullStream contains explicit finish-step events that Chat SDK uses to inject paragraph breaks between steps automatically.
Both stream types are auto-detected:
For a human-in-the-loop turn, wrap the stream in StreamingPlan and leave the
Slack Agent Session suspended after the final chunk:
TanStack AI integration
The stream returned by chat() from @tanstack/ai can be passed straight to thread.post():
TanStack AI streams follow the AG-UI protocol. Chat SDK extracts the text from TEXT_MESSAGE_CONTENT deltas and treats each TEXT_MESSAGE_END as a paragraph break, so when the tool loop runs several model turns their text is separated by \n\n (the same behavior as finish-step for the AI SDK). Tool call, reasoning, run, and step events are skipped.
This is for text responses. A structured-output run (chat({ ..., outputSchema, stream: true })) streams its JSON as TEXT_MESSAGE_CONTENT deltas, so posting that stream would show raw JSON in the channel. Format the result yourself and post the string instead.
TanStack's chat() takes an AbortController rather than a signal. Forwarding thread.signal to it, as above, lets platform cancellation stop model generation.
To feed prior thread history into chat() and give it Chat SDK tools, use toTanStackMessages and createTanStackTools from chat/ai/tanstack, covered in TanStack AI.
Custom streams
Any async iterable works:
Platform behavior
| Platform | Method | Description |
|---|---|---|
| Slack | Native streaming API | Uses Slack's chatStream to update the message as chunks arrive |
| Telegram | Post + Edit | Posts a message then edits it as chunks arrive (throttled to ~1 edit/s). Set nativeStreaming: true to use sendRichMessageDraft previews in private chats instead, persisted with sendRichMessage |
| Teams | Native (DMs) / Buffered (group chats) | Uses the Teams SDK's native stream.emit() for direct messages; accumulates chunks and posts one final message when no native streamer is active |
| Google Chat | Post + Edit | Posts a message then edits it as chunks arrive |
| Discord | Post + Edit | Posts a message then edits it as chunks arrive |
| GitHub | Buffered | Accumulates chunks and posts one final comment |
| Linear | Agent sessions / Post + Edit | Uses agent session activities in agent-session threads; falls back to post+edit comments in issue threads |
| Notion | Post + Edit | Posts a comment then edits it as chunks arrive (throttled for ~3 req/s) |
| Buffered | Accumulates chunks and sends one final message | |
| Messenger | Buffered | Accumulates chunks and sends one final message |
| Twitch | Buffered | Accumulates chunks and sends one final chat message or whisper |
The post+edit fallback throttles edits to avoid rate limits. Configure the update interval when creating your Chat instance:
Disabling the placeholder message
By default, post+edit adapters send an initial "..." placeholder message before the first chunk arrives. You can disable this to wait for real content before posting:
You can also customize the placeholder text:
On Teams, a custom placeholder becomes an informative status during native direct-message streaming, while group chats edit the placeholder into the final answer. Leave the option unset to preserve native DM and buffered group-chat behavior, or set it to null to disable the status.
Markdown healing
During streaming, chunks often arrive mid-word or mid-syntax. For example, **bold can arrive before the closing **. The SDK heals incomplete markdown in intermediate renders using remend, so messages display with correct formatting while streaming.
The final message uses the raw accumulated text without healing, so the original markdown is preserved.
Table buffering
When streamed content contains GFM tables, as LLM output often does, the SDK buffers potential table headers until a separator line (|---|---|) confirms them. This stops tables from briefly flashing as raw pipe-delimited text before the table structure is complete. Table buffering needs no configuration.
Structured streaming chunks
For Slack native streams and Linear agent-session streams, you can yield StreamChunk objects alongside plain text for rich progress updates:
Chunk types
| Type | Fields | Description |
|---|---|---|
markdown_text | text | Streamed text content |
task_update | id, title, status, details?, output? | Tool/step progress updates (pending, in_progress, complete, error) with optional extra task context |
plan_update | title | Plan title updates on supported platforms |
Streaming with options
Wrap a stream in a StreamingPlan to pass platform-specific options through thread.post() without dropping down to adapter.stream() directly:
| Option | Platform | Description |
|---|---|---|
groupTasks | Slack | "timeline" (default) renders task cards inline; "plan" groups them into one plan block |
endWith | Slack | Block Kit elements attached when the stream stops (e.g. retry / feedback buttons) |
updateIntervalMs | Post+edit adapters | Minimum interval between post+edit cycles in ms (default 500) |
aiGenerated | Teams | Show the "AI generated" label on the streamed message |
Adapters without structured chunk support extract text from markdown_text chunks and ignore other types. Slack-only options are silently ignored on other platforms.
Stop blocks (Slack only)
Use endWith on StreamingPlan to attach Block Kit elements to the final message, such as action buttons that appear after a streamed response completes:
Plan API
For step-by-step task progress that lives outside an LLM stream, post a Plan directly. Plan is a PostableObject you can mutate after posting, and each mutation re-renders the block in place.
By default updateTask() mutates the most recent in_progress task. For parallel work, pass autoCompletePrevious: false to addTask() so earlier tasks stay in progress; then pass { id } to updateTask() to target a specific task when updates run out of order:
Adapters that don't support PostableObject editing (e.g. WhatsApp) render the plan as a fallback emoji-list message; the plan still posts, but mutations are no-ops.
| Method | Description |
|---|---|
addTask({ title, children?, autoCompletePrevious? }) | Append a new task. When autoCompletePrevious is true (default), existing in-progress tasks are marked complete first; pass false for parallel workflows |
updateTask(input) | Mutate the current (or { id }-targeted) task's output, status, or title |
complete({ completeMessage }) | Mark all in-progress tasks complete and update the plan title |
reset({ initialMessage }) | Discard all tasks and start fresh with a new initial message. Use it to reuse a plan handle for a new run |
Streaming with conversation history
Combine message history with streaming for multi-turn AI conversations.
Use toAiMessages() to convert chat messages into the { role, content } format expected by AI SDKs:
See the toAiMessages reference for all options including includeNames, transformMessage, and attachment handling.
Read more
Markdown
AST builder functions and utilities for programmatic message formatting.
Overlapping Messages
Control how overlapping messages on the same thread are handled, with the burst, queue, debounce, drop, and concurrent strategies.
Overview
AI utilities that ship with Chat SDK, including agent tools, message conversion, and supporting types.
Creating a Chat Instance
Initialize the Chat class with adapters, state, and configuration options.