Docs / channels / telegram

Telegram

Telegram (Bot API)

Quick setup (beginner)

  1. Create a bot with @BotFather (direct link). Confirm the handle is exactly @BotFather, then copy the token.
  2. Set the token:
    • Env: TELEGRAM_BOT_TOKEN=...
    • Or config: channels.telegram.botToken: "...".
    • If both are set, config takes precedence (env fallback is default-account only).
  3. Env: TELEGRAM_BOT_TOKEN=...
  4. Or config: channels.telegram.botToken: "...".
  5. If both are set, config takes precedence (env fallback is default-account only).
  6. Start the gateway.
  7. DM access is pairing by default; approve the pairing code on first contact.
  • Env: TELEGRAM_BOT_TOKEN=...
  • Or config: channels.telegram.botToken: "...".
  • If both are set, config takes precedence (env fallback is default-account only).
text
{
  channels: {
    telegram: {
      enabled: true,
      botToken: "123:abc",
      dmPolicy: "pairing",
    },
  },
}

What it is

  • A Telegram Bot API channel owned by the Gateway.
  • Deterministic routing: replies go back to Telegram; the model never chooses channels.
  • DMs share the agent’s main session; groups stay isolated (agent:<agentId>:telegram:group:<chatId>).

Setup (fast path)

1) Create a bot token (BotFather)

  1. Open Telegram and chat with @BotFather (direct link). Confirm the handle is exactly @BotFather.
  2. Run /newbot, then follow the prompts (name + username ending in bot).
  3. Copy the token and store it safely.
  • /setjoingroups — allow/deny adding the bot to groups.
  • /setprivacy — control whether the bot sees all group messages.

2) Configure the token (env or config)

text
{
  channels: {
    telegram: {
      enabled: true,
      botToken: "123:abc",
      dmPolicy: "pairing",
      groups: { "*": { requireMention: true } },
    },
  },
}
  1. Start the gateway. Telegram starts when a token is resolved (config first, env fallback).
  2. DM access defaults to pairing. Approve the code when the bot is first contacted.
  3. For groups: add the bot, decide privacy/admin behavior (below), then set channels.telegram.groups to control mention gating + allowlists.

Token + privacy + permissions (Telegram side)

Token creation (BotFather)

  • /newbot creates the bot and returns the token (keep it secret).
  • If a token leaks, revoke/regenerate it via @BotFather and update your config.

Group message visibility (Privacy Mode)

  • Disable privacy mode with /setprivacy or
  • Add the bot as a group admin (admin bots receive all messages).

Group permissions (admin rights)

How it works (behavior)

  • Inbound messages are normalized into the shared channel envelope with reply context and media placeholders.
  • Group replies require a mention by default (native @mention or agents.list[].groupChat.mentionPatterns / messages.groupChat.mentionPatterns).
  • Multi-agent override: set per-agent patterns on agents.list[].groupChat.mentionPatterns.
  • Replies always route back to the same Telegram chat.
  • Long-polling uses grammY runner with per-chat sequencing; overall concurrency is capped by agents.defaults.maxConcurrent.
  • Telegram Bot API does not support read receipts; there is no sendReadReceipts option.

Draft streaming

  • Threaded Mode enabled for the bot in @BotFather (forum topic mode).
  • Private chat threads only (Telegram includes message_thread_id on inbound messages).
  • channels.telegram.streamMode not set to "off" (default: "partial", "block" enables chunked draft updates).

Formatting (Telegram HTML)

  • Outbound Telegram text uses parse_mode: "HTML" (Telegram’s supported tag subset).
  • Markdown-ish input is rendered into Telegram-safe HTML (bold/italic/strike/code/links); block elements are flattened to text with newlines/bullets.
  • Raw HTML from models is escaped to avoid Telegram parse errors.
  • If Telegram rejects the HTML payload, OpenClaw retries the same message as plain text.

Commands (native + custom)

text
{
  channels: {
    telegram: {
      customCommands: [
        { command: "backup", description: "Git backup" },
        { command: "generate", description: "Create an image" },
      ],
    },
  },
}

