Streaming and chunking in OpenClaw

Learn how OpenClaw streams channel messages via block replies and previews, and how startup status appears in the Control UI. This is for developers integrating or debugging channel message delivery.

Read this when

  • Explaining how streaming or chunking works on channels
  • Changing block streaming or channel chunking behavior
  • Debugging duplicate/early block replies or channel preview streaming

OpenClaw operates with two separate streaming layers, and at present there is no genuine token-by-token delta streaming for channel messages:

  • Block streaming (channels): sends out completed blocks as the assistant generates them. These are standard channel messages, not token deltas.
  • Preview streaming (Telegram/Discord/Slack/Matrix/Mattermost/MS Teams): refreshes a temporary preview message during generation (send plus edits/appends).

Control UI startup status

Once chat.send confirms an active run, the Gateway may dispatch a typed, coarse startup status before any assistant text or tool activity appears. The Control UI displays this status next to the working indicator, covering stages such as workspace preparation, environment provisioning, context preparation, and model startup.

The initial assistant delta or tool start permanently replaces the startup status for that run. While a tool waits for operator action, approval status takes priority. Worktree creation and initial cloud dispatch occur before a chat run exists, so their pre-run RPC progress is not shown as run startup status; environment provisioning appears here only when an active run reprovisions a reclaimed worker.

Block streaming (channel messages)

Block streaming delivers assistant output in coarse chunks as it becomes available.

Model output
  └─ text_delta/events
       ├─ (blockStreamingBreak=text_end)
       │    └─ chunker emits blocks as buffer grows
       └─ (blockStreamingBreak=message_end)
            └─ chunker flushes at message_end
                   └─ channel send (block replies)
  • text_delta/events: model stream events (may be sparse for non-streaming models).
  • chunker: EmbeddedBlockChunker applying min/max bounds plus break preference.
  • channel send: actual outbound messages (block replies).

Controls (all under agents.defaults unless noted):

KeyValues / shapeDefault
blockStreamingDefault"on" / "off""off"
blockStreamingBreak"text_end" / "message_end"-
blockStreamingChunk{ minChars, maxChars, breakPreference? }-
blockStreamingCoalesce{ minChars?, maxChars?, idleMs? } (merge streamed blocks before send)-
*.streaming.block.enabled (channel override)true / false, forces block streaming per channel (and per account)-
*.textChunkLimit (e.g. channels.whatsapp.textChunkLimit)number, hard cap4000
*.streaming.chunkMode"length" / "newline""length"
channels.discord.maxLinesPerMessagenumber, soft line cap that splits tall replies to avoid UI clipping17

streaming.chunkMode: "newline" splits on blank lines (paragraph boundaries), not every newline, before falling back to length chunking once the text exceeds the limit.

Bundled channels spell these overrides as channels.<id>.streaming.{chunkMode,block.enabled,block.coalesce}. The flat *.chunkMode / *.blockStreaming / *.blockStreamingCoalesce spellings are rejected everywhere. openclaw doctor --fix migrates legacy configs into the nested shape.

Boundary semantics for blockStreamingBreak:

  • text_end: stream blocks as soon as the chunker emits; flush on each text_end.
  • message_end: wait until the assistant message finishes, then flush buffered output. Still uses the chunker if the buffered text exceeds maxChars, so it can emit multiple chunks at the end.

Media delivery with block streaming

Streaming media must use structured payload fields such as mediaUrl or mediaUrls; streamed text is not parsed as an attachment command. When block streaming sends media early, OpenClaw remembers that delivery for the turn. If the final assistant payload repeats the same media URL, final delivery strips the duplicate media instead of sending the attachment again.

Exact duplicate final payloads are suppressed. If the final payload adds distinct text around media that was already streamed, OpenClaw still sends the new text while keeping the media single-delivery. This prevents duplicate voice notes or files on channels such as Telegram.

Chunking algorithm (low/high bounds)

Block chunking is implemented by EmbeddedBlockChunker:

  • Low bound: don't emit until buffer >= minChars (unless forced).
  • High bound: prefer splits before maxChars; if forced, split at maxChars.
  • Break preference chain: paragraph -> newline -> sentence -> whitespace -> hard break.
  • Code fences: never split inside fences; when forced at maxChars, close and reopen the fence to keep Markdown valid.

