This page has been translated by machine translation. View original
Since 2026, new blocks have been continuously added to Slack's Block Kit. In this series, we will introduce these new blocks and related APIs while actually trying them out.
In this fourth installment, we introduce three methods of the text streaming API: chat.startStream / chat.appendStream / chat.stopStream. These are APIs for implementing the familiar LLM app experience of responses appearing gradually in Slack apps.
Previously, a streaming-like display could be achieved by repeatedly editing the same message with chat.update. However, this approach required preparing and resending the full text with each request, making it easy to hit rate limits when increasing the update frequency.
In contrast, the streaming API only requires sending the appended portion, and the rate limit for appendStream, which handles appending, is more lenient, making it increasingly easier to work with.
Incidentally, the mechanism for sequentially updating the plan block and task_card block introduced in Part 3 is also handled by this API.
Note that since this cannot be verified in the familiar Block Kit Builder, we will call the API directly using the slack api command of the Slack CLI for verification.
Please refer to the following article for details on this command.
All three methods only require the chat:write scope. By passing the timestamp received in the start response to append and stop, you can append to and finalize a single message.
Key Parameters
chat.startStream
Item
Description
channel
Required. Channel, thread, or DM ID
markdown_text
Standard Markdown format text. Maximum 12,000 characters
chunks
Array of streaming chunks. Cannot be used together with markdown_text
thread_ts
Thread to reply to. Omitting this in a regular channel results in an invalid_thread_ts error
recipient_user_id / recipient_team_id
ID of the user and team receiving the stream. Required when streaming to a channel
task_display_mode
How tasks are displayed. timeline (displays task cards individually alternating with text) or plan (displays grouped in a plan block). Default is timeline
An important point to note: streaming in regular channels is only possible as a thread reply.
Delivery as a regular non-reply post is only possible in channels with a special configuration where the entire channel becomes a single session. Additionally, when delivering to a channel, specifying recipient_user_id and recipient_team_id is required. This is a mechanism that allows Slack to understand who the streaming is directed to.
chat.appendStream
channel and ts are required, and content is passed via markdown_text or chunks. markdown_text is not a retransmission of the full text, but a method of sending only the appended portion.
chat.stopStream
Similarly, channel and ts are required, with content passed via markdown_text or chunks. In addition, the following unique items are available.
Item
Description
blocks
Array of blocks rendered at the end of the finalized message. Maximum 50, separate from the 50 via chunks
metadata
Message metadata
session_status
Status of the session to set after streaming ends. active / processing / suspended / closed
Streaming Text with markdown_text
Assuming a bot that answers expense reimbursement questions, let's call the three methods in sequence. First, the stream start process.
$ slack api chat.startStream --json '{ "channel": "C0123456789", "thread_ts": "<timestamp of the message to use as the thread root>", "recipient_team_id": "T0123456789", "recipient_user_id": "U0123456789", "markdown_text": "Looking into it.\n\n"}'
The ts in the response becomes the identifier for the streaming message. We specify this ts to append content.
$ slack api chat.appendStream --json '{ "channel": "C0123456789", "ts": "<timestamp from the stream start response>", "markdown_text": "## Business Trip Expense Reimbursement Deadline\n\nPlease submit your claim **within one week of your return date**."}'
Finally, we stop the stream. Let's pass the context_actions block introduced in Part 3 to blocks to add feedback buttons at the end of the finalized message.
Unlike overwriting and updating a message, you only need to send the appended portion, so you can simply send the LLM output as-is. It suddenly looks very much the part!
Streaming the Thought Process with chunks
Next, let's use the chunks parameter to sequentially update the plan block introduced in Part 3. There are four types of chunks.
Chunk Type
Purpose
markdown_text
Appending Markdown text
task_update
Adding or updating tasks. Items with the same id are treated as updates
plan_update
Updating the plan title
blocks
Adding block arrays. Maximum 50 per array; excess items are discarded with a warning via the API
The structure of the task_update chunk is almost the same as the task_card block, but differs in that details and output are strings rather than rich_text. Also, there is a 256-character limit on the chunk size for task_update and plan_update.
Specify plan for task_display_mode to start streaming, then send tasks one by one.
$ slack api chat.startStream --json '{ "channel": "C0123456789", "thread_ts": "<timestamp of the message to use as the thread root>", "recipient_team_id": "T0123456789", "recipient_user_id": "U0123456789", "task_display_mode": "plan", "chunks": [ { "type": "plan_update", "title": "Answering expense reimbursement question" }, { "type": "task_update", "id": "step_1", "title": "Search internal documents", "status": "in_progress" } ]}'
Task completion and the start of the next task are expressed through task_update on the same id. As before, specify the value from the start response for ts.
In Part 3, the plan block was presented as static JSON, but this is the more practical implementation.
Summary
In this article, we introduced the three methods of the text streaming API.
From sequential text display to plan block updates and feedback button placement, all of this can be achieved with a simple implementation, giving the impression that the display components for agents are becoming increasingly easy to use. If you have been using chat.update, why not take this opportunity to try out the new methods?
Next time is the final installment. Part 5 covers Validation & CI, where we will try payload validation using blocks.validate and integrating it into CI. Stay tuned!