Telegram (Bot API)

Status: production-ready for bot DMs + groups via grammY. Long polling is the default mode; webhook mode is optional.

Pairing Default DM policy for Telegram is pairing.

Channel troubleshooting Cross-channel diagnostics and repair playbooks.

Gateway configuration Full channel config patterns and examples.

Quick setup

Create the bot token in BotFather

Open Telegram and chat with **@BotFather** (confirm the handle is exactly `@BotFather`).

Run `/newbot`, follow prompts, and save the token.

Configure token and DM policy

json5
{
  channels: {
    telegram: {
      enabled: true,
      botToken: "123:abc",
      dmPolicy: "pairing",
      groups: { "*": { requireMention: true } },
    },
  },
}
Env fallback: `TELEGRAM_BOT_TOKEN=...` (default account only).

Start gateway and approve first DM

bash
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 set `channels.telegram.groups` and `groupPolicy` to match your access model.

INFO

Token resolution order is account-aware. In practice, config values win over env fallback, and TELEGRAM_BOT_TOKEN only applies to the default account.

Telegram side settings

Privacy mode and group visibility

Telegram bots default to **Privacy Mode**, which limits what group messages they receive.

If the bot must see all group messages, either:

- disable privacy mode via `/setprivacy`, or
- make the bot a group admin.

When toggling privacy mode, remove + 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, which is useful for always-on group behavior.

Helpful BotFather toggles

- `/setjoingroups` to allow/deny group adds
- `/setprivacy` for group visibility behavior

Access control and activation

DM policy

`channels.telegram.dmPolicy` controls direct message access:

- `pairing` (default)
- `allowlist`
- `open` (requires `allowFrom` to include `"*"`)
- `disabled`

`channels.telegram.allowFrom` accepts numeric Telegram user IDs. `telegram:` / `tg:` prefixes are accepted and normalized.
The onboarding wizard accepts `@username` input and resolves it to numeric IDs.

### Finding your Telegram user ID

Safer (no third-party bot):

1. DM your bot.
2. Run `openclaw logs --follow`.
3. Read `from.id`.

Official Bot API method:
bash
curl "https://api.telegram.org/bot<bot_token>/getUpdates"
Third-party method (less private): `@userinfobot` or `@getidsbot`.

Group policy and allowlists

There are two independent controls:

1. **Which groups are allowed** (`channels.telegram.groups`)
   - no `groups` config: all groups allowed
   - `groups` configured: acts as allowlist (explicit IDs or `"*"`)

2. **Which senders are allowed in groups** (`channels.telegram.groupPolicy`)
   - `open`
   - `allowlist` (default)
   - `disabled`

`groupAllowFrom` is used for group sender filtering. If not set, Telegram falls back to `allowFrom`.

Example: allow any member in one specific group:
json5
{
  channels: {
    telegram: {
      groups: {
        "-1001234567890": {
          groupPolicy: "open",
          requireMention: false,
        },
      },
    },
  },
}

Mention behavior

Group replies require mention by default.

Mention can come from:

- native `@botusername` mention, or
- mention patterns in:
  - `agents.list[].groupChat.mentionPatterns`
  - `messages.groupChat.mentionPatterns`

Session-level command toggles:

- `/activation always`
- `/activation mention`

These update session state only. Use config for persistence.

Persistent config example:
json5
{
  channels: {
    telegram: {
      groups: {
        "*": { requireMention: false },
      },
    },
  },
}
Getting the group chat ID:

- forward a group message to `@userinfobot` / `@getidsbot`
- or read `chat.id` from `openclaw logs --follow`
- or inspect Bot API `getUpdates`

Runtime behavior

  • Telegram is owned by the gateway process.
  • Routing is deterministic: Telegram inbound replies back to Telegram (the model does not pick channels).
  • Inbound messages normalize into the shared channel envelope with reply metadata and media placeholders.
  • Group sessions are isolated by group ID. Forum topics append :topic:\<threadId\> to keep topics isolated.
  • DM messages can carry message_thread_id; OpenClaw routes them with thread-aware session keys and preserves thread ID for replies.
  • Long polling uses grammY runner with per-chat/per-thread sequencing. Overall runner sink concurrency uses agents.defaults.maxConcurrent.
  • Telegram Bot API has no read-receipt support (sendReadReceipts does not apply).