maxChars is clamped to the channel textChunkLimit, so you cannot exceed per-channel caps.

Coalescing (merge streamed blocks)

When block streaming is enabled, OpenClaw can merge consecutive block chunks before sending them, reducing single-line spam while still providing progressive output.

  • Flushing is deferred until an idle gap (idleMs) is detected.
  • Buffer size is limited by maxChars, and once that ceiling is hit, a flush is triggered.
  • Tiny fragments are held back by minChars until enough text has built up (a final flush always delivers whatever remains).
  • The joiner comes from blockStreamingChunk.breakPreference: paragraph becomes \n\n, newline becomes \n, and sentence becomes a space.
  • Channel-level overrides are exposed through *.streaming.block.coalesce (which also covers per-account settings).
  • Unless overridden, Discord, Signal, and Slack all coalesce to { minChars: 1500, idleMs: 1000 }.

Human-like pacing between blocks

With block streaming turned on, insert a randomized pause between block replies, starting after the first block, so multi-bubble responses come across as more natural.

agents.defaults.humanDelay.modeBehavior
off (default)No pause
natural800-2500ms random pause
customminMs/maxMs

An agent-level override is available via agents.entries.*.humanDelay. This applies exclusively to block replies, not to final replies or tool summaries.

"Stream chunks or everything"

  • Stream chunks: use blockStreamingDefault: "on" plus blockStreamingBreak: "text_end" (emit incrementally). For non-Telegram channels, *.streaming.block.enabled: true is also required.
  • Stream everything at end: use blockStreamingBreak: "message_end" (a single flush, which may split into multiple chunks for very long content).
  • No block streaming: use blockStreamingDefault: "off" (final reply only).

Block streaming follows agents.defaults.blockStreamingDefault unless a channel or account explicitly sets *.streaming.block.enabled. QQ Bot lacks streaming.block keys, so it streams block replies unless channels.qqbot.streaming.mode is set to "off". A live preview (channels.<channel>.streaming.mode) can be streamed by channels without block replies. The blockStreaming* defaults are located under agents.defaults, not at the config root.

On Discord and Telegram, an explicitly configured preview mode that is not off takes priority over inherited agents.defaults.blockStreamingDefault: "on". When block replies should win over the preview, set that channel's streaming.block.enabled: true. If the preview is unavailable for a given turn, inherited block delivery still applies.

Preview streaming modes

Canonical key: channels.<channel>.streaming (nested under { mode, ... }; legacy top-level boolean/string spellings get rewritten by openclaw doctor --fix).

ModeBehavior
offTurn off preview streaming
partialSingle preview replaced with latest text
blockPreview updates in chunked/appended steps
progressProgress/status preview during generation, final answer at completion

For edit-capable channels like Discord and Telegram, streaming.mode: "block" acts as a preview-streaming mode; on its own it does not enable channel block delivery there. Use streaming.block.enabled for standard block replies. Microsoft Teams stands apart: it lacks a draft-preview block transport, so streaming.mode: "block" fully disables native streaming, and the reply arrives as ordinary block delivery instead of native partial/progress streaming. Mattermost behaves differently too: in block mode it cycles the preview between completed text and tool-activity blocks, keeping earlier blocks visible as separate posts rather than overwriting them in a single editable draft.

Channel mapping

When streaming is unset, Discord defaults to off, Telegram and Slack default to progress, and Mattermost and MS Teams default to partial.

Channeloffpartialblockprogress
TelegramYesYesYeseditable progress draft (default)
DiscordYes (default)YesYeseditable progress draft (opt-in)
SlackYesYesYesBlock Kit session card (default)
MattermostYesYesYesYes
MS TeamsYesYesYesnative progress stream