Troubleshooting

  • setMyCommands failed in logs usually means outbound HTTPS/DNS is blocked to api.telegram.org.
  • If you see sendMessage or sendChatAction failures, check IPv6 routing and DNS.
  • Custom commands are menu entries only; OpenClaw does not implement them unless you handle them elsewhere.
  • Command names are normalized (leading / stripped, lowercased) and must match a-z, 0-9, _ (1–32 chars).
  • Custom commands cannot override native commands. Conflicts are ignored and logged.
  • If commands.native is disabled, only custom commands are registered (or cleared if none).

Limits

  • Outbound text is chunked to channels.telegram.textChunkLimit (default 4000).
  • Optional newline chunking: set channels.telegram.chunkMode="newline" to split on blank lines (paragraph boundaries) before length chunking.
  • Media downloads/uploads are capped by channels.telegram.mediaMaxMb (default 5).
  • Telegram Bot API requests time out after channels.telegram.timeoutSeconds (default 500 via grammY). Set lower to avoid long hangs.
  • Group history context uses channels.telegram.historyLimit (or channels.telegram.accounts.*.historyLimit), falling back to messages.groupChat.historyLimit. Set 0 to disable (default 50).
  • DM history can be limited with channels.telegram.dmHistoryLimit (user turns). Per-user overrides: channels.telegram.dms["<user_id>"].historyLimit.

Group activation modes

text
{
  channels: {
    telegram: {
      groups: {
        "-1001234567890": { requireMention: false }, // always respond in this group
      },
    },
  },
}
text
{
  channels: {
    telegram: {
      groups: {
        "*": { requireMention: false }, // all groups, always respond
      },
    },
  },
}
text
{
  channels: {
    telegram: {
      groups: {
        "*": { requireMention: true }, // or omit groups entirely
      },
    },
  },
}

Via command (session-level)

  • /activation always - respond to all messages
  • /activation mention - require mentions (default)

Getting the group chat ID

Config writes

  • A group is upgraded to a supergroup and Telegram emits migrate_to_chat_id (chat ID changes). OpenClaw can migrate channels.telegram.groups automatically.
  • You run /config set or /config unset in a Telegram chat (requires commands.config: true).
text
{
  channels: { telegram: { configWrites: false } },
}

Topics (forum supergroups)

  • Appends :topic:<threadId> to the Telegram group session key so each topic is isolated.
  • Sends typing indicators and replies with message_thread_id so responses stay in the topic.
  • General topic (thread id 1) is special: message sends omit message_thread_id (Telegram rejects it), but typing indicators still include it.
  • Exposes MessageThreadId + IsForum in template context for routing/templating.
  • Topic-specific configuration is available under channels.telegram.groups.<chatId>.topics.<threadId> (skills, allowlists, auto-reply, system prompts, disable).
  • Topic configs inherit group settings (requireMention, allowlists, skills, prompts, enabled) unless overridden per topic.

Inline Buttons

text
{
  channels: {
    telegram: {
      capabilities: {
        inlineButtons: "allowlist",
      },
    },
  },
}
text
{
  channels: {
    telegram: {
      accounts: {
        main: {
          capabilities: {
            inlineButtons: "allowlist",
          },
        },
      },
    },
  },
}
  • off — inline buttons disabled
  • dm — only DMs (group targets blocked)
  • group — only groups (DM targets blocked)
  • all — DMs + groups
  • allowlist — DMs + groups, but only senders allowed by allowFrom/groupAllowFrom (same rules as control commands)

Sending buttons

text
{
  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" }],
  ],
}

Configuration options

  • channels.telegram.capabilities: Global default capability config applied to all Telegram accounts unless overridden.
  • channels.telegram.accounts.<account>.capabilities: Per-account capabilities that override the global defaults for that specific account.

Access control (DMs + groups)

DM access

  • Default: channels.telegram.dmPolicy = "pairing". Unknown senders receive a pairing code; messages are ignored until approved (codes expire after 1 hour).
  • Approve via:
    • openclaw pairing list telegram
    • openclaw pairing approve telegram <CODE>
  • openclaw pairing list telegram
  • openclaw pairing approve telegram <CODE>
  • Pairing is the default token exchange used for Telegram DMs. Details: Pairing
  • channels.telegram.allowFrom accepts numeric user IDs (recommended) or @username entries. It is not the bot username; use the human sender’s ID. The wizard accepts @username and resolves it to the numeric ID when possible.
  • openclaw pairing list telegram
  • openclaw pairing approve telegram <CODE>