Feature reference

Draft streaming in Telegram DMs

OpenClaw can stream partial replies with Telegram draft bubbles (`sendMessageDraft`).

Requirements:

- `channels.telegram.streamMode` is not `"off"` (default: `"partial"`)
- private chat
- inbound update includes `message_thread_id`
- bot topics are enabled (`getMe().has_topics_enabled`)

Modes:

- `off`: no draft streaming
- `partial`: frequent draft updates from partial text
- `block`: chunked draft updates using `channels.telegram.draftChunk`

`draftChunk` defaults for block mode:

- `minChars: 200`
- `maxChars: 800`
- `breakPreference: "paragraph"`

`maxChars` is clamped by `channels.telegram.textChunkLimit`.

Draft streaming is DM-only; groups/channels do not use draft bubbles.

If you want early real Telegram messages instead of draft updates, use block streaming (`channels.telegram.blockStreaming: true`).

Telegram-only reasoning stream:

- `/reasoning stream` sends reasoning to the draft bubble while generating
- final answer is sent without reasoning text

Formatting and HTML fallback

Outbound text uses Telegram `parse_mode: "HTML"`.

- Markdown-ish text is rendered to Telegram-safe HTML.
- Raw model HTML is escaped to reduce Telegram parse failures.
- If Telegram rejects parsed HTML, OpenClaw retries as plain text.

Link previews are enabled by default and can be disabled with `channels.telegram.linkPreview: false`.

Native commands and custom commands

Telegram command menu registration is handled at startup with `setMyCommands`.

Native command defaults:

- `commands.native: "auto"` enables native commands for Telegram

Add custom command menu entries:
json5
{
  channels: {
    telegram: {
      customCommands: [
        { command: "backup", description: "Git backup" },
        { command: "generate", description: "Create an image" },
      ],
    },
  },
}
Rules:

- names are normalized (strip leading `/`, lowercase)
- valid pattern: `a-z`, `0-9`, `_`, length `1..32`
- custom commands cannot override native commands
- conflicts/duplicates are skipped and logged

Notes:

- custom commands are menu entries only; they do not auto-implement behavior
- plugin/skill commands can still work when typed even if not shown in Telegram menu

If native commands are disabled, built-ins are removed. Custom/plugin commands may still register if configured.

Common setup failure:

- `setMyCommands failed` usually means outbound DNS/HTTPS to `api.telegram.org` is blocked.

### Device pairing commands (`device-pair` plugin)

When the `device-pair` plugin is installed:

1. `/pair` generates setup code
2. paste code in iOS app
3. `/pair approve` approves latest pending request