Preview chunk configuration (streaming.preview.chunk.*, for instance under channels.discord.streaming or channels.telegram.streaming) is set by default to minChars: 200, maxChars: 800 (limited by the channel's textChunkLimit), and breakPreference: "paragraph".

Applies to Slack only:

  • When channels.slack.streaming.mode="partial" is active (nativeTransport has a default of true), channels.slack.streaming.nativeTransport enables Slack's native streaming API calls (chat.startStream/chat.appendStream/chat.stopStream).
  • A reply thread target is necessary for both Slack native streaming and Slack assistant thread status. Top-level DMs do not display that thread-style preview, though they can still utilize Slack draft preview posts and edits.

Legacy key migration

ChannelLegacy keysStatus
TelegramstreamMode, scalar/boolean streamingConverted to streaming.mode via openclaw doctor --fix; not consulted during runtime
DiscordstreamMode, boolean streamingConverted to streaming.mode via openclaw doctor --fix; not consulted during runtime
SlackstreamMode; boolean streaming; legacy nativeStreamingConverted to streaming.mode (plus streaming.nativeTransport for the boolean/legacy variants) via openclaw doctor --fix; not consulted during runtime
Matrixscalar/boolean streamingConverted to streaming.mode (covering Matrix's "quiet" mode as well) via openclaw doctor --fix; not consulted during runtime
Feishuboolean streamingConverted to streaming.mode via openclaw doctor --fix; not consulted during runtime
QQ Botboolean streaming; streaming.c2cStreamApiConverted to streaming.mode (plus streaming.nativeTransport for the boolean/c2cStreamApi variants) via openclaw doctor --fix; not consulted during runtime

Runtime behavior

Telegram

  • Preview updates rely on sendMessage combined with editMessageText across DMs and group/topics; the final text modifies the active preview in place. Telegram's ephemeral 30-second "typing" drafts (sendMessageDraft) are not employed for answer streaming.
  • Short initial previews still undergo debouncing for push-notification UX, but they appear after a fixed delay so active runs never remain visually silent.
  • Long finals reuse the preview message for the initial chunk, transmitting only the leftover chunks afterward.
  • In block mode, the preview is rotated into a fresh message at streaming.preview.chunk.maxChars (default 800, limited by Telegram's 4096 edit cap); other modes expand a single preview up to 4096 characters.
  • progress mode keeps tool progress in an editable status draft, materializes the status label when answer streaming is active but no tool line is ready, clears the draft upon completion, and routes the final answer through standard delivery.
  • If the final edit fails before the completed text is confirmed, OpenClaw falls back to normal final delivery and removes the stale preview.
  • When Telegram block streaming is explicitly enabled, preview streaming is bypassed to prevent double-streaming.
  • /reasoning stream has the ability to write reasoning to a transient preview that gets removed after final delivery.
  • Telegram selected quote replies are an exception: when replyToMode differs from "off" and selected quote text exists, OpenClaw omits the answer preview stream for that turn (the final answer must use the native quote-reply path), so tool-progress preview lines cannot appear. Current-message replies lacking selected quote text still retain preview streaming. Consult the Telegram channel docs for further information.

Discord

  • Send and edit preview messages are the mechanism used.
  • Draft chunking (draftChunk) is applied in block mode.
  • When Discord block streaming is explicitly enabled, preview streaming is skipped.
  • Once the final answer is delivered, progress mode removes the status draft, keeping busy channels free of orphaned tool logs above the reply. Error finals preserve the draft as the record of the failed turn.
  • Pending previews are cancelled without flushing a new draft for final media, error, and explicit-reply payloads, which then use standard delivery.

Slack

  • partial takes advantage of Slack's native streaming capabilities (chat.startStream/append/stop) whenever those are offered.
  • Draft previews in the append style are what block relies on.
  • By default, progress streams Slack's native agent card: a single message combines narration, the live plan/task card, and the final response. Only turns that perform actual work trigger the card, so straightforward questions get answered without it. When that isn't possible, streaming.progress.nativeTaskCards: false switches to the Block Kit session card, which concludes with either success or error and delivers the assistant's closing text as its own message.
  • The Open in OpenClaw button shows up on cards solely when the session is genuinely openable: gateway.publicOrigin must be configured and gateway.controlUi.enabled must not equal false.
  • In top-level DMs that lack a reply thread, draft preview posts and edits are used instead of Slack's native streaming.
  • Both native and draft preview streaming suppress block replies for that turn, guaranteeing a Slack reply travels through exactly one delivery path.
  • A successful turn that produces no visible reply still removes its draft card. A failed turn without a reply leaves the card showing its error state.

Mattermost

  • Under partial mode, thinking and partial reply text stream into one draft preview post, which finalizes in place once the final answer is ready to send.
  • Under progress mode, thinking and tool activity stream into a single status preview, finalizing in place when the final answer can be safely delivered.
  • Under block mode, it alternates between completed text and tool-activity posts; parallel and consecutive tool updates share the current tool-activity post.
  • If the preview post was deleted or otherwise unavailable at finalize time, a fresh final post is sent as a fallback.
  • Final media or error payloads cancel any pending preview updates before normal delivery, rather than flushing a temporary preview post.

Matrix

  • Draft previews finalize in place when the final text can reuse the preview event.
  • Media-only, error, and reply-target-mismatch finals cancel pending preview updates before normal delivery; a stale preview that's already visible gets redacted.

Tool-progress preview updates

Preview streaming can also carry tool-progress updates: short status lines like "searching the web", "reading file", or "calling tool" that show up in the same preview message while tools are running, ahead of the final reply. In Codex app-server mode, Codex preamble and commentary messages follow this same preview path, so brief "I am checking..." progress notes can stream into the editable draft without being part of the final answer. This keeps multi-step tool turns visually active instead of silent between the first thinking preview and the final answer.

Long-running tools may emit typed progress before they return. For instance, web_fetch sets a five-second timer at startup: if the fetch is still pending, the preview shows Fetching page content...; if the fetch completes or is canceled before that, no progress line appears. The later final tool result still reaches the model through normal delivery.

Supported surfaces:

  • Discord, Slack, Telegram, and Matrix stream tool-progress and Codex preamble updates into the live preview edit by default when preview streaming is active. Microsoft Teams uses its native progress stream in personal chats.
  • Telegram has shipped with tool-progress preview updates enabled since v2026.4.22; leaving them on preserves that released behavior.
  • Mattermost folds tool activity into one preview post in partial and progress modes, or one tool-activity post between text blocks in block mode (see above).
  • Tool-progress edits follow the active preview streaming mode; they're skipped when preview streaming is off or when block streaming has taken over the message. On Telegram, streaming.mode: "off" is final-only: generic progress chatter is also suppressed instead of delivered as standalone status messages, while approval prompts, media payloads, and errors still route normally.
  • To keep preview streaming but hide tool-progress lines, set streaming.preview.toolProgress or streaming.progress.toolProgress to false for that channel (both default true, and both are honored in every mode). To keep tool-progress lines visible while hiding command/exec text, set streaming.preview.commandText or streaming.progress.commandText to "status" (the default). Set either option to "raw" to opt into command text. This policy is shared by draft/progress channels that use OpenClaw's compact progress renderer, including Discord, Matrix, Microsoft Teams, Mattermost, Slack session cards, and Telegram. To disable preview edits entirely, set streaming.mode to off.

Progress draft rendering

Progress-mode drafts (streaming.progress.*) are bounded and configurable per channel:

KeyDefaultBehavior
streaming.progress.maxLines8Max compact progress lines kept below the draft label
streaming.progress.maxLineChars120Max characters per compact line before truncation (word-aware)
streaming.progress.label"auto"Draft title; a custom string, or false to hide it
streaming.progress.labelsbuilt-in poolCandidate labels used when label: "auto"

Slack always renders progress mode as its fixed session-card layout; these limits still bound the activity rows and plan text inside that card.

Commentary progress lane

Beyond tool-progress, the compact progress renderer can surface one more lane in the draft:

  • streaming.progress.commentary - render the model's pre-tool commentary (a short "I'll check... then..." narration) interleaved with tool lines in the progress draft. On Discord and Telegram in progress mode, the same preamble supplies the status headline even when this optional lane is off; other channels keep their existing progress behavior. See Progress drafts.
{
  "channels": {
    "discord": {
      "streaming": { "mode": "progress", "progress": { "commentary": true } }
    }
  }
}

Keep progress lines visible but hide raw command/exec text:

{
  "channels": {
    "telegram": {
      "streaming": {
        "mode": "partial",
        "preview": {
          "toolProgress": true,
          "commandText": "status"
        }
      }
    }
  }
}

Use the same shape under another compact progress channel key, for example channels.discord, channels.matrix, channels.msteams, channels.mattermost, or Slack draft previews. For progress-draft mode, put the same policy under streaming.progress:

{
  "channels": {
    "telegram": {
      "streaming": {
        "mode": "progress",
        "progress": {
          "toolProgress": true,
          "commandText": "status"
        }
      }
    }
  }
}
2,949 words · updated Aug 24, 2026