Telegram message behavior
Runtime model, stream previews, native commands, reply tags, ack reactions, and send limits
How inbound and outbound Telegram messages are routed, previewed, acknowledged, and delivered.
Runtime behavior
- Telegram message handling runs inside the gateway process.
- Routing is deterministic: Telegram inbound replies back to Telegram (the model does not pick channels).
- Inbound messages normalize into the shared channel envelope with reply metadata, media placeholders, and persisted reply-chain context for replies the gateway has observed.
- Group sessions are isolated by group ID. Forum topics append
:topic:<threadId>. - When the bot joins an allowed group or supergroup, it posts one introduction grounded in available room metadata: the group title, description, and pinned message. The Telegram Bot API cannot read group messages from before the bot joined, so introductions never claim to use prior chat history. Introductions are enabled by default, never run in private chats, and can be disabled with
channels.telegram.joinIntro: falseor overridden per account withchannels.telegram.accounts.<accountId>.joinIntro. See group join introductions for once-per-room behavior and untrusted-content handling. - DM messages can carry
message_thread_id; OpenClaw preserves it for replies. DM topic sessions split only when TelegramgetMereportshas_topics_enabled: truefor the bot; otherwise DMs stay on the flat session. - Long polling runs in an isolated worker. Updates are saved to a durable queue and processed in order for each chat and topic.
- Multi-account startup bounds concurrent
getMechecks so large bot fleets do not fan out every account check at once. - Each gateway process guards long polling so only one active poller can use a bot token at a time. Persistent
getUpdates409 conflicts point to another OpenClaw gateway, script, or external poller using the same token. - The polling watchdog restarts after 120 seconds without completed
getUpdatesliveness. - Telegram Bot API has no read-receipt support (
sendReadReceiptsdoes not apply).
After an upgrade, old unversioned reply-cache entries are treated as cache misses. Their reply-chain context may be unavailable until those messages are observed again. Version-1 cache entries, retained group history, and session transcripts remain supported. Transcript deduplication uses recorded message identities; markerless assistant replies may appear in both reply context and the transcript.
Upgrade note: Telegram's default preview changed in 2026.8.1. With channels.telegram.streaming unset, Telegram keeps one editable status draft during the turn (the agent's current status plus its tool lines) and sends the final answer as a normal message. It previously streamed the answer text itself into the preview. No config becomes invalid and no doctor --fix is needed; to keep the previous behavior, set:
{ channels: { telegram: { streaming: { mode: "partial" } } } }channels.telegram.dm.threadReplies and channels.telegram.direct.<chatId>.threadReplies were removed. Run openclaw doctor --fix after upgrading if your config still has those keys. DM topic routing now follows Telegram getMe.has_topics_enabled (controlled by BotFather threaded mode): topics-enabled bots use thread-scoped DM sessions when Telegram sends message_thread_id; other DMs stay on the flat session.
Changes to replyToMode, streaming, and textChunkLimit apply to the next
assembled turn without reconnecting Telegram, including account overrides.
Active turns keep their captured delivery settings.
Inbound text batching
Telegram batches rapid text messages from the same sender into one agent turn by default. Ordinary text waits for a 300ms quiet window; a non-forwarded message of at least 4000 characters allows up to 1500ms for likely long-paste continuations.
- Short and long text share one batch, so a short introduction, long paste, and short follow-up can arrive as one turn. Message IDs do not need to be consecutive.
- Batches stay isolated by bot account, sender, chat, and topic. Reply metadata and source message IDs are preserved.
messages.inbound.byChannel.telegramoverridesmessages.inbound.debounceMs, which overrides the 300ms ordinary-text default. An explicit0disables ordinary burst batching but keeps automatic long-paste assembly.- Control commands bypass batching and dispatch immediately. Stop/abort commands cancel pending text for their target conversation.
- Forwarded messages use a separate 1-second collection window, but share the sender's dispatch queue so they cannot overtake earlier text. Telegram delivers each message of a multi-message forward as its own update, often in a later poll, so the window waits long enough for the rest of the burst to arrive. A single forward therefore starts its turn about one second after it arrives. If another forward from the same sender is already queued on the conversation's ingress lane, collection waits for it, bounded by the existing 5-second batch deadline.
Inbound photo albums use a 500ms quiet window. When another member of the same album is already queued on the conversation's ingress lane, OpenClaw waits for it before flushing, with holds bounded to 20 seconds from the first buffered member. These windows cover Telegram delivery gaps; local processing delays do not split an album or forward burst while matching members remain queued within those bounds.
Ordinary text batches are bounded to 12 messages and 50,000 characters. Their collection deadline is 7.5 seconds from the first message, or the configured quiet window if longer. Messages arriving after a batch flushes cannot join it. This heuristic does not guarantee that Telegram delivers every paste fragment together. See Inbound debouncing for hot-reload behavior.
Message behavior
Live stream preview (message edits)
OpenClaw streams partial replies in real time in direct chats, groups, and topics: send a preview message, then editMessageText repeatedly, finalizing in place. Preview edits that carry writer authority, such as finalization, recheck the active writer before each queued Telegram request, so a replaced turn cannot send a stale edit from that path.
channels.telegram.streamingisoff | partial | block | progress(default:progress); setmode: "partial"to stream answer text into the preview instead of a status draft- short initial answer previews are debounced, then materialized after a bounded delay if the run is still active
progresskeeps one editable status draft, shows the stable status label when answer activity arrives before tool progress, clears it at completion, and sends the final answer as a normal message. By default the draft is quiet: status headline, commentary, plan milestones, and approval requests. Intermediate tool failures and nonzero command exits are hidden; terminal task errors still use normal error delivery.streaming.progress.toolProgress: trueadds the rolling tool log, including tool failures.streaming.preview.toolProgresscontrols whether tool/progress updates reuse the same edited preview message inpartialandblockmodes (default:truewhen preview streaming is active)streaming.preview.commandTextcontrols command/exec detail inside those lines:status(default, tool label only) orraw(explicit command text)- completed assistant preambles update the status headline by default; a new preamble keeps the previous readable status until it finishes
streaming.progress.commentary(default:false) shows those preambles as interleaved commentary rows instead of a headline; commentary remains visible beside plan steps- successful background-process polls and internal waits stay out of the progress log; failures still follow the selected tool-progress policy, and
/verboseretains diagnostic summaries - legacy
channels.telegram.streamMode, booleanstreamingvalues, and retired native draft preview keys are detected; runopenclaw doctor --fixto migrate them
Tool-progress lines are the short status updates shown while tools run (command execution, file reads, planning updates, patch summaries, Codex preamble/commentary in app-server mode). partial and block previews show them by default; the progress draft shows them only with streaming.progress.toolProgress: true. Compaction status follows the same settings and appears as soon as compaction starts, including before the first model output.
In default progress mode, /verbose on sends separate tool summaries and /verbose full also sends completed tool output. An explicit toolProgress: false wins over /verbose; streaming.mode: "off" also suppresses these diagnostics.
Keep answer-preview edits but hide tool-progress lines:
{
"channels": {
"telegram": {
"streaming": {
"mode": "partial",
"preview": { "toolProgress": false }
}
}
}
}Keep tool-progress visible but hide command/exec text:
{
"channels": {
"telegram": {
"streaming": {
"mode": "partial",
"preview": { "commandText": "status" }
}
}
}
}progress mode can show the tool log without editing the final answer into that message. Opt in with toolProgress: true and put the command-text policy under streaming.progress:
{
"channels": {
"telegram": {
"streaming": {
"mode": "progress",
"progress": {
"toolProgress": true,
"commandText": "status"
}
}
}
}
}streaming.mode: "off" disables preview edits and suppresses generic tool/progress chatter instead of sending it as standalone status messages; approval prompts, media, and errors still route through normal final delivery. streaming.preview.toolProgress: false keeps only answer-preview edits.
Selected quote replies are the exception. When replyToMode is first, all, or batched and the inbound message has selected quote text, OpenClaw sends the final answer through Telegram's native quote-reply path and skips draft previews for that turn. Current-message replies without selected quote text still stream. When reply threading is enabled, their previews carry automatic quote excerpts and retain them when finalized in place. Set replyToMode: "off" when tool-progress visibility matters more than native quote replies. To keep native quote replies and hide tool-progress lines, use streaming.progress.toolProgress: false in progress mode or streaming.preview.toolProgress: false in partial and block modes.
For text-only replies: short previews get the final edit in place; long finals that split into multiple messages reuse the preview as the first chunk, then send only the remainder; progress-mode finals clear the status draft and use normal final delivery; if the final edit fails before completion is confirmed, OpenClaw falls back to normal final delivery and cleans up the stale preview. For complex replies (media payloads), OpenClaw always falls back to normal final delivery and cleans up the preview.
Preview streaming and block streaming are mutually exclusive. An explicit non-off preview mode overrides inherited agents.defaults.blockStreamingDefault: "on"; explicit streaming.block.enabled: true overrides the preview. For ordinary single-agent turns, when a reply-modifying plugin hook prevents previews, completed answer blocks use normal hooked delivery instead, unless block streaming is explicitly disabled globally with agents.defaults.blockStreamingDefault: "off" or for Telegram with streaming.block.enabled: false. Configured multi-agent group-thread turns do not use this forced fallback; like other turns that cannot use previews, they retain the configured block delivery policy.
Reasoning: /reasoning stream streams reasoning into the live preview while generating, then deletes the reasoning preview after final delivery (use /reasoning on to keep it visible). The final answer is sent without reasoning text.
Native commands and custom commands
Telegram's command menu is registered at startup with `setMyCommands`. `commands.native: "auto"` enables native commands for Telegram.
Add custom command menu entries:{
channels: {
telegram: {
customCommands: [
{ command: "backup", description: "Git backup" },
{ command: "generate", description: "Create an image" },
],
},
},
}Rules: names are normalized (strip leading `/`, lowercase); valid pattern `a-z`, `0-9`, `_`, length 1-32; custom commands cannot override native commands; conflicts/duplicates are skipped and logged.
When Telegram menu limits require trimming, configured custom commands come first unless omitted per-skill entries are replaced by a leading `/skill` fallback.
Custom commands are menu entries only — they do not auto-implement behavior. Plugin/skill commands can still work when typed even if not shown in the Telegram menu. If native commands are disabled, built-ins are removed; custom/plugin commands may still register if configured.
Common setup failures:
- `setMyCommands failed` with `BOT_COMMANDS_TOO_MUCH` after a trim retry means the menu still overflows; reduce plugin/skill/custom commands or disable `channels.telegram.commands.native`.
- `apiRoot must be the Bot API root` means `channels.telegram.apiRoot` includes a full `/bot` endpoint. Run `openclaw doctor --fix` to remove that suffix before starting Telegram. Runtime requests use the repaired root; custom proxy paths remain supported.
- `getMe returned 401` means Telegram rejected the configured bot token. Update `botToken`, `tokenFile`, or `TELEGRAM_BOT_TOKEN` (default account) with the current BotFather token; OpenClaw stops before polling so this is not reported as a webhook cleanup failure.
- `setMyCommands failed` with network/fetch errors usually means outbound DNS/HTTPS to `api.telegram.org` is blocked.
### Device pairing commands (`device-pair` plugin)
When installed:
1. `/pair` generates a setup code
2. paste the code in the iOS app
3. `/pair pending` lists pending requests (including role/scopes)
4. approve: `/pair approve <requestId>`, `/pair approve` (only pending request), or `/pair approve latest`
If a device retries with changed auth details (role, scopes, public key), the previous pending request is superseded with a new `requestId`; re-run `/pair pending` before approving.
More detail: [Pairing](/agents/openclaw/channels/pairing/#pair-via-telegram).Reply threading tags
Explicit reply threading tags in generated output:
[[reply_to_current]]— replies to the triggering message[[reply_to:<id>]]— replies to a specific message ID
channels.telegram.replyToMode: off (default), first, all.
When reply threading is enabled and the original text/caption is available, OpenClaw adds a native quote excerpt automatically. Telegram caps native quote text at 1024 UTF-16 code units; longer messages are quoted from the start and fall back to a plain reply if Telegram rejects the quote.
off disables implicit reply threading only; explicit [[reply_to_*]] tags are still honored.
In partial streaming mode, a final reply that targets a different message replaces the preview instead of editing it in place. Telegram does not allow edits to change a message's reply target.
Ack reactions
ackReaction sends an acknowledgement emoji while OpenClaw processes an inbound message. messages.ackReactionScope decides when it is sent.
Emoji resolution order:
channels.telegram.accounts.<accountId>.ackReactionchannels.telegram.ackReactionmessages.ackReaction- agent identity emoji fallback (
agents.entries.*.identity.emoji, else "👀")
Telegram expects a unicode emoji (for example "👀"); use "" to disable the reaction for a channel or account.
Scope (messages.ackReactionScope, default "group-mentions"; no Telegram-account or Telegram-channel override):
all (DMs + groups, including ambient room events), direct (DMs only), group-all (every group message except ambient room events, no DMs), group-mentions (groups when the bot is mentioned; no DMs — default), off / none (disabled).
The default scope (group-mentions) does not fire ack reactions in DMs or ambient room events. Use direct or all for DMs; only all acknowledges ambient room events. Changes follow hot reload and apply to subsequent messages. Each assembled turn keeps its captured value.
Retained group history
Telegram groups and forum topics use a recent automatic context window plus explicit history reads. With requireMention: true, permitted unmentioned messages are recorded without starting agent turns. A later addressed turn receives recent context, and the agent can use message(action="read") when it needs earlier discussion.
channels.telegram.historyLimitormessages.groupChat.historyLimitcaps the automatic observed-message window (default 50, hard ceiling 200).0disables automatic history injection, not recording or explicit reads. The JSON integer maximum (9007199254740991) selects the 50-message default.- Automatic context examines a bounded recent slice before applying topic and sender permissions. A busy group can supply fewer than the configured number of messages when other topics or excluded senders dominate that slice. The agent can page farther back with explicit history reads.
- History reads stay within the authorized account, chat, and topic. They use Telegram message IDs for references and paging; omitting a topic must not expand a topic-scoped read to the whole group.
- Agent reads default to 50 messages per page, up to 100. Use
beforewith the returnedoldestMessageIdto read older messages,afterto read newer messages, ormessageIdfor an exact reference. These reads require the agent's authenticated current group/topic; they do not fetch Telegram server history. /newand/resetreset automatic session context, not the retained conversation. Explicit history reads can retrieve permitted earlier discussion.- The existing SQLite plugin-state table owns retained group messages. Successfully persisted records survive Gateway restarts and are not evicted by message count. Direct-message cache behavior remains bounded.
- On first use, existing group-cache records move atomically into retained storage. Legacy records without history-admission provenance, including embedded reply ancestors, remain available as explicit reply context within the existing depth and visibility limits; they are not treated as a verified conversation archive.
- History contains only messages OpenClaw received and was permitted to record. It cannot recover messages evicted before this feature, messages Telegram did not deliver, or messages from before the bot joined. Media references do not guarantee that attachment bytes remain available indefinitely.
No separate observation setting is needed. Keep Telegram group visibility enabled so the bot receives ordinary messages. Enabling history does not change explicit requireMention: false activation.
Older OpenClaw releases apply different cache and plugin-quota rules. Do not run them against expanded retained history. Downgrading requires a compatible pre-update backup; this feature does not add a database-version fence.
Limits and CLI targets
- `channels.telegram.textChunkLimit` default 4000; `streaming.chunkMode="newline"` prefers paragraph boundaries (blank lines) before length splitting.
- `channels.telegram.mediaMaxMb` (default 100) caps inbound and outbound media size.
- Inbound albums in one chat or topic reach the agent in arrival order, and an album waiting behind an earlier one keeps its delivery claim alive while the earlier album is still being processed.
- When an inbound attachment cannot be downloaded and the message proceeds to the agent, its body includes a `[media unavailable: ...]` notice. Oversize notices include the effective size limit; partial albums include the failed and total attachment counts. This also applies to admitted channel posts, even when their separate chat warning is suppressed.
- automatic group context uses `channels.telegram.historyLimit` or `messages.groupChat.historyLimit` (default 50); `0` disables the automatic window, not retained history.
- reply/quote/forward supplemental context normalizes into one selected conversation context window when the gateway has observed the parent messages; the observed-message cache lives in OpenClaw SQLite plugin state. To import pre-June cache sidecars, [upgrade through `2026.9.5`](/agents/openclaw/install/updating/#upgrading-very-old-versions) and run its Doctor first. Telegram only includes one shallow `reply_to_message` per update, so chains older than the cache are limited to that payload.
- Telegram allowlists primarily gate who can trigger the agent, not a full supplemental-context redaction boundary.
- DM history: `channels.telegram.dmHistoryLimit`, `channels.telegram.dms["<user_id>"].historyLimit`. Automatic observed-DM context defaults to 10 messages and is capped at 200; `0` disables that extra context, and the JSON integer maximum selects the default.
- These channel/account/DM fields also control embedded session transcript trimming in **user turns**, not observed messages. That separate limiter treats `0` as no trimming and keeps its existing eviction cushion. Doctor preserves valid saved limits, including the JSON integer maximum, so it does not silently change transcript context.
CLI and message-tool send targets accept a numeric chat ID, username, or forum topic target:openclaw message send --channel telegram --target 123456789 --message "hi"
openclaw message send --channel telegram --target @name --message "hi"
openclaw message send --channel telegram --target -1001234567890:topic:42 --message "hi topic"Polls use `openclaw message poll` and support forum topics:openclaw message poll --channel telegram --target 123456789 \
--poll-question "Ship it?" --poll-option "Yes" --poll-option "No"
openclaw message poll --channel telegram --target -1001234567890:topic:42 \
--poll-question "Pick a time" --poll-option "10am" --poll-option "2pm" \
--poll-duration-seconds 300 --poll-publicTelegram-only poll flags: `--poll-duration-seconds` (5-604800; up to seven days), `--poll-anonymous`, `--poll-public`, `--thread-id` (or a `:topic:` target). `--poll-option` repeats 2-12 times (Telegram's option cap).
Telegram send also supports `--presentation` with `buttons` blocks for inline keyboards (when `channels.telegram.capabilities.inlineButtons` allows it), `--pin` or `--delivery '{"pin":true}'` to request pinned delivery when the bot can pin in that chat, and `--force-document` to send outbound images, GIFs, and videos as documents instead of compressed/animated/video uploads.
Action gating: `channels.telegram.actions.sendMessage=false` disables all outbound messages including polls; `channels.telegram.actions.poll=false` disables poll creation while leaving regular sends enabled.