Telegram Channel Setup and Configuration for OpenClaw
Learn how to set up Telegram bots for OpenClaw using BotFather, configure tokens and DM policies, and start the gateway. Covers long polling and webhook options.
Read this when
- Working on Telegram features or webhooks
Production-ready for bot DMs and groups via grammY. Long polling is the default transport; webhook mode is optional.
-
Pairing, Telegram defaults to pairing as its DM policy.
-
Channel troubleshooting, Cross-channel diagnostics and repair playbooks.
-
Gateway configuration, Full channel config patterns and examples.
Quick setup
Create the bot token in BotFather
Both flows end with a token you paste into OpenClaw, pick one:
- Chat flow: open Telegram, chat with @BotFather (confirm the handle is exactly
@BotFather), run/newbot, follow the prompts, and save the token. - Web flow: open BotFather's web app, it runs in every Telegram client, including web.telegram.org, create the bot in the UI, and copy its token.
Configure token and DM policy
{
channels: {
telegram: {
enabled: true,
botToken: "123:abc",
dmPolicy: "pairing",
groups: { "*": { requireMention: true } },
},
},
}
Env fallback: TELEGRAM_BOT_TOKEN (default account only; named accounts must use botToken or tokenFile).
Telegram does not use openclaw channels login telegram; set the token in config/env, then start the gateway.
Start gateway and approve first DM
openclaw gateway
openclaw pairing list telegram
openclaw pairing approve telegram <CODE>
Pairing codes expire after 1 hour.
Add the bot to a group
Add the bot to your group, then get the two IDs group access needs:
- your Telegram user ID, for
allowFrom/groupAllowFrom - the Telegram group chat ID, as the key under
channels.telegram.groups
Get the group chat ID from openclaw logs --follow, a forwarded-ID bot, or Bot API getUpdates. After the group is allowed, /whoami@<bot_username> confirms the user and group IDs.
Negative supergroup IDs starting with -100 are group chat IDs. They go under channels.telegram.groups, not groupAllowFrom.
Note
Token resolution is account-aware:
tokenFilebeatsbotTokenbeats env, and config always wins overTELEGRAM_BOT_TOKEN(which only resolves for the default account). After a successful startup, OpenClaw caches the bot identity for up to 24 hours so restarts skip an extragetMecall; changing or removing the token clears that cache.
Telegram side settings
Privacy mode and group visibility
Telegram bots default to Privacy Mode, which limits which group messages they receive.
To see all group messages, either:
- disable privacy mode via
/setprivacy, or - make the bot a group admin.
After toggling privacy mode, remove and re-add the bot in each group so Telegram applies the change.
Group permissions
Admin status is controlled in Telegram group settings. Admin bots receive all group messages, useful for always-on group behavior.
Helpful BotFather toggles
/setjoingroups, allow/deny group adds/setprivacy, group visibility behavior
The same settings are available in BotFather's web app if you prefer a UI over chat commands.
Dashboard Mini App
The Dashboard Mini App opens the full OpenClaw Control UI as a Telegram WebApp. Run /dashboard in a DM with the bot, then tap Open dashboard. The command is registered automatically when the Telegram plugin is active; there is no separate Mini App flag.
Requirements:
gateway.tailscale.mode: "serve"or"funnel"for the published HTTPS Mini App URL.- Your numeric Telegram user ID must be in the selected account's effective
allowFromor incommands.ownerAllowFrom. Wildcards and usernames do not grant Mini App owner access. - Use a DM. In groups,
/dashboardreplies withopen this in a DM with the botand sends no button. - Docker installs: Serve/Funnel modes require the gateway to bind loopback next to
tailscaled, which bridge networking with published ports cannot satisfy. Run the gateway container withnetwork_mode: hostand mount the hosttailscaledsocket (/var/run/tailscale) plus thetailscaleCLI into the container.
Configure one of the supported Tailscale publishing modes:
{
gateway: {
tailscale: {
mode: "serve", // or "funnel"
},
},
}
OpenClaw automatically honors gateway.controlUi.basePath when building the Control UI and WebSocket URLs.
When the Mini App opens, Telegram provides signed WebApp initData. OpenClaw verifies its signature with the selected bot account's token, rejects missing, invalid, expired, or replayed data, extracts the numeric Telegram user ID, and checks owner access again before handing off to the Control UI.
If /dashboard cannot resolve a published HTTPS URL, it replies with:
Mini App needs an HTTPS gateway URL. Set `gateway.tailscale.mode: serve` or `funnel`, then retry.
Set one of the modes shown above, make sure Tailscale is running on the gateway host, and retry the command.
The Mini App is a Tailscale-only v1 path and does not support Telegram Web iframe.
Access control and activation
Group bot identity
In group chats and forum topics, mentioning the configured bot handle explicitly (for instance @my_bot) directs the message to the chosen OpenClaw agent, even if the agent's persona name does not match the Telegram username. The group silence rule still governs unrelated messages, yet the bot handle is never regarded as "someone else."
DM policy
channels.telegram.dmPolicy governs access to direct messages:
-
pairing(default) -
allowlist(needs at least one sender ID inallowFrom) -
open(requiresallowFromto contain"*") -
disabledWhen paired with
allowFrom: ["*"],dmPolicy: "open"enables any Telegram account that locates or deduces the bot username to issue commands to the bot. Reserve this for bots that are intentionally public and have tightly constrained tools; for a bot with a single owner, opt forallowlistwith numeric user IDs.Numeric Telegram user IDs are what
channels.telegram.allowFromaccepts. Prefixes liketelegram:andtg:are accepted and standardized. In setups with multiple accounts, a restrictive top-levelchannels.telegram.allowFromserves as a safety barrier: an account-levelallowFrom: ["*"]does not expose that account publicly unless the merged effective allowlist still includes an explicit wildcard. An emptyallowFromcombined withdmPolicy: "allowlist"blocks all DMs and fails config validation. Setup only requests numeric user IDs. If your configuration contains@usernameallowlist entries from a prior setup, executeopenclaw doctor --fixto convert them to numeric IDs (best-effort; a Telegram bot token is needed). If you depended on pairing-store allowlist files before,openclaw doctor --fixcan transfer entries intochannels.telegram.allowFromfor allowlist workflows (for example, whendmPolicy: "allowlist"has no explicit IDs yet).For single-owner bots, choose
dmPolicy: "allowlist"with explicit numericallowFromIDs rather than relying on earlier pairing approvals.A frequent misunderstanding: approving a DM pairing does not mean "this sender is authorized everywhere." Pairing only grants access to DMs. If no command owner exists yet, the first approved pairing also establishes
commands.ownerAllowFrom, giving owner-only commands and exec approvals a designated operator account. Authorization for group senders still comes from explicit config allowlists. To be authorized for both DMs and group commands with a single identity: place your numeric Telegram user ID inchannels.telegram.allowFrom, and for owner-only commands, verify thatcommands.ownerAllowFromcontainstelegram:<your user id>.The built-in tool policy for a single DM is set using
channels.telegram.direct.<chatId>.tools.toolsBySenderpicks a sender-specific policy based on a typed sender key likechannel:telegram:<userId>orid:<userId>:
{
channels: {
telegram: {
direct: {
"*": { tools: { deny: ["write", "edit"] } },
"603767951": { tools: {} },
},
},
},
}
A matching toolsBySender entry overrides tools for that DM. An exact chat entry replaces the entire "*" entry; it does not inherit wildcard fields. When present, account-level direct replaces the root direct map and inherits it only when omitted. The chosen direct policy, global policy, per-agent policy, tools.toolsBySender, and agents.<id>.tools.toolsBySender function as intersecting layers; a deny in any layer still blocks the tool. Codex relies on policy-filtered OpenClaw tools for explicitly restricted turns and retains its native tool surface for default profile narrowing. ACP-bound sessions reject a restrictive direct policy when their runtime cannot enforce it.
Finding your Telegram user ID
Safer (no third-party bot): DM your bot, run openclaw logs --follow, read from.id.
Official Bot API method:
curl "https://api.telegram.org/bot<bot_token>/getUpdates"
Third-party (less private): @userinfobot or @getidsbot.
Group policy and allowlists
Two controls operate together:
-
Which groups are allowed (
channels.telegram.groups)- no
groupsconfig,groupPolicy: "open": any group passes group-ID checks - no
groupsconfig,groupPolicy: "allowlist"(default): all groups blocked until you addgroupsentries (or"*") groupsconfigured: acts as an allowlist (explicit IDs or"*")
- no
-
Which senders are allowed in groups (
channels.telegram.groupPolicy)open/allowlist(default) /disabled
Group senders are filtered through groupAllowFrom; when it is absent, Telegram defaults to allowFrom rather than the pairing store, because group sender authentication never inherits DM pairing-store approvals, a security boundary in place since 2026.2.25.
Entries in groupAllowFrom must be numeric Telegram user IDs, with telegram: / tg: prefixes normalized; anything non-numeric is discarded. Avoid placing group or supergroup chat IDs here, since negative chat IDs belong under channels.telegram.groups.
In setups with multiple accounts, the root channels.telegram.groups serves as the shared default for any account lacking groups. An account-level groups map overrides the root map entirely for that account, with no deep merging. An explicit empty account map (groups: {}) keeps that account separate from the shared groups.
A typical one-owner bot pattern: put your user ID in channels.telegram.allowFrom, leave groupAllowFrom empty, and authorize the desired groups under channels.telegram.groups.
If channels.telegram is absent from the config entirely, runtime defaults to fail-closed groupPolicy="allowlist" unless channels.defaults.groupPolicy is explicitly provided.
Owner-only group setup:
{
channels: {
telegram: {
enabled: true,
dmPolicy: "pairing",
allowFrom: ["<YOUR_TELEGRAM_USER_ID>"],
groupPolicy: "allowlist",
groups: {
"<GROUP_CHAT_ID>": {
requireMention: true,
},
},
},
},
}
Test from the group using @<bot_username> ping. While requireMention: true is active, plain group messages will not activate the bot.
Allow any member in one specific group:
{
channels: {
telegram: {
groups: {
"-1001234567890": {
groupPolicy: "open",
requireMention: false,
},
},
},
},
}
Allow only specific users inside one specific group:
{
channels: {
telegram: {
groups: {
"-1001234567890": {
requireMention: true,
allowFrom: ["8734062810", "745123456"],
},
},
},
},
}
Warning
A frequent error:
groupAllowFromis not a group allowlist.
- Negative Telegram group/supergroup chat IDs (
-1001234567890) go underchannels.telegram.groups.- Telegram user IDs (
8734062810) go undergroupAllowFromto restrict which people within an allowed group can activate the bot.- Use
groupAllowFrom: ["*"]solely to permit any member of an allowed group to interact with the bot.
Mention behavior
Group replies need a mention by default. A mention can originate from:
-
a native
@botusernamemention, or -
a mention pattern in
agents.entries.*.groupChat.mentionPatternsormessages.groupChat.mentionPatternsSession-level toggles (state only, not persisted):
/activation always,/activation mention. For persistence, rely on config:
{
channels: {
telegram: {
groups: {
"*": { requireMention: false },
},
},
},
}
Group history context is always enabled and capped by historyLimit. Setting channels.telegram.historyLimit: 0 turns off the group history window. openclaw doctor --fix removes the deprecated includeGroupHistoryContext key.
To obtain the group chat ID: forward a group message to @userinfobot / @getidsbot, read chat.id from openclaw logs --follow, check the Bot API getUpdates, or (once the group is allowed) execute /whoami@<bot_username>.
Runtime behavior
- The gateway process hosts Telegram itself.
- Message routing follows a fixed rule: inbound Telegram traffic is answered back through Telegram, with no model involvement in channel selection.
- Incoming messages are converted into the standard channel envelope, carrying reply metadata, media placeholders, and stored reply-chain context for replies the gateway has observed.
- Group conversations are separated by group ID. Forum topics add
:topic:<threadId>to that separation. - When the bot enters an allowed group or supergroup, it sends a single introduction built from available room metadata: the group's title, description, and pinned message. Because the Telegram Bot API cannot access messages sent before the bot joined, introductions never claim to draw on prior chat history. Introductions are on by default, never trigger in private chats, and can be turned off with
channels.telegram.joinIntro: falseor set per account withchannels.telegram.accounts.<accountId>.joinIntro. Refer to group join introductions for details on once-per-room behavior and how untrusted content is handled. - DM messages may include
message_thread_id, which OpenClaw keeps for replies. DM topic sessions split only when TelegramgetMereportshas_topics_enabled: truefor the bot; otherwise DMs remain on the flat session. - Long polling relies on the grammY runner, sequencing per chat and per thread. Runner sink concurrency is set by
agents.defaults.maxConcurrent. - Multi-account startup limits concurrent
getMeprobes so large bot fleets do not launch every account probe simultaneously. - Each gateway process guards long polling so only one active poller can use a bot token at any moment. Persistent
getUpdates409 conflicts indicate another OpenClaw gateway, script, or external poller is using the same token. - The polling watchdog restarts after 120 seconds without completed
getUpdatesliveness. - Telegram Bot API lacks read-receipt support, so
sendReadReceiptsdoes not apply.
Note
Upgrade note: Telegram's default preview changed. When
channels.telegram.streamingis not set, Telegram now holds one editable status draft during the turn (the agent's current status plus its tool lines) and delivers the final answer as a regular message. Previously it streamed the answer text directly into the preview. No configuration becomes invalid and nodoctor --fixis required; to restore the old behavior, set:{ channels: { telegram: { streaming: { mode: "partial" } } } }
Note
channels.telegram.dm.threadRepliesandchannels.telegram.direct.<chatId>.threadReplieshave been removed. After upgrading, runopenclaw doctor --fixif your config still contains those keys. DM topic routing now follows TelegramgetMe.has_topics_enabled(governed by BotFather threaded mode): topics-enabled bots use thread-scoped DM sessions when Telegram sendsmessage_thread_id; other DMs stay on the flat session.
Feature reference
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.
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 for tool progress, shows the stable status label when answer activity arrives before tool progress, clears it at completion, and sends the final answer as a normal messagestreaming.preview.toolProgresscontrols whether tool/progress updates reuse the same edited preview message (default:truewhen preview streaming is active)streaming.preview.commandTextcontrols command/exec detail inside those lines:status(default, tool label only) orraw(explicit command text)streaming.progress.commentary(default:false) opts into assistant commentary/preamble text in the temporary progress draft- 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). Telegram keeps these on by default (matches released behavior from v2026.4.22+).
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 shows tool progress without editing the final answer into that message. 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.
Note
Selected quote replies are the exception. When
replyToModeisfirst,all, orbatchedand the inbound message has selected quote text, OpenClaw sends the final answer through Telegram's native quote-reply path instead of editing the answer preview, sostreaming.preview.toolProgresscannot show status lines that turn. Current-message replies without selected quote text still stream. SetreplyToMode: "off"when tool-progress visibility matters more than native quote replies, orstreaming.preview.toolProgress: falseto accept that trade-off.
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. If a turn cannot use previews, inherited block delivery still applies.
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.
Rich message formatting
Outbound text uses standard Telegram HTML messages by default, readable across current clients: bold, italic, links, code, spoilers, quotes, not Bot API 10.2 rich-only blocks (native tables, details, rich media, formulas).
Opt into Bot API 10.2 rich messages:
{
channels: {
telegram: {
richMessages: true,
},
},
}
When this option is turned on, the agent learns that rich messages are supported for this bot or account, following the documented Markdown plus HTML-island authoring contract. Markdown text is rendered through OpenClaw's Markdown IR as typed Bot API 10.2 rich blocks, which cover headings, tables, details, checklists, rich media, formulas, maps, and collages. Media captions continue to use Telegram HTML captions, since rich messages do not take the place of captions, and captions are limited to 1024 characters.
Model text is thereby kept away from Telegram's rich-Markdown sigils, so something like $400-600K will not be interpreted as math. Long rich text is split automatically to fit within Telegram's limits. Tables exceeding the 20-column limit revert to a code block.
The default is off for client compatibility reasons. Some current Desktop, Web, Android, and third-party clients show accepted rich messages as unsupported. Leave this off unless every client used with the bot can render them. /status indicates whether rich messages are currently active for the session.
Link previews are enabled by default. Automatic entity detection for rich text can be turned off with channels.telegram.linkPreview: false.
Native commands and custom commands
At startup, Telegram's command menu is registered through setMyCommands. Native commands for Telegram are enabled with commands.native: "auto".
Custom command menu entries can be added as follows:
{
channels: {
telegram: {
customCommands: [
{ command: "backup", description: "Git backup" },
{ command: "generate", description: "Create an image" },
],
},
},
}
The rules are: names get normalized by stripping a leading / and lowercasing; the valid pattern is a-z, 0-9, _, with a length of 1 to 32; custom commands cannot override native ones; conflicts and duplicates are skipped and logged.
When trimming is needed due to Telegram menu limits, configured custom commands take priority, unless omitted per-skill entries are replaced by a leading /skill fallback.
Custom commands only appear as menu entries, they do not implement behavior automatically. Plugin or skill commands can still function when typed, even if they are not shown in the Telegram menu. With native commands disabled, built-ins are removed, while custom or plugin commands may still register if configured.
Common setup issues:
- When
setMyCommands failedruns withBOT_COMMANDS_TOO_MUCHafter a trim retry, the menu is still overflowing; reduce plugin, skill, or custom commands, or disablechannels.telegram.commands.native. - If
deleteWebhook,deleteMyCommands, orsetMyCommandsfail with404: Not Foundwhile direct Bot API curl commands work,channels.telegram.apiRootwas likely set to the full/bot<TOKEN>endpoint.apiRootshould be only the Bot API root, andopenclaw doctor --fixremoves an accidental trailing/bot<TOKEN>. getMe returned 401signals that Telegram rejected the configured bot token. UpdatebotToken,tokenFile, orTELEGRAM_BOT_TOKEN(the default account) with the current BotFather token. OpenClaw halts before polling, so this is not reported as a webhook cleanup failure.setMyCommands failedcombined with network or fetch errors usually points to outbound DNS or HTTPS toapi.telegram.orgbeing blocked.
Device pairing commands (device-pair plugin)
Once installed:
- A setup code is generated by
/pair - paste the code into the iOS app
- pending requests, including role and scopes, are listed by
/pair pending - approve with
/pair approve <requestId>,/pair approve(only for a pending request), or/pair approve latest
If a device retries with changed auth details such as role, scopes, or public key, the previous pending request is superseded by a new requestId; run /pair pending again before approving.
Further details are available under Pairing.
Inline buttons
Inline keyboard scope configuration:
{
channels: {
telegram: {
capabilities: {
inlineButtons: "allowlist",
},
},
},
}
Per-account override:
{
channels: {
telegram: {
accounts: {
main: {
capabilities: {
inlineButtons: "allowlist",
},
},
},
},
},
}
The scopes are off, dm, group, all, and allowlist (the default). Legacy capabilities: ["inlineButtons"] is mapped to "all".
An account with capabilities: [] inherits the channel capabilities. To explicitly disable inline buttons, use capabilities: { inlineButtons: "off" }.
For a single select question, ask_user relies on these native controls. Each choice occupies one row, and Other⦠opens Telegram's reply input.
Message action example:
{
action: "send",
channel: "telegram",
to: "123456789",
message: "Choose an option:",
presentation: {
blocks: [
{
type: "buttons",
buttons: [
{ label: "Yes", action: { type: "callback", value: "yes" }, style: "success" },
{ label: "No", action: { type: "callback", value: "no" }, style: "danger" },
{ label: "Cancel", action: { type: "callback", value: "cancel" } },
],
},
],
},
}
Mini App button example:
{
action: "send",
channel: "telegram",
to: "123456789",
message: "Open app:",
presentation: {
blocks: [
{
type: "buttons",
buttons: [
{
label: "Launch",
action: { type: "web-app", url: "https://example.com/app" },
},
],
},
],
},
}
Mini App buttons function only in private chats between a user and the bot.
Callback action values that no registered plugin interactive handler claims are forwarded to the agent as text: callback_data: <value>.
Telegram message actions for agents and automation
Actions:
-
sendMessage(to,content, optionalmediaUrl,replyToMessageId,messageThreadId) -
react(chatId,messageId,emoji) -
emoji-list(optionalchatId,limit) -
deleteMessage(chatId,messageId) -
editMessage(chatId,messageId,contentorcaption, optionalpresentationinline buttons; button-only edits update reply markup) -
createForumTopic(chatId,name, optionaliconColor,iconCustomEmojiId)Convenience shortcuts:
send,react,delete,edit,sticker,sticker-search,topic-create.Access control:
channels.telegram.actions.sendMessage,deleteMessage,reactions,sticker(default: off).reactionsgoverns bothreactandemoji-list.edit,createForumTopic, andeditForumTopicstart enabled and lack a separate switch. Runtime dispatches rely on the config/secrets snapshot captured at startup or reload, so action paths do not fetchSecretRefvalues anew for each send.Use
emoji-listto query reactions in the current trusted chat and account. Agents lack access to other chats; direct operators can supply a differentchatId.limitis capped at 100 and defaults to that value:
{
"ok": true,
"emojis": [
{ "name": "π", "identifier": "π" },
{ "identifier": "5368324170671202286", "type": "custom_emoji" }
]
}
Hand a Unicode identifier or numeric custom emoji identifier straight to react. Chats without reaction limits return the standard Telegram reactions along with a note noting that all standard reactions are permitted. If Telegram rejects a reaction and the chat's permitted Unicode reactions are known, the error carries a brief sample of valid options.
How reaction removal works: /tools/reactions.
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 active and the original text/caption exists, OpenClaw inserts a native quote excerpt automatically. Telegram limits native quote text to 1024 UTF-16 code units; longer messages are quoted from the start and revert to a plain reply if Telegram rejects the quote.
off turns off implicit reply threading alone; explicit [[reply_to_*]] tags continue to be respected.
Forum topics and thread behavior
Forum supergroups: topic session keys append :topic:<threadId>; replies and typing go to the topic thread; the topic config path is channels.telegram.groups.<chatId>.topics.<threadId>.
The general topic (threadId=1) is handled specially: message sends drop message_thread_id (Telegram rejects sendMessage(...thread_id=1) with "thread not found"), yet typing actions still carry message_thread_id (needed in practice for the typing indicator to show up).
Topic entries pick up group settings unless they are overridden (requireMention, allowFrom, skills, systemPrompt, enabled, groupPolicy). agentId applies only to topics and does not fall back to group defaults. topics."*" establishes defaults for every topic in that group; exact topic IDs still take precedence over "*".
Per-topic agent routing: each topic can point to a different agent using agentId in the topic config, giving it its own workspace, memory, and session:
{
channels: {
telegram: {
groups: {
"-1001234567890": {
topics: {
"1": { agentId: "main" }, // General topic -> main agent
"3": { agentId: "zu" }, // Dev topic -> zu agent
"5": { agentId: "coder" } // Code review -> coder agent
}
}
}
}
}
}
Each topic then gets its own session key, for example agent:zu:telegram:group:-1001234567890:topic:3.
Persistent ACP topic binding: forum topics can attach ACP harness sessions via top-level typed bindings (bindings[] with type: "acp", match.channel: "telegram", peer.kind: "group", and a topic-qualified id like -1001234567890:topic:42). Currently limited to forum topics in groups/supergroups. See ACP Agents.
Thread-bound ACP spawn from chat: /acp spawn <agent> --thread here|auto ties the current topic to a new ACP session; follow-ups route there directly, and OpenClaw pins the spawn confirmation in-topic. Governed by session.threadBindings.spawnSessions (default: true).
Template context exposes MessageThreadId and IsForum. DM chats with message_thread_id retain reply metadata but only use thread-aware session keys when Telegram getMe reports has_topics_enabled: true.
The retired dm.threadReplies and direct.*.threadReplies overrides are no longer present; BotFather threaded mode is the sole authority. Run openclaw doctor --fix to clear stale config keys.
Audio, video, and stickers
Audio messages
Telegram separates voice notes from audio files. Default: audio-file behavior; tag [[audio_as_voice]] in the agent reply to force a voice-note send. Inbound voice-note transcripts are framed as machine-generated, untrusted text in agent context, but mention detection still uses the raw transcript so mention-gated voice messages keep working.
{
action: "send",
channel: "telegram",
to: "123456789",
media: "https://example.com/voice.ogg",
asVoice: true,
}
Video messages
Telegram separates video files from video notes. Video notes do not support captions; provided message text sends separately.
{
action: "send",
channel: "telegram",
to: "123456789",
media: "https://example.com/video.mp4",
asVideoNote: true,
}
Locations and venues
Use the existing send action with one standalone location object. Coordinates send a native pin; adding both name and address sends a native venue card. Location sends cannot be combined with message text or media.
{
action: "send",
channel: "telegram",
to: "123456789",
location: {
latitude: 48.858844,
longitude: 2.294351,
accuracy: 12,
name: "Eiffel Tower",
address: "Champ de Mars, Paris",
},
}
Stickers
Inbound: static WEBP is downloaded and processed (placeholder <media:sticker>); animated TGS and video WEBM are skipped.
Sticker context fields: Sticker.emoji, Sticker.setName, Sticker.fileId, Sticker.fileUniqueId, Sticker.cachedDescription. Descriptions are cached in OpenClaw SQLite plugin state to reduce repeated vision calls.
Enable sticker actions:
{
channels: {
telegram: {
actions: {
sticker: true,
},
},
},
}
Send:
{
action: "sticker",
channel: "telegram",
to: "123456789",
fileId: "CAACAgIAAxkBAAI...",
}
Search cached stickers:
{
action: "sticker-search",
channel: "telegram",
query: "cat waving",
limit: 5,
}
Reaction notifications
Reaction data from Telegram arrives through message_reaction updates, which are distinct from the payloads of regular messages. Once this feature is active, OpenClaw queues system events such as Telegram reaction added: π by Alice (@alice) on msg 42.
channels.telegram.reactionNotifications:off | own | all(defaults toown)channels.telegram.reactionLevel:off | ack | minimal | extensive(defaults tominimal)
With own, only reactions that users send to messages posted by the bot are captured, and this operates on a best-effort basis using a cache of sent messages. Reaction events continue to honor Telegram's access controls (dmPolicy, allowFrom, groupPolicy, groupAllowFrom); any sender who is not authorized gets filtered out.
Topic metadata is absent from reaction updates as provided by Telegram. Standard groups that are not forums stay scoped to the chat level. For reactions in forum channels and channel Direct Messages, the originating topic is retrieved from OpenClaw's bounded message cache, which is keyed by account, chat, and message ID. This means topic config, topic agents, and conversation bindings remain applicable. Should the cached topic be missing or fall outside the correct scope, OpenClaw drops the reaction notification and records a warning rather than defaulting to General or the base chat.
For polling or webhook setups, allowed_updates automatically includes message_reaction.
Ack reactions
While OpenClaw handles an incoming message, ackReaction transmits a confirmation emoji. The timing of that transmission is governed by messages.ackReactionScope.
How the emoji is chosen:
channels.telegram.accounts.<accountId>.ackReactionchannels.telegram.ackReactionmessages.ackReaction- fallback to the agent identity emoji (
agents.entries.*.identity.emoji, otherwise "π")
A unicode emoji, such as "π", is what Telegram expects; to turn off the reaction for a specific channel or account, set "".
Scope (messages.ackReactionScope, with "group-mentions" as the default; no per-account or per-channel Telegram override exists at this time):
all applies to DMs and groups, including ambient room events; direct covers only DMs; group-all targets every group message except ambient room events, leaving out DMs; group-mentions handles groups where the bot is mentioned, and DMs are excluded, which is the default; off and none both disable the feature.
Note
With the default scope (
group-mentions), ack reactions are not sent in DMs or for ambient room events. To cover DMs, usedirectorall; ambient room events are acknowledged only byall. Since this setting is read when the Telegram provider starts, a gateway restart is required for any change to take effect.
Config writes from Telegram events and commands
Channel config writes are turned on by default (configWrites !== false). Writes that Telegram triggers include group migration events (migrate_to_chat_id, which update channels.telegram.groups) and /config set or /config unset, the latter requiring command enablement.
To disable:
{
channels: {
telegram: {
configWrites: false,
},
},
}
Long polling vs webhook
Long polling is the default mode. For webhook operation, configure channels.telegram.webhookUrl and channels.telegram.webhookSecret; optionally set webhookPath (defaults to /telegram-webhook), webhookHost (defaults to 127.0.0.1), webhookPort (defaults to 8787), and webhookCertPath for a self-signed cert PEM when using a direct IP or no domain.
The listener reserves /healthz for health checks, so webhookPath has to be placed on a different route. If an existing deployment already uses /healthz, pick another route, update the path in webhookUrl along with the reverse proxy mapping, and then restart OpenClaw.
When long polling is active, OpenClaw saves its restart watermark only after an update dispatches successfully. A handler that fails leaves that update retryable within the same process instead of being marked as done.
By default, the local listener binds to 127.0.0.1:8787. For public access, place a reverse proxy in front of the local port, or deliberately set webhookHost: "0.0.0.0".
Webhook mode checks request guards, the Telegram secret token, and the JSON body before committing the update to its durable ingress queue, then returns an empty 200. A successful durable adoption includes x-openclaw-delivery-accepted: durable; this header is absent from health, routing, authentication, validation, and storage-error responses. Reverse proxies and host controllers can require this header to tell OpenClaw adoption apart from a generic empty 200, without relying on response timing to infer acceptance.
Once the durable write completes, OpenClaw claims and processes updates through the core channel-ingress drain, which uses per-chat and per-topic lanes, completes at turn adoption, and applies a pre-adoption stall timeout. Slow agent turns never hold Telegram's delivery ACK.
Limits and CLI targets
-
channels.telegram.textChunkLimitdefaults to 4000;streaming.chunkMode="newline"prefers paragraph boundaries, meaning blank lines, before splitting by length. -
channels.telegram.mediaMaxMb(default 100) limits the size of inbound and outbound media. -
If an inbound attachment cannot be downloaded and the message still reaches the agent, its body carries a
[media unavailable: ...]notice. Oversize notices state the effective size limit; partial albums list the failed and total attachment counts. This rule also applies to admitted channel posts, even when their separate chat warning is suppressed. -
Group context history uses
channels.telegram.historyLimitormessages.groupChat.historyLimit(default 50);0turns it off. -
Reply, quote, and forward supplemental context merges into one selected conversation context window when the gateway has seen the parent messages; the observed-message cache lives in OpenClaw SQLite plugin state, and
openclaw doctor --fiximports legacy sidecars. Telegram includes only one shallowreply_to_messageper update, so chains older than the cache are limited to that payload. -
Telegram allowlists mainly control who can trigger the agent, not a full supplemental-context redaction boundary.
-
DM history:
channels.telegram.dmHistoryLimit,channels.telegram.dms["<user_id>"].historyLimit.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-public
Telegram-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, which is 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, or 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.
Exec approvals in Telegram
Telegram supports exec approvals in approver DMs and can optionally post prompts in the originating chat or topic. Approvers must be numeric Telegram user IDs.
channels.telegram.execApprovals.enabled("auto"enables when at least one approver is resolvable)channels.telegram.execApprovals.approvers(falls back to numeric owner IDs fromcommands.ownerAllowFrom)channels.telegram.execApprovals.target:dm(default) |channel|bothagentFilter,sessionFilter
channels.telegram.allowFrom, groupAllowFrom, and defaultTo control who can talk to the bot and where it sends normal replies; they do not grant exec approver status. The first approved DM pairing bootstraps commands.ownerAllowFrom when no command owner exists yet, so one-owner setups work without duplicating IDs under execApprovals.approvers.
Channel delivery shows the command text in the chat; only enable channel or both in trusted groups or topics. When the prompt lands in a forum topic, OpenClaw preserves the topic for the approval prompt and follow-up. Exec approvals expire after 30 minutes by default.
Inline approval buttons also require channels.telegram.capabilities.inlineButtons to allow the target surface (dm, group, or all). Approval IDs prefixed with plugin: resolve through plugin approvals; others resolve through exec approvals first.
See Exec approvals.
Error reply controls
When the agent hits a delivery or provider error, the error policy controls whether error messages reach the Telegram chat:
| Key | Values | Default | Description |
|---|---|---|---|
channels.telegram.errorPolicy | always, once, silent | always | always forwards every error notification to the chat. once delivers each distinct error only once within the built-in cooldown period. silent suppresses error notifications entirely. |
Overrides at the per-account, per-group, and per-topic levels follow the same inheritance rules as the other Telegram configuration keys.
{
channels: {
telegram: {
errorPolicy: "always",
groups: {
"-1001234567890": {
errorPolicy: "silent", // suppress errors in this group
},
},
},
},
}
Troubleshooting
Bot does not respond to non mention group messages
- Full visibility is required from Telegram privacy mode when
requireMention=falseis active: turn off the setting in BotFather via/setprivacy, then remove the bot from the group and add it back. - A warning is issued by
openclaw channels statuswhen the configuration anticipates group messages that are not being sent. - Explicit numeric group IDs are validated by
openclaw channels status --probe; the wildcard"*"cannot be checked for membership. - To test a session quickly, use
/activation always.
Bot not seeing group messages at all
- When
channels.telegram.groupsis present, the group must appear in the list or be covered by"*". - Confirm that the bot is a member of the group.
- Look at
openclaw logs --followto understand why items were skipped.
Commands work partially or not at all
- Your sender identity must be authorized, either through pairing or a numeric
allowFrom; command authorization remains in effect even when the group policy isopen. - A native menu with too many entries results when
setMyCommands failedis combined withBOT_COMMANDS_TOO_MUCH; trim plugin, skill, or custom commands, or turn off native menus. - Startup calls from
deleteMyCommands/setMyCommandsand typing calls fromsendChatActionare rate-limited and make one retry through Telegram's transport fallback when a request times out. Ongoing network or fetch failures usually point to DNS or HTTPS problems reachingapi.telegram.org.
Startup reports unauthorized token
- An auth failure for the configured bot token is what
getMe returned 401indicates. Regenerate or recopy the token in BotFather, then refreshchannels.telegram.botToken,tokenFile,accounts.<id>.botToken, orTELEGRAM_BOT_TOKEN(the default account). - An auth failure also appears as
deleteWebhook 401 Unauthorizedduring startup; treating it as "no webhook exists" would only postpone the same bad-token error to a later API call.
Polling or network instability
- Immediate abort behavior can appear on Node 22+ with a custom fetch or proxy when
AbortSignaltypes do not match. - Some hosts resolve
api.telegram.orgto IPv6 first; broken IPv6 egress causes sporadic API failures. - Logs containing
TypeError: fetch failedorNetwork request for 'getUpdates' failed!are treated as recoverable network errors and retried. - During polling startup, OpenClaw reuses the successful startup
getMeprobe for grammY, so the runner avoids a secondgetMebefore the firstgetUpdates. - If
deleteWebhookhits a transient network error during polling startup, OpenClaw moves straight into long polling without another pre-poll control-plane call. A webhook that is still active then shows up as agetUpdatesconflict; OpenClaw rebuilds the transport and retries webhook cleanup. - When
Polling stall detectedappears in logs, OpenClaw restarts polling and rebuilds the transport after 120 seconds without completed long-poll liveness by default. openclaw channels status --probeandopenclaw doctorwarn when a running polling account has not completedgetUpdatesafter the startup grace period, a running webhook account has not completedsetWebhookafter the startup grace period, or the most recent successful polling transport activity is stale.- Process proxy env is honored by Telegram for Bot API transport:
HTTP_PROXY,HTTPS_PROXY,ALL_PROXY, and lowercase variants.NO_PROXY/no_proxycan still bypassapi.telegram.org. - If
OPENCLAW_PROXY_URLis configured for a service environment and no standard proxy env is present, Telegram also uses that URL for Bot API transport. - On VPS hosts where direct egress or TLS is unstable, direct Telegram API traffic through a proxy:
channels:
telegram:
proxy: socks5://<user>:<password>@proxy-host:1080
- On Node 22 and later,
autoSelectFamily=trueis the default, with WSL2 being the exception. Telegram's DNS result ordering gives priority toOPENCLAW_TELEGRAM_DNS_RESULT_ORDER, thenchannels.telegram.network.dnsResultOrder, and finally the process default (such asNODE_OPTIONS=--dns-result-order=ipv4first); if none of these apply, Node 22+ falls back toipv4first. - When running under WSL2, or when an IPv4-only setup proves more effective, you can enforce the family choice:
channels:
telegram:
network:
autoSelectFamily: false
- Media downloads from Telegram already permit RFC 2544 benchmark-range responses (
198.18.0.0/15) out of the box. Should a trusted fake-IP or transparent proxy mapapi.telegram.orgto a different private, internal, or special-use address during these downloads, you can enable the Telegram-only bypass:
channels:
telegram:
network:
dangerouslyAllowPrivateNetwork: true
- The same opt-in exists per account via
channels.telegram.accounts.<accountId>.network.dangerouslyAllowPrivateNetwork. - If your proxy resolves Telegram media hosts to
198.18.x.x, keep the risky flag disabled initially, since that range is permitted by default already.
Warning
Turning on
channels.telegram.network.dangerouslyAllowPrivateNetworkreduces SSRF protections for Telegram media. Only activate it for trusted, operator-controlled proxy environments (Clash, Mihomo, Surge fake-IP routing) that generate private or special-use answers outside the RFC 2544 benchmark range. For ordinary public internet Telegram use, leave it disabled.
- Temporary environment overrides are available through
OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY=1,OPENCLAW_TELEGRAM_ENABLE_AUTO_SELECT_FAMILY=1, andOPENCLAW_TELEGRAM_DNS_RESULT_ORDER=ipv4first. - Verify DNS responses with:
dig +short api.telegram.org A
dig +short api.telegram.org AAAA
Additional guidance: Channel troubleshooting.
Configuration reference
Main reference: Configuration reference - Telegram.
High-signal Telegram fields
- startup/auth:
enabled,botToken,tokenFile(only regular files qualify; symlinks are not permitted),accounts.* - access control:
dmPolicy,allowFrom,direct.*.tools,direct.*.toolsBySender,groupPolicy,groupAllowFrom,groups,groups.*.topics.*, top-levelbindings[](type: "acp") - group introductions:
joinIntro,accounts.*.joinIntro(defaults totrue) - topic defaults:
groups.<chatId>.topics."*"handles unmatched forum topics; exact topic IDs take precedence - exec approvals:
execApprovals,accounts.*.execApprovals - command/menu:
commands.native,commands.nativeSkills,customCommands - threading/replies:
replyToMode,threadBindings - streaming:
streaming(optionsoff | partial | block | progress),streaming.preview.toolProgress - formatting/delivery:
textChunkLimit,streaming.chunkMode,richMessages,markdown.tables(off | bullets | code | block),linkPreview,responsePrefix - media/network:
mediaMaxMb,network.autoSelectFamily,network.dangerouslyAllowPrivateNetwork,proxy - custom API root:
apiRoot(exclusively for Bot API root; omit/bot<TOKEN>),trustedLocalFileRoots(self-hosted Bot API absolutefile_pathroots) - webhook:
webhookUrl,webhookSecret,webhookPath,webhookHost,webhookPort,webhookCertPath - actions/capabilities:
capabilities.inlineButtons,actions.sendMessage|editMessage|deleteMessage|reactions|sticker|createForumTopic|editForumTopic - reactions:
reactionNotifications,reactionLevel - errors:
errorPolicy,silentErrorReplies - writes/history:
configWrites,historyLimit,dmHistoryLimit,dms.*.historyLimit
Note
When multiple account IDs are present, set
channels.telegram.defaultAccount(or addchannels.telegram.accounts.default) to clarify default routing. If this is not done, OpenClaw selects the first normalized account ID andopenclaw doctorissues a warning. Accounts that omitdmPolicy,groupPolicy,allowFrom, andgroupAllowFromfall back to the channel root rather thanaccounts.default.*. Policies set explicitly for an account take precedence; when neither scope defines them, DMs rely onpairingand groups onallowlist.
Related
-
Pairing, Link a Telegram user to the gateway.
-
Groups, Allowlist rules for groups and topics.
-
Channel routing, Direct inbound messages to agents.
-
Security, Threat model and hardening measures.
-
Multi-agent routing, Assign groups and topics to agents.
-
Troubleshooting, Diagnostics across channels.