More details: [Pairing](/channels/pairing#pair-via-telegram-recommended-for-ios).

Inline buttons

Configure inline keyboard scope:
json5
{
  channels: {
    telegram: {
      capabilities: {
        inlineButtons: "allowlist",
      },
    },
  },
}
Per-account override:
json5
{
  channels: {
    telegram: {
      accounts: {
        main: {
          capabilities: {
            inlineButtons: "allowlist",
          },
        },
      },
    },
  },
}
Scopes:

- `off`
- `dm`
- `group`
- `all`
- `allowlist` (default)

Legacy `capabilities: ["inlineButtons"]` maps to `inlineButtons: "all"`.

Message action example:
json5
{
  action: "send",
  channel: "telegram",
  to: "123456789",
  message: "Choose an option:",
  buttons: [
    [
      { text: "Yes", callback_data: "yes" },
      { text: "No", callback_data: "no" },
    ],
    [{ text: "Cancel", callback_data: "cancel" }],
  ],
}
Callback clicks are passed to the agent as text:
`callback_data: \<value\>`

Telegram message actions for agents and automation

Telegram tool actions include:

- `sendMessage` (`to`, `content`, optional `mediaUrl`, `replyToMessageId`, `messageThreadId`)
- `react` (`chatId`, `messageId`, `emoji`)
- `deleteMessage` (`chatId`, `messageId`)
- `editMessage` (`chatId`, `messageId`, `content`)

Channel message actions expose ergonomic aliases (`send`, `react`, `delete`, `edit`, `sticker`, `sticker-search`).

Gating controls:

- `channels.telegram.actions.sendMessage`
- `channels.telegram.actions.editMessage`
- `channels.telegram.actions.deleteMessage`
- `channels.telegram.actions.reactions`
- `channels.telegram.actions.sticker` (default: disabled)

Reaction removal semantics: [/tools/reactions](/tools/reactions)

Reply threading tags

Telegram supports explicit reply threading tags in generated output:

- `[[reply_to_current]]` replies to the triggering message
- `[[reply_to:\<id\>]]` replies to a specific Telegram message ID

`channels.telegram.replyToMode` controls handling:

- `off` (default)
- `first`
- `all`

Note: `off` disables implicit reply threading. Explicit `[[reply_to_*]]` tags are still honored.

Forum topics and thread behavior

Forum supergroups:

- topic session keys append `:topic:\<threadId\>`
- replies and typing target the topic thread
- topic config path:
  `channels.telegram.groups.\<chatId\>.topics.\<threadId\>`

General topic (`threadId=1`) special-case:

- message sends omit `message_thread_id` (Telegram rejects `sendMessage(...thread_id=1)`)
- typing actions still include `message_thread_id`

Topic inheritance: topic entries inherit group settings unless overridden (`requireMention`, `allowFrom`, `skills`, `systemPrompt`, `enabled`, `groupPolicy`).

Template context includes:

- `MessageThreadId`
- `IsForum`

DM thread behavior:

- private chats with `message_thread_id` keep DM routing but use thread-aware session keys/reply targets.

Audio, video, and stickers

### Audio messages

Telegram distinguishes voice notes vs audio files.

- default: audio file behavior
- tag `[[audio_as_voice]]` in agent reply to force voice-note send

Message action example:
json5
{
  action: "send",
  channel: "telegram",
  to: "123456789",
  media: "https://example.com/voice.ogg",
  asVoice: true,
}
### Video messages

Telegram distinguishes video files vs video notes.

Message action example:
json5
{
  action: "send",
  channel: "telegram",
  to: "123456789",
  media: "https://example.com/video.mp4",
  asVideoNote: true,
}
Video notes do not support captions; provided message text is sent separately.

### Stickers

Inbound sticker handling:

- static WEBP: downloaded and processed (placeholder `<media:sticker>`)
- animated TGS: skipped
- video WEBM: skipped

Sticker context fields:

- `Sticker.emoji`
- `Sticker.setName`
- `Sticker.fileId`
- `Sticker.fileUniqueId`
- `Sticker.cachedDescription`

Sticker cache file:

- `~/.openclaw/telegram/sticker-cache.json`

Stickers are described once (when possible) and cached to reduce repeated vision calls.

Enable sticker actions:
json5
{
  channels: {
    telegram: {
      actions: {
        sticker: true,
      },
    },
  },
}
Send sticker action:
json5
{
  action: "sticker",
  channel: "telegram",
  to: "123456789",
  fileId: "CAACAgIAAxkBAAI...",
}
Search cached stickers:
json5
{
  action: "sticker-search",
  channel: "telegram",
  query: "cat waving",
  limit: 5,
}

Reaction notifications

Telegram reactions arrive as `message_reaction` updates (separate from message payloads).

When enabled, OpenClaw enqueues system events like:

- `Telegram reaction added: 👍 by Alice (@alice) on msg 42`

Config:

- `channels.telegram.reactionNotifications`: `off | own | all` (default: `own`)
- `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` (default: `minimal`)

Notes:

- `own` means user reactions to bot-sent messages only (best-effort via sent-message cache).
- Telegram does not provide thread IDs in reaction updates.
  - non-forum groups route to group chat session
  - forum groups route to the group general-topic session (`:topic:1`), not the exact originating topic

`allowed_updates` for polling/webhook include `message_reaction` automatically.

Config writes from Telegram events and commands

Channel config writes are enabled by default (`configWrites !== false`).

Telegram-triggered writes include:

- group migration events (`migrate_to_chat_id`) to update `channels.telegram.groups`
- `/config set` and `/config unset` (requires command enablement)

Disable:
json5
{
  channels: {
    telegram: {
      configWrites: false,
    },
  },
}

Long polling vs webhook

Default: long polling.

Webhook mode:

- set `channels.telegram.webhookUrl`
- set `channels.telegram.webhookSecret` (required when webhook URL is set)
- optional `channels.telegram.webhookPath` (default `/telegram-webhook`)
- optional `channels.telegram.webhookHost` (default `127.0.0.1`)

Default local listener for webhook mode binds to `127.0.0.1:8787`.

If your public endpoint differs, place a reverse proxy in front and point `webhookUrl` at the public URL.
Set `webhookHost` (for example `0.0.0.0`) when you intentionally need external ingress.

Limits, retry, and CLI targets

- `channels.telegram.textChunkLimit` default is 4000.
- `channels.telegram.chunkMode="newline"` prefers paragraph boundaries (blank lines) before length splitting.
- `channels.telegram.mediaMaxMb` (default 5) caps inbound Telegram media download/processing size.
- `channels.telegram.timeoutSeconds` overrides Telegram API client timeout (if unset, grammY default applies).
- group context history uses `channels.telegram.historyLimit` or `messages.groupChat.historyLimit` (default 50); `0` disables.
- DM history controls:
  - `channels.telegram.dmHistoryLimit`
  - `channels.telegram.dms["\<user_id\>"].historyLimit`
- outbound Telegram API retries are configurable via `channels.telegram.retry`.

CLI send target can be numeric chat ID or username:
bash
openclaw message send --channel telegram --target 123456789 --message "hi"
openclaw message send --channel telegram --target @name --message "hi"

Troubleshooting

Bot does not respond to non mention group messages

- If `requireMention=false`, Telegram privacy mode must allow full visibility.
  - BotFather: `/setprivacy` -> Disable
  - then remove + re-add bot to group
- `openclaw channels status` warns when config expects unmentioned group messages.
- `openclaw channels status --probe` can check explicit numeric group IDs; wildcard `"*"` cannot be membership-probed.
- quick session test: `/activation always`.

Bot not seeing group messages at all

- when `channels.telegram.groups` exists, group must be listed (or include `"*"`)
- verify bot membership in group
- review logs: `openclaw logs --follow` for skip reasons

Commands work partially or not at all

- authorize your sender identity (pairing and/or `allowFrom`)
- command authorization still applies even when group policy is `open`
- `setMyCommands failed` usually indicates DNS/HTTPS reachability issues to `api.telegram.org`

Polling or network instability

- Node 22+ + custom fetch/proxy can trigger immediate abort behavior if AbortSignal types mismatch.
- Some hosts resolve `api.telegram.org` to IPv6 first; broken IPv6 egress can cause intermittent Telegram API failures.
- Validate DNS answers:
bash
dig +short api.telegram.org A
dig +short api.telegram.org AAAA

More help: Channel troubleshooting.

Telegram config reference pointers

Primary reference:

  • channels.telegram.enabled: enable/disable channel startup.

  • channels.telegram.botToken: bot token (BotFather).

  • channels.telegram.tokenFile: read token from file path.

  • channels.telegram.dmPolicy: pairing | allowlist | open | disabled (default: pairing).

  • channels.telegram.allowFrom: DM allowlist (numeric Telegram user IDs). open requires "*".

  • channels.telegram.groupPolicy: open | allowlist | disabled (default: allowlist).

  • channels.telegram.groupAllowFrom: group sender allowlist (numeric Telegram user IDs).

  • channels.telegram.groups: per-group defaults + allowlist (use "*" for global defaults).

    • channels.telegram.groups.\<id\>.groupPolicy: per-group override for groupPolicy (open | allowlist | disabled).
    • channels.telegram.groups.\<id\>.requireMention: mention gating default.
    • channels.telegram.groups.\<id\>.skills: skill filter (omit = all skills, empty = none).
    • channels.telegram.groups.\<id\>.allowFrom: per-group sender allowlist override.
    • channels.telegram.groups.\<id\>.systemPrompt: extra system prompt for the group.
    • channels.telegram.groups.\<id\>.enabled: disable the group when false.
    • channels.telegram.groups.\<id\>.topics.\<threadId\>.*: per-topic overrides (same fields as group).
    • channels.telegram.groups.\<id\>.topics.\<threadId\>.groupPolicy: per-topic override for groupPolicy (open | allowlist | disabled).
    • channels.telegram.groups.\<id\>.topics.\<threadId\>.requireMention: per-topic mention gating override.
  • channels.telegram.capabilities.inlineButtons: off | dm | group | all | allowlist (default: allowlist).

  • channels.telegram.accounts.\<account\>.capabilities.inlineButtons: per-account override.

  • channels.telegram.replyToMode: off | first | all (default: off).

  • channels.telegram.textChunkLimit: outbound chunk size (chars).

  • channels.telegram.chunkMode: length (default) or newline to split on blank lines (paragraph boundaries) before length chunking.

  • channels.telegram.linkPreview: toggle link previews for outbound messages (default: true).

  • channels.telegram.streamMode: off | partial | block (draft streaming).

  • channels.telegram.mediaMaxMb: inbound/outbound media cap (MB).

  • channels.telegram.retry: retry policy for outbound Telegram API calls (attempts, minDelayMs, maxDelayMs, jitter).

  • channels.telegram.network.autoSelectFamily: override Node autoSelectFamily (true=enable, false=disable). Defaults to disabled on Node 22 to avoid Happy Eyeballs timeouts.

  • channels.telegram.proxy: proxy URL for Bot API calls (SOCKS/HTTP).

  • channels.telegram.webhookUrl: enable webhook mode (requires channels.telegram.webhookSecret).

  • channels.telegram.webhookSecret: webhook secret (required when webhookUrl is set).

  • channels.telegram.webhookPath: local webhook path (default /telegram-webhook).

  • channels.telegram.webhookHost: local webhook bind host (default 127.0.0.1).

  • channels.telegram.actions.reactions: gate Telegram tool reactions.

  • channels.telegram.actions.sendMessage: gate Telegram tool message sends.

  • channels.telegram.actions.deleteMessage: gate Telegram tool message deletes.

  • channels.telegram.actions.sticker: gate Telegram sticker actions — send and search (default: false).

  • channels.telegram.reactionNotifications: off | own | all — control which reactions trigger system events (default: own when not set).

  • channels.telegram.reactionLevel: off | ack | minimal | extensive — control agent's reaction capability (default: minimal when not set).

  • Configuration reference - Telegram

Telegram-specific high-signal fields:

  • startup/auth: enabled, botToken, tokenFile, accounts.*
  • access control: dmPolicy, allowFrom, groupPolicy, groupAllowFrom, groups, groups.*.topics.*
  • command/menu: commands.native, customCommands
  • threading/replies: replyToMode
  • streaming: streamMode, draftChunk, blockStreaming
  • formatting/delivery: textChunkLimit, chunkMode, linkPreview, responsePrefix
  • media/network: mediaMaxMb, timeoutSeconds, retry, network.autoSelectFamily, proxy
  • webhook: webhookUrl, webhookSecret, webhookPath, webhookHost
  • actions/capabilities: capabilities.inlineButtons, actions.sendMessage|editMessage|deleteMessage|reactions|sticker
  • reactions: reactionNotifications, reactionLevel
  • writes/history: configWrites, historyLimit, dmHistoryLimit, dms.*.historyLimit