Finding your Telegram user ID

  1. Start the gateway and DM your bot.
  2. Run openclaw logs --follow and look for from.id.
  1. DM your bot.
  2. Fetch updates with your bot token and read message.from.id:
    curl "https://api.telegram.org/bot<bot_token>/getUpdates"
    
text
curl "https://api.telegram.org/bot<bot_token>/getUpdates"
  • DM @userinfobot or @getidsbot and use the returned user id.

Group access

  • No groups config = all groups allowed
  • With groups config = only listed groups or "*" are allowed
  • Example: "groups": { "-1001234567890": {}, "*": {} } allows all groups
  • "open" = all senders in allowed groups can message
  • "allowlist" = only senders in channels.telegram.groupAllowFrom can message
  • "disabled" = no group messages accepted at all Default is groupPolicy: "allowlist" (blocked unless you add groupAllowFrom).

Long-polling vs webhook

  • Default: long-polling (no public URL required).
  • Webhook mode: set channels.telegram.webhookUrl and channels.telegram.webhookSecret (optionally channels.telegram.webhookPath).
    • The local listener binds to 0.0.0.0:8787 and serves POST /telegram-webhook by default.
    • If your public URL is different, use a reverse proxy and point channels.telegram.webhookUrl at the public endpoint.
  • The local listener binds to 0.0.0.0:8787 and serves POST /telegram-webhook by default.
  • If your public URL is different, use a reverse proxy and point channels.telegram.webhookUrl at the public endpoint.
  • The local listener binds to 0.0.0.0:8787 and serves POST /telegram-webhook by default.
  • If your public URL is different, use a reverse proxy and point channels.telegram.webhookUrl at the public endpoint.

Reply threading

  • [[reply_to_current]] — reply to the triggering message.
  • [[reply_to:<id>]] — reply to a specific message id.
  • first (default), all, off.

Audio messages (voice vs file)

  • [[audio_as_voice]] — send audio as a voice note instead of a file.
text
{
  action: "send",
  channel: "telegram",
  to: "123456789",
  media: "https://example.com/voice.ogg",
  asVoice: true,
}

Stickers

Receiving stickers

  • Static stickers (WEBP): Downloaded and processed through vision. The sticker appears as a <media:sticker> placeholder in the message content.
  • Animated stickers (TGS): Skipped (Lottie format not supported for processing).
  • Video stickers (WEBM): Skipped (video format not supported for processing).
  • Sticker — object with:
    • emoji — emoji associated with the sticker
    • setName — name of the sticker set
    • fileId — Telegram file ID (send the same sticker back)
    • fileUniqueId — stable ID for cache lookup
    • cachedDescription — cached vision description when available
  • emoji — emoji associated with the sticker
  • setName — name of the sticker set
  • fileId — Telegram file ID (send the same sticker back)
  • fileUniqueId — stable ID for cache lookup
  • cachedDescription — cached vision description when available
  • emoji — emoji associated with the sticker
  • setName — name of the sticker set
  • fileId — Telegram file ID (send the same sticker back)
  • fileUniqueId — stable ID for cache lookup
  • cachedDescription — cached vision description when available

Sticker cache

  1. First encounter: The sticker image is sent to the AI for vision analysis. The AI generates a description (e.g., “A cartoon cat waving enthusiastically”).
  2. Cache storage: The description is saved along with the sticker’s file ID, emoji, and set name.
  3. Subsequent encounters: When the same sticker is seen again, the cached description is used directly. The image is not sent to the AI.
text
{
  "fileId": "CAACAgIAAxkBAAI...",
  "fileUniqueId": "AgADBAADb6cxG2Y",
  "emoji": "👋",
  "setName": "CoolCats",
  "description": "A cartoon cat waving enthusiastically",
  "cachedAt": "2026-01-15T10:30:00.000Z"
}
  • Reduces API costs by avoiding repeated vision calls for the same sticker
  • Faster response times for cached stickers (no vision processing delay)
  • Enables sticker search functionality based on cached descriptions

Sending stickers

text
{
  channels: {
    telegram: {
      actions: {
        sticker: true,
      },
    },
  },
}
text
{
  action: "sticker",
  channel: "telegram",
  to: "123456789",
  fileId: "CAACAgIAAxkBAAI...",
}
  • fileId (required) — the Telegram file ID of the sticker. Obtain this from Sticker.fileId when receiving a sticker, or from a sticker-search result.
  • replyTo (optional) — message ID to reply to.
  • threadId (optional) — message thread ID for forum topics.
