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.

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: tokenFile beats botToken beats env, and config always wins over TELEGRAM_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 extra getMe call; 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 allowFrom or in commands.ownerAllowFrom. Wildcards and usernames do not grant Mini App owner access.
  • Use a DM. In groups, /dashboard replies with open this in a DM with the bot and 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 with network_mode: host and mount the host tailscaled socket (/var/run/tailscale) plus the tailscale CLI 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 in allowFrom)

  • open (requires allowFrom to contain "*")

  • disabled

    When 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 for allowlist with numeric user IDs.

    Numeric Telegram user IDs are what channels.telegram.allowFrom accepts. Prefixes like telegram: and tg: are accepted and standardized. In setups with multiple accounts, a restrictive top-level channels.telegram.allowFrom serves as a safety barrier: an account-level allowFrom: ["*"] does not expose that account publicly unless the merged effective allowlist still includes an explicit wildcard. An empty allowFrom combined with dmPolicy: "allowlist" blocks all DMs and fails config validation. Setup only requests numeric user IDs. If your configuration contains @username allowlist entries from a prior setup, execute openclaw doctor --fix to convert them to numeric IDs (best-effort; a Telegram bot token is needed). If you depended on pairing-store allowlist files before, openclaw doctor --fix can transfer entries into channels.telegram.allowFrom for allowlist workflows (for example, when dmPolicy: "allowlist" has no explicit IDs yet).

    For single-owner bots, choose dmPolicy: "allowlist" with explicit numeric allowFrom IDs 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 in channels.telegram.allowFrom, and for owner-only commands, verify that commands.ownerAllowFrom contains telegram:<your user id>.

    The built-in tool policy for a single DM is set using channels.telegram.direct.<chatId>.tools. toolsBySender picks a sender-specific policy based on a typed sender key like channel:telegram:<userId> or id:<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:

  1. Which groups are allowed (channels.telegram.groups)

    • no groups config, groupPolicy: "open": any group passes group-ID checks
    • no groups config, groupPolicy: "allowlist" (default): all groups blocked until you add groups entries (or "*")
    • groups configured: acts as an allowlist (explicit IDs or "*")
  2. 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: groupAllowFrom is not a group allowlist.

  • Negative Telegram group/supergroup chat IDs (-1001234567890) go under channels.telegram.groups.
  • Telegram user IDs (8734062810) go under groupAllowFrom to 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 @botusername mention, or

  • a mention pattern in agents.entries.*.groupChat.mentionPatterns or messages.groupChat.mentionPatterns

    Session-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: false or set per account with channels.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 Telegram getMe reports has_topics_enabled: true for 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 getMe probes 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 getUpdates 409 conflicts indicate another OpenClaw gateway, script, or external poller is using the same token.
  • The polling watchdog restarts after 120 seconds without completed getUpdates liveness.
  • Telegram Bot API lacks read-receipt support, so sendReadReceipts does not apply.

Note

Upgrade note: Telegram's default preview changed. When channels.telegram.streaming is 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 no doctor --fix is required; to restore the old behavior, set:

{ channels: { telegram: { streaming: { mode: "partial" } } } }

Note

