Files
openclaw/docs/channels/telegram.md
2026-02-11 12:10:52 -05:00

19 KiB

summary, read_when, title
summary read_when title
Telegram bot support status, capabilities, and configuration
Working on Telegram features or webhooks
Telegram

Telegram (Bot API)

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

Default DM policy for Telegram is pairing. Cross-channel diagnostics and repair playbooks. Full channel config patterns and examples.

Quick setup

Open Telegram and chat with **@BotFather** (confirm the handle is exactly `@BotFather`).
Run `/newbot`, follow prompts, and save the token.
{
  channels: {
    telegram: {
      enabled: true,
      botToken: "123:abc",
      dmPolicy: "pairing",
      groups: { "*": { requireMention: true } },
    },
  },
}
Env fallback: `TELEGRAM_BOT_TOKEN=...` (default account only).
openclaw gateway
openclaw pairing list telegram
openclaw pairing approve telegram <CODE>
Pairing codes expire after 1 hour.
Add the bot to your group, then set `channels.telegram.groups` and `groupPolicy` to match your access model. 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

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.
Admin status is controlled in Telegram group settings.
Admin bots receive all group messages, which is useful for always-on group behavior.
- `/setjoingroups` to allow/deny group adds
- `/setprivacy` for group visibility behavior

Access control and activation

`channels.telegram.dmPolicy` controls direct message access:
- `pairing` (default)
- `allowlist`
- `open` (requires `allowFrom` to include `"*"`)
- `disabled`

`channels.telegram.allowFrom` accepts numeric IDs and usernames. `telegram:` / `tg:` prefixes are accepted and normalized.

### 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:
curl "https://api.telegram.org/bot<bot_token>/getUpdates"
Third-party method (less private): `@userinfobot` or `@getidsbot`.
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:
{
  channels: {
    telegram: {
      groups: {
        "-1001234567890": {
          groupPolicy: "open",
          requireMention: false,
        },
      },
    },
  },
}
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:
{
  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

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
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`.
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:
{
  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).
Configure inline keyboard scope:
{
  channels: {
    telegram: {
      capabilities: {
        inlineButtons: "allowlist",
      },
    },
  },
}
Per-account override:
{
  channels: {
    telegram: {
      accounts: {
        main: {
          capabilities: {
            inlineButtons: "allowlist",
          },
        },
      },
    },
  },
}
Scopes:

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

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

Message action example:
{
  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 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)
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:

- `first` (default)
- `all`
- `off`
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 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:
{
  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:
{
  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:
{
  channels: {
    telegram: {
      actions: {
        sticker: true,
      },
    },
  },
}
Send sticker action:
{
  action: "sticker",
  channel: "telegram",
  to: "123456789",
  fileId: "CAACAgIAAxkBAAI...",
}
Search cached stickers:
{
  action: "sticker-search",
  channel: "telegram",
  query: "cat waving",
  limit: 5,
}
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.
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:
{
  channels: {
    telegram: {
      configWrites: false,
    },
  },
}
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`)

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

If your public endpoint differs, place a reverse proxy in front and point `webhookUrl` at the public URL.
- `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[""].historyLimit` - outbound Telegram API retries are configurable via `channels.telegram.retry`.
CLI send target can be numeric chat ID or username:
openclaw message send --channel telegram --target 123456789 --message "hi"
openclaw message send --channel telegram --target @name --message "hi"

Troubleshooting

- 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`.
- when `channels.telegram.groups` exists, group must be listed (or include `"*"`)
- verify bot membership in group
- review logs: `openclaw logs --follow` for skip reasons
- 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`
- 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:
dig +short api.telegram.org A
dig +short api.telegram.org AAAA

More help: Channel troubleshooting.

Telegram config reference pointers

Primary reference:

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
  • actions/capabilities: capabilities.inlineButtons, actions.sendMessage|editMessage|deleteMessage|reactions|sticker
  • reactions: reactionNotifications, reactionLevel
  • writes/history: configWrites, historyLimit, dmHistoryLimit, dms.*.historyLimit