text
{
  action: "sticker-search",
  channel: "telegram",
  query: "cat waving",
  limit: 5,
}
text
{
  ok: true,
  count: 2,
  stickers: [
    {
      fileId: "CAACAgIAAxkBAAI...",
      emoji: "👋",
      description: "A cartoon cat waving enthusiastically",
      setName: "CoolCats",
    },
  ],
}
text
{
  action: "sticker",
  channel: "telegram",
  to: "-1001234567890",
  fileId: "CAACAgIAAxkBAAI...",
  replyTo: 42,
  threadId: 123,
}

Streaming (drafts)

  • Private chats with topics enabled (forum topic mode for the bot).
  • Incoming messages must include message_thread_id (private topic thread).
  • Streaming is ignored for groups/supergroups/channels.
  • channels.telegram.streamMode: "off" | "partial" | "block" (default: partial)
    • partial: update the draft bubble with the latest streaming text.
    • block: update the draft bubble in larger blocks (chunked).
    • off: disable draft streaming.
  • partial: update the draft bubble with the latest streaming text.
  • block: update the draft bubble in larger blocks (chunked).
  • off: disable draft streaming.
  • Optional (only for streamMode: "block"):
    • channels.telegram.draftChunk: { minChars?, maxChars?, breakPreference? }
      • defaults: minChars: 200, maxChars: 800, breakPreference: "paragraph" (clamped to channels.telegram.textChunkLimit).
  • channels.telegram.draftChunk: { minChars?, maxChars?, breakPreference? }
    • defaults: minChars: 200, maxChars: 800, breakPreference: "paragraph" (clamped to channels.telegram.textChunkLimit).
  • defaults: minChars: 200, maxChars: 800, breakPreference: "paragraph" (clamped to channels.telegram.textChunkLimit).
  • partial: update the draft bubble with the latest streaming text.
  • block: update the draft bubble in larger blocks (chunked).
  • off: disable draft streaming.
  • channels.telegram.draftChunk: { minChars?, maxChars?, breakPreference? }
    • defaults: minChars: 200, maxChars: 800, breakPreference: "paragraph" (clamped to channels.telegram.textChunkLimit).
  • defaults: minChars: 200, maxChars: 800, breakPreference: "paragraph" (clamped to channels.telegram.textChunkLimit).
  • defaults: minChars: 200, maxChars: 800, breakPreference: "paragraph" (clamped to channels.telegram.textChunkLimit).
  • /reasoning stream streams reasoning into the draft bubble while the reply is generating, then sends the final answer without reasoning.
  • If channels.telegram.streamMode is off, reasoning stream is disabled. More context: Streaming + chunking.

Retry policy

Agent tool (messages + reactions)

  • Tool: telegram with sendMessage action (to, content, optional mediaUrl, replyToMessageId, messageThreadId).
  • Tool: telegram with react action (chatId, messageId, emoji).
  • Tool: telegram with deleteMessage action (chatId, messageId).
  • Reaction removal semantics: see /tools/reactions.
  • Tool gating: channels.telegram.actions.reactions, channels.telegram.actions.sendMessage, channels.telegram.actions.deleteMessage (default: enabled), and channels.telegram.actions.sticker (default: disabled).

