Trying Out the New Slack API Methods [Streaming Edition]
This page has been translated by machine translation. View original
Since the start of 2026, new blocks have been steadily added to Slack's Block Kit. In this series, we introduce these new blocks and related APIs by actually trying them out.
- Part 1: Data
- Part 2: General Display
- Part 3: Agents
- Part 4: Streaming (this article)
- Part 5: Validation and CI
In Part 4, we introduce three methods of the text streaming API: chat.startStream / chat.appendStream / chat.stopStream. These APIs let a Slack app achieve the familiar LLM-app experience where a response is displayed gradually.
Until now, you could create a streaming-like display by repeatedly editing the same message with chat.update. However, this approach required preparing and resending the full text with every request, and it was easy to hit rate limits as you increased the update frequency.
In contrast, with the streaming API you only need to send the appended portion, and the rate limit for appendStream, which handles appending, is relaxed, so it has become even easier to use.
Incidentally, this API is also what powers the mechanism for incrementally updating the plan block and task_card block introduced in Part 3.
Note that this time we can't verify with the familiar Block Kit Builder, so we'll call the APIs directly using the slack api command of the Slack CLI.
For details on this command, please refer to the following article.
Method Overview
| Method | Role | Rate limit |
|---|---|---|
| chat.startStream[1] | Starts a streaming message | Tier 2 (20+ per minute) |
| chat.appendStream[2] | Appends to a message being streamed | Tier 4 (100+ per minute) |
| chat.stopStream[3] | Ends the streaming and finalizes the message | Tier 2 (20+ per minute) |
All three methods require only the chat:write scope. By passing the timestamp received in the response of start to append and stop, you append to and finalize a single message.
Main Parameters
chat.startStream
| Item | Description |
|---|---|
channel |
Required. The ID of the channel, thread, or DM |
markdown_text |
Text in standard Markdown format. Up to 12,000 characters |
chunks |
An array of streaming chunks. Cannot be used together with markdown_text |
thread_ts |
The thread to reply to. In a regular channel, omitting it results in an invalid_thread_ts error |
recipient_user_id / recipient_team_id |
The IDs of the user and team that receive the streaming. Required when streaming to a channel |
task_display_mode |
How tasks are displayed. timeline (task cards are displayed individually, alternating with text) or plan (displayed together in a plan block). The default is timeline |
An important point is that in a regular channel, streaming is only possible as a thread reply.
Delivery as a regular post rather than a reply is possible only in channels with a special setup where the entire channel acts as a single session. Also, when delivering to a channel, specifying recipient_user_id and recipient_team_id is mandatory. This is a mechanism that lets Slack know who the streaming is intended for.
chat.appendStream
channel and ts are required, and you pass the content via markdown_text or chunks. markdown_text works by sending only the appended portion, not resending the full text.
chat.stopStream
Similarly, channel and ts are required, and you pass the content via markdown_text or chunks. In addition, it has the following specific items.
| Item | Description |
|---|---|
blocks |
An array of blocks rendered at the end of the finalized message. Up to 50, separate from the 50 via chunks |
metadata |
Message metadata |
session_status |
The session status to set after streaming ends. active / processing / suspended / closed |
Streaming Text with markdown_text
Assuming a bot that answers questions about expense reimbursement, let's call the three methods in order. First is the streaming start process.
$ slack api chat.startStream --json '{
"channel": "C0123456789",
"thread_ts": "<スレッド元となるメッセージのタイムスタンプ>",
"recipient_team_id": "T0123456789",
"recipient_user_id": "U0123456789",
"markdown_text": "調べています。\n\n"
}'
The ts in the response becomes the identifier of the streaming message. We specify this ts to append.
$ slack api chat.appendStream --json '{
"channel": "C0123456789",
"ts": "<ストリーミング開始処理レスポンスのタイムスタンプ>",
"markdown_text": "## 出張費の精算期限\n\n**帰着日から 1 週間以内**に申請してください。"
}'
Finally, we stop it. We'll pass the context_actions block introduced in Part 3 to blocks to add feedback buttons at the end of the finalized message.
$ slack api chat.stopStream --json '{
"channel": "C0123456789",
"ts": "<ストリーミング開始処理レスポンスのタイムスタンプ>",
"blocks": [
{
"type": "context_actions",
"elements": [
{
"type": "feedback_buttons",
"action_id": "answer_feedback",
"positive_button": {
"text": { "type": "plain_text", "text": "👍" },
"value": "positive_feedback"
},
"negative_button": {
"text": { "type": "plain_text", "text": "👎" },
"value": "negative_feedback"
}
}
]
}
]
}'

Unlike overwriting a message, you only need to send the appended portion, so you can simply send the LLM output as it is. The display suddenly looks the part!
Streaming the Thought Process with chunks
Next, let's use the chunks parameter to incrementally 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. Those with the same id are treated as updates |
plan_update |
Updating the plan title |
blocks |
Adding an array of blocks. Up to 50 per array; any excess is discarded and a warning is issued via the API |
The structure of the task_update chunk is almost the same as the task_card block, except that details and output are strings rather than rich_text. Also, the chunk size of task_update and plan_update is limited to 256 characters.
We'll start streaming with plan specified for task_display_mode and send tasks in order.
$ slack api chat.startStream --json '{
"channel": "C0123456789",
"thread_ts": "<スレッド元となるメッセージのタイムスタンプ>",
"recipient_team_id": "T0123456789",
"recipient_user_id": "U0123456789",
"task_display_mode": "plan",
"chunks": [
{ "type": "plan_update", "title": "経費精算の質問に回答中" },
{ "type": "task_update", "id": "step_1", "title": "社内ドキュメントを検索", "status": "in_progress" }
]
}'
The completion of a task and the start of the next task are expressed with task_update for the same id. For ts, as before, specify the value from the start response.
$ slack api chat.appendStream --json '{
"channel": "C0123456789",
"ts": "<ストリーミング開始処理レスポンスのタイムスタンプ>",
"chunks": [
{ "type": "task_update", "id": "step_1", "title": "社内ドキュメントを検索", "status": "complete", "output": "経費精算ガイドが見つかりました" },
{ "type": "task_update", "id": "step_2", "title": "回答を生成", "status": "in_progress" }
]
}'

In Part 3 we pasted the plan block 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 incremental text display to updating plan blocks and placing feedback buttons, all of it can now be achieved with simple implementations, and it feels like display components for agents are becoming easier and easier to use. If you've been using the conventional chat.update, why not take this opportunity to try the new methods?
Next time is finally the last installment. Part 5 is Validation and CI, where we'll try payload validation with blocks.validate and integrating it into CI. Look forward to it!