channels.telegram.dm.threadReplies and channels.telegram.direct.<chatId>.threadReplies have been removed. After upgrading, run openclaw doctor --fix if your config still contains those keys. DM topic routing now follows Telegram getMe.has_topics_enabled (governed 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.

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.streaming is off | partial | block | progress (default: progress); set mode: "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
  • progress keeps 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 message
  • streaming.preview.toolProgress controls whether tool/progress updates reuse the same edited preview message (default: true when preview streaming is active)
  • streaming.preview.commandText controls command/exec detail inside those lines: status (default, tool label only) or raw (explicit command text)
  • streaming.progress.commentary (default: false) opts into assistant commentary/preamble text in the temporary progress draft
  • legacy channels.telegram.streamMode, boolean streaming values, and retired native draft preview keys are detected; run openclaw doctor --fix to 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 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 instead of editing the answer preview, so streaming.preview.toolProgress cannot show status lines that turn. Current-message replies without selected quote text still stream. Set replyToMode: "off" when tool-progress visibility matters more than native quote replies, or streaming.preview.toolProgress: false to 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 failed runs with BOT_COMMANDS_TOO_MUCH after a trim retry, the menu is still overflowing; reduce plugin, skill, or custom commands, or disable channels.telegram.commands.native.
  • If deleteWebhook, deleteMyCommands, or setMyCommands fail with 404: Not Found while direct Bot API curl commands work, channels.telegram.apiRoot was likely set to the full /bot<TOKEN> endpoint. apiRoot should be only the Bot API root, and openclaw doctor --fix removes an accidental trailing /bot<TOKEN>.
  • getMe returned 401 signals that Telegram rejected the configured bot token. Update botToken, tokenFile, or TELEGRAM_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 failed combined with network or fetch errors usually points to outbound DNS or HTTPS to api.telegram.org being blocked.

Device pairing commands (device-pair plugin)

Once installed:

  1. A setup code is generated by /pair
  2. paste the code into the iOS app
  3. pending requests, including role and scopes, are listed by /pair pending
  4. 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, optional mediaUrl, replyToMessageId, messageThreadId)

  • react (chatId, messageId, emoji)

  • emoji-list (optional chatId, limit)

  • deleteMessage (chatId, messageId)

  • editMessage (chatId, messageId, content or caption, optional presentation inline buttons; button-only edits update reply markup)

  • createForumTopic (chatId, name, optional iconColor, iconCustomEmojiId)

    Convenience shortcuts: send, react, delete, edit, sticker, sticker-search, topic-create.

    Access control: channels.telegram.actions.sendMessage, deleteMessage, reactions, sticker (default: off). reactions governs both react and emoji-list. edit, createForumTopic, and editForumTopic start enabled and lack a separate switch. Runtime dispatches rely on the config/secrets snapshot captured at startup or reload, so action paths do not fetch SecretRef values anew for each send.

    Use emoji-list to query reactions in the current trusted chat and account. Agents lack access to other chats; direct operators can supply a different chatId. limit is 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 to own)
  • channels.telegram.reactionLevel: off | ack | minimal | extensive (defaults to minimal)

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>.ackReaction
  • channels.telegram.ackReaction
  • messages.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, use direct or all; ambient room events are acknowledged only by all. 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.textChunkLimit defaults 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.historyLimit or messages.groupChat.historyLimit (default 50); 0 turns 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 --fix imports legacy sidecars. Telegram includes only one shallow reply_to_message per 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 from commands.ownerAllowFrom)
  • channels.telegram.execApprovals.target: dm (default) | channel | both
  • agentFilter, 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:

KeyValuesDefaultDescription
channels.telegram.errorPolicyalways, once, silentalwaysalways 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=false is 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 status when 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.groups is 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 --follow to 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 is open.
  • A native menu with too many entries results when setMyCommands failed is combined with BOT_COMMANDS_TOO_MUCH; trim plugin, skill, or custom commands, or turn off native menus.
  • Startup calls from deleteMyCommands / setMyCommands and typing calls from sendChatAction are 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 reaching api.telegram.org.

Startup reports unauthorized token

  • An auth failure for the configured bot token is what getMe returned 401 indicates. Regenerate or recopy the token in BotFather, then refresh channels.telegram.botToken, tokenFile, accounts.<id>.botToken, or TELEGRAM_BOT_TOKEN (the default account).
  • An auth failure also appears as deleteWebhook 401 Unauthorized during 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 AbortSignal types do not match.
  • Some hosts resolve api.telegram.org to IPv6 first; broken IPv6 egress causes sporadic API failures.
  • Logs containing TypeError: fetch failed or Network request for 'getUpdates' failed! are treated as recoverable network errors and retried.
  • During polling startup, OpenClaw reuses the successful startup getMe probe for grammY, so the runner avoids a second getMe before the first getUpdates.
  • If deleteWebhook hits 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 a getUpdates conflict; OpenClaw rebuilds the transport and retries webhook cleanup.
  • When Polling stall detected appears in logs, OpenClaw restarts polling and rebuilds the transport after 120 seconds without completed long-poll liveness by default.
  • openclaw channels status --probe and openclaw doctor warn when a running polling account has not completed getUpdates after the startup grace period, a running webhook account has not completed setWebhook after 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_proxy can still bypass api.telegram.org.
  • If OPENCLAW_PROXY_URL is 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=true is the default, with WSL2 being the exception. Telegram's DNS result ordering gives priority to OPENCLAW_TELEGRAM_DNS_RESULT_ORDER, then channels.telegram.network.dnsResultOrder, and finally the process default (such as NODE_OPTIONS=--dns-result-order=ipv4first); if none of these apply, Node 22+ falls back to ipv4first.
  • 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 map api.telegram.org to 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.dangerouslyAllowPrivateNetwork reduces 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, and OPENCLAW_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-level bindings[] (type: "acp")
  • group introductions: joinIntro, accounts.*.joinIntro (defaults to true)
  • 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 (options off | 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 absolute file_path roots)
  • 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 add channels.telegram.accounts.default) to clarify default routing. If this is not done, OpenClaw selects the first normalized account ID and openclaw doctor issues a warning. Accounts that omit dmPolicy, groupPolicy, allowFrom, and groupAllowFrom fall back to the channel root rather than accounts.default.*. Policies set explicitly for an account take precedence; when neither scope defines them, DMs rely on pairing and groups on allowlist.

7,083 words Β· updated Sep 1, 2026