Reaction notifications

  1. Receives the message_reaction update from Telegram API
  2. Converts it to a system event with format: "Telegram reaction added: {emoji} by {user} on msg {id}"
  3. Enqueues the system event using the same session key as regular messages
  4. When the next message arrives in that conversation, system events are drained and prepended to the agent’s context
  • channels.telegram.reactionNotifications: Controls which reactions trigger notifications
    • "off" — ignore all reactions
    • "own" — notify when users react to bot messages (best-effort; in-memory) (default)
    • "all" — notify for all reactions
  • "off" — ignore all reactions
  • "own" — notify when users react to bot messages (best-effort; in-memory) (default)
  • "all" — notify for all reactions
  • channels.telegram.reactionLevel: Controls agent’s reaction capability
    • "off" — agent cannot react to messages
    • "ack" — bot sends acknowledgment reactions (👀 while processing) (default)
    • "minimal" — agent can react sparingly (guideline: 1 per 5-10 exchanges)
    • "extensive" — agent can react liberally when appropriate
  • "off" — agent cannot react to messages
  • "ack" — bot sends acknowledgment reactions (👀 while processing) (default)
  • "minimal" — agent can react sparingly (guideline: 1 per 5-10 exchanges)
  • "extensive" — agent can react liberally when appropriate
  • "off" — ignore all reactions
  • "own" — notify when users react to bot messages (best-effort; in-memory) (default)
  • "all" — notify for all reactions
  • "off" — agent cannot react to messages
  • "ack" — bot sends acknowledgment reactions (👀 while processing) (default)
  • "minimal" — agent can react sparingly (guideline: 1 per 5-10 exchanges)
  • "extensive" — agent can react liberally when appropriate
text
{
  channels: {
    telegram: {
      reactionNotifications: "all", // See all reactions
      reactionLevel: "minimal", // Agent can react sparingly
    },
  },
}
  • Telegram bots must explicitly request message_reaction in allowed_updates (configured automatically by OpenClaw)
  • For webhook mode, reactions are included in the webhook allowed_updates
  • For polling mode, reactions are included in the getUpdates allowed_updates

Delivery targets (CLI/cron)

  • Use a chat id (123456789) or a username (@name) as the target.
  • Example: openclaw message send --channel telegram --target 123456789 --message "hi".

Troubleshooting

  • If you set channels.telegram.groups.*.requireMention=false, Telegram’s Bot API privacy mode must be disabled.
    • BotFather: /setprivacyDisable (then remove + re-add the bot to the group)
  • BotFather: /setprivacyDisable (then remove + re-add the bot to the group)
  • openclaw channels status shows a warning when config expects unmentioned group messages.
  • openclaw channels status --probe can additionally check membership for explicit numeric group IDs (it can’t audit wildcard "*" rules).
  • Quick test: /activation always (session-only; use config for persistence)
  • BotFather: /setprivacyDisable (then remove + re-add the bot to the group)
  • If channels.telegram.groups is set, the group must be listed or use "*"
  • Check Privacy Settings in @BotFather → “Group Privacy” should be OFF
  • Verify bot is actually a member (not just an admin with no read access)
  • Check gateway logs: openclaw logs --follow (look for “skipping group message”)
  • The /activation command updates session state but doesn’t persist to config
  • For persistent behavior, add group to channels.telegram.groups with requireMention: false
  • Make sure your Telegram user ID is authorized (via pairing or channels.telegram.allowFrom)
  • Commands require authorization even in groups with groupPolicy: "open"
  • Node 22+ is stricter about AbortSignal instances; foreign signals can abort fetch calls right away.
  • Upgrade to a OpenClaw build that normalizes abort signals, or run the gateway on Node 20 until you can upgrade.
  • Some hosts resolve api.telegram.org to IPv6 first. If your server does not have working IPv6 egress, grammY can get stuck on IPv6-only requests.
  • Fix by enabling IPv6 egress or forcing IPv4 resolution for api.telegram.org (for example, add an /etc/hosts entry using the IPv4 A record, or prefer IPv4 in your OS DNS stack), then restart the gateway.
  • Quick check: dig +short api.telegram.org A and dig +short api.telegram.org AAAA to confirm what DNS returns.

Configuration reference (Telegram)

  • 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 (ids/usernames). open requires "*".
  • channels.telegram.groupPolicy: open | allowlist | disabled (default: allowlist).
  • channels.telegram.groupAllowFrom: group sender allowlist (ids/usernames).
  • channels.telegram.groups: per-group defaults + allowlist (use "*" for global defaults).
    • 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>.requireMention: per-topic mention gating override.
  • 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>.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: first).
  • 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.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).
  • 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>.requireMention: per-topic mention gating override.
  • agents.list[].groupChat.mentionPatterns (mention gating patterns).
  • messages.groupChat.mentionPatterns (global fallback).
  • commands.native (defaults to "auto" → on for Telegram/Discord, off for Slack), commands.text, commands.useAccessGroups (command behavior). Override with channels.telegram.commands.native.
  • messages.responsePrefix, messages.ackReaction, messages.ackReactionScope, messages.removeAckAfterReply.