diff --git a/docs/channels/telegram.md b/docs/channels/telegram.md index 04a0102a308..ae9c6202aef 100644 --- a/docs/channels/telegram.md +++ b/docs/channels/telegram.md @@ -7,54 +7,31 @@ title: "Telegram" # Telegram (Bot API) -Status: production-ready for bot DMs + groups via grammY. Long-polling by default; webhook optional. +Status: production-ready for bot DMs + groups via grammY. Long polling is the default mode; webhook mode is optional. -## Quick setup (beginner) + + + Default DM policy for Telegram is pairing. + + + Cross-channel diagnostics and repair playbooks. + + + Full channel config patterns and examples. + + -1. Create a bot with **@BotFather** ([direct link](https://t.me/BotFather)). 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. Start the gateway. -4. DM access is pairing by default; approve the pairing code on first contact. +## Quick setup -Minimal config: + + + Open Telegram and chat with **@BotFather** (confirm the handle is exactly `@BotFather`). -```json5 -{ - channels: { - telegram: { - enabled: true, - botToken: "123:abc", - dmPolicy: "pairing", - }, - }, -} -``` + Run `/newbot`, follow prompts, and save the token. -## 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::telegram:group:`). - -## Setup (fast path) - -### 1) Create a bot token (BotFather) - -1. Open Telegram and chat with **@BotFather** ([direct link](https://t.me/BotFather)). 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. - -Optional BotFather settings: - -- `/setjoingroups` — allow/deny adding the bot to groups. -- `/setprivacy` — control whether the bot sees all group messages. - -### 2) Configure the token (env or config) - -Example: + ```json5 { @@ -69,70 +46,232 @@ Example: } ``` -Env option: `TELEGRAM_BOT_TOKEN=...` (works for the default account). -If both env and config are set, config takes precedence. + Env fallback: `TELEGRAM_BOT_TOKEN=...` (default account only). -Multi-account support: use `channels.telegram.accounts` with per-account tokens and optional `name`. See [`gateway/configuration`](/gateway/configuration#telegramaccounts--discordaccounts--slackaccounts--signalaccounts--imessageaccounts) for the shared pattern. + -3. Start the gateway. Telegram starts when a token is resolved (config first, env fallback). -4. DM access defaults to pairing. Approve the code when the bot is first contacted. -5. 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) +```bash +openclaw gateway +openclaw pairing list telegram +openclaw pairing approve telegram +``` -### Token creation (BotFather) + Pairing codes expire after 1 hour. -- `/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) + + Add the bot to your group, then set `channels.telegram.groups` and `groupPolicy` to match your access model. + + -Telegram bots default to **Privacy Mode**, which limits which group messages they receive. -If your bot must see _all_ group messages, you have two options: + +Token resolution order is account-aware. In practice, config values win over env fallback, and `TELEGRAM_BOT_TOKEN` only applies to the default account. + -- Disable privacy mode with `/setprivacy` **or** -- Add the bot as a group **admin** (admin bots receive all messages). +## Telegram side settings -**Note:** When you toggle privacy mode, Telegram requires removing + re‑adding the bot -to each group for the change to take effect. + + + Telegram bots default to **Privacy Mode**, which limits what group messages they receive. -### Group permissions (admin rights) + If the bot must see all group messages, either: -Admin status is set inside the group (Telegram UI). Admin bots always receive all -group messages, so use admin if you need full visibility. + - disable privacy mode via `/setprivacy`, or + - make the bot a group admin. -## How it works (behavior) + When toggling privacy mode, remove + re-add the bot in each group so Telegram applies the change. -- 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 + + Admin status is controlled in Telegram group settings. -OpenClaw can stream partial replies in Telegram DMs using `sendMessageDraft`. + Admin bots receive all group messages, which is useful for always-on group behavior. -Requirements: + -- 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). + -Draft streaming is DM-only; Telegram does not support it in groups or channels. + - `/setjoingroups` to allow/deny group adds + - `/setprivacy` for group visibility behavior -## 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. +## Access control and activation -## Commands (native + custom) + + + `channels.telegram.dmPolicy` controls direct message access: -OpenClaw registers native commands (like `/status`, `/reset`, `/model`) with Telegram’s bot menu on startup. -You can add custom commands to the menu via config: + - `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: + +```bash +curl "https://api.telegram.org/bot/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: + +```json5 +{ + 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: + +```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:` 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: ```json5 { @@ -147,139 +286,38 @@ You can add custom commands to the menu via config: } ``` -## Setup troubleshooting (commands) + Rules: -- `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. + - 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 -More help: [Channel troubleshooting](/channels/troubleshooting). + Notes: -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 -- Custom commands are **menu entries only**; OpenClaw does not implement them unless you handle them elsewhere. -- Some commands can be handled by plugins/skills without being registered in Telegram’s command menu. These still work when typed (they just won't show up in `/commands` / the menu). -- 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). + If native commands are disabled, built-ins are removed. Custom/plugin commands may still register if configured. -### Device pairing commands (`device-pair` plugin) + Common setup failure: -If the `device-pair` plugin is installed, it adds a Telegram-first flow for pairing a new phone: + - `setMyCommands failed` usually means outbound DNS/HTTPS to `api.telegram.org` is blocked. -1. `/pair` generates a setup code (sent as a separate message for easy copy/paste). -2. Paste the setup code in the iOS app to connect. -3. `/pair approve` approves the latest pending device request. + ### Device pairing commands (`device-pair` plugin) -More details: [Pairing](/channels/pairing#pair-via-telegram-recommended-for-ios). + When the `device-pair` plugin is installed: -## Limits + 1. `/pair` generates setup code + 2. paste code in iOS app + 3. `/pair approve` approves latest pending request -- 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[""].historyLimit`. + More details: [Pairing](/channels/pairing#pair-via-telegram-recommended-for-ios). -## Group activation modes + -By default, the bot only responds to mentions in groups (`@botname` or patterns in `agents.list[].groupChat.mentionPatterns`). To change this behavior: - -### Via config (recommended) - -```json5 -{ - channels: { - telegram: { - groups: { - "-1001234567890": { requireMention: false }, // always respond in this group - }, - }, - }, -} -``` - -**Important:** Setting `channels.telegram.groups` creates an **allowlist** - only listed groups (or `"*"`) will be accepted. -Forum topics inherit their parent group config (allowFrom, requireMention, skills, prompts) unless you add per-topic overrides under `channels.telegram.groups..topics.`. - -To allow all groups with always-respond: - -```json5 -{ - channels: { - telegram: { - groups: { - "*": { requireMention: false }, // all groups, always respond - }, - }, - }, -} -``` - -To keep mention-only for all groups (default behavior): - -```json5 -{ - channels: { - telegram: { - groups: { - "*": { requireMention: true }, // or omit groups entirely - }, - }, - }, -} -``` - -### Via command (session-level) - -Send in the group: - -- `/activation always` - respond to all messages -- `/activation mention` - require mentions (default) - -**Note:** Commands update session state only. For persistent behavior across restarts, use config. - -### Getting the group chat ID - -Forward any message from the group to `@userinfobot` or `@getidsbot` on Telegram to see the chat ID (negative number like `-1001234567890`). - -**Tip:** For your own user ID, DM the bot and it will reply with your user ID (pairing message), or use `/whoami` once commands are enabled. - -**Privacy note:** `@userinfobot` is a third-party bot. If you prefer, add the bot to the group, send a message, and use `openclaw logs --follow` to read `chat.id`, or use the Bot API `getUpdates`. - -## Config writes - -By default, Telegram is allowed to write config updates triggered by channel events or `/config set|unset`. - -This happens when: - -- 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`). - -Disable with: - -```json5 -{ - channels: { telegram: { configWrites: false } }, -} -``` - -## Topics (forum supergroups) - -Telegram forum topics include a `message_thread_id` per message. OpenClaw: - -- Appends `:topic:` 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..topics.` (skills, allowlists, auto-reply, system prompts, disable). -- Topic configs inherit group settings (requireMention, allowlists, skills, prompts, enabled) unless overridden per topic. - -Private chats can include `message_thread_id` in some edge cases. OpenClaw keeps the DM session key unchanged, but still uses the thread id for replies/draft streaming when it is present. - -## Inline Buttons - -Telegram supports inline keyboards with callback buttons. + + Configure inline keyboard scope: ```json5 { @@ -293,7 +331,7 @@ Telegram supports inline keyboards with callback buttons. } ``` -For per-account configuration: + Per-account override: ```json5 { @@ -311,20 +349,17 @@ For per-account configuration: } ``` -Scopes: + Scopes: -- `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) + - `off` + - `dm` + - `group` + - `all` + - `allowlist` (default) -Default: `allowlist`. -Legacy: `capabilities: ["inlineButtons"]` = `inlineButtons: "all"`. + Legacy `capabilities: ["inlineButtons"]` maps to `inlineButtons: "all"`. -### Sending buttons - -Use the message tool with the `buttons` parameter: + Message action example: ```json5 { @@ -342,116 +377,82 @@ Use the message tool with the `buttons` parameter: } ``` -When a user clicks a button, the callback data is sent back to the agent as a message with the format: -`callback_data: value` + Callback clicks are passed to the agent as text: + `callback_data: ` -### Configuration options + -Telegram capabilities can be configured at two levels (object form shown above; legacy string arrays still supported): + + Telegram tool actions include: -- `channels.telegram.capabilities`: Global default capability config applied to all Telegram accounts unless overridden. -- `channels.telegram.accounts..capabilities`: Per-account capabilities that override the global defaults for that specific account. + - `sendMessage` (`to`, `content`, optional `mediaUrl`, `replyToMessageId`, `messageThreadId`) + - `react` (`chatId`, `messageId`, `emoji`) + - `deleteMessage` (`chatId`, `messageId`) + - `editMessage` (`chatId`, `messageId`, `content`) -Use the global setting when all Telegram bots/accounts should behave the same. Use per-account configuration when different bots need different behaviors (for example, one account only handles DMs while another is allowed in groups). + Channel message actions expose ergonomic aliases (`send`, `react`, `delete`, `edit`, `sticker`, `sticker-search`). -## Access control (DMs + groups) + Gating controls: -### DM access + - `channels.telegram.actions.sendMessage` + - `channels.telegram.actions.editMessage` + - `channels.telegram.actions.deleteMessage` + - `channels.telegram.actions.reactions` + - `channels.telegram.actions.sticker` (default: disabled) -- 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 ` -- Pairing is the default token exchange used for Telegram DMs. Details: [Pairing](/channels/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. + Reaction removal semantics: [/tools/reactions](/tools/reactions) -#### Finding your Telegram user ID + -Safer (no third-party bot): + + Telegram supports explicit reply threading tags in generated output: -1. Start the gateway and DM your bot. -2. Run `openclaw logs --follow` and look for `from.id`. + - `[[reply_to_current]]` replies to the triggering message + - `[[reply_to:]]` replies to a specific Telegram message ID -Alternate (official Bot API): + `channels.telegram.replyToMode` controls handling: -1. DM your bot. -2. Fetch updates with your bot token and read `message.from.id`: + - `first` (default) + - `all` + - `off` - ```bash - curl "https://api.telegram.org/bot/getUpdates" - ``` + -Third-party (less private): + + Forum supergroups: -- DM `@userinfobot` or `@getidsbot` and use the returned user id. + - topic session keys append `:topic:` + - replies and typing target the topic thread + - topic config path: + `channels.telegram.groups..topics.` -### Group access + General topic (`threadId=1`) special-case: -Two independent controls: + - message sends omit `message_thread_id` (Telegram rejects `sendMessage(...thread_id=1)`) + - typing actions still include `message_thread_id` -**1. Which groups are allowed** (group allowlist via `channels.telegram.groups`): + Topic inheritance: topic entries inherit group settings unless overridden (`requireMention`, `allowFrom`, `skills`, `systemPrompt`, `enabled`, `groupPolicy`). -- No `groups` config = all groups allowed -- With `groups` config = only listed groups or `"*"` are allowed -- Example: `"groups": { "-1001234567890": {}, "*": {} }` allows all groups + Template context includes: -**2. Which senders are allowed** (sender filtering via `channels.telegram.groupPolicy`): + - `MessageThreadId` + - `IsForum` -- `"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`). + DM thread behavior: -Most users want: `groupPolicy: "allowlist"` + `groupAllowFrom` + specific groups listed in `channels.telegram.groups` + - private chats with `message_thread_id` keep DM routing but use thread-aware session keys/reply targets. -To allow **any group member** to talk in a specific group (while still keeping control commands restricted to authorized senders), set a per-group override: + -```json5 -{ - channels: { - telegram: { - groups: { - "-1001234567890": { - groupPolicy: "open", - requireMention: false, - }, - }, - }, - }, -} -``` + + ### Audio messages -## Long-polling vs webhook + Telegram distinguishes voice notes vs audio files. -- 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. + - default: audio file behavior + - tag `[[audio_as_voice]]` in agent reply to force voice-note send -## Reply threading - -Telegram supports optional threaded replies via tags: - -- `[[reply_to_current]]` -- reply to the triggering message. -- `[[reply_to:]]` -- reply to a specific message id. - -Controlled by `channels.telegram.replyToMode`: - -- `first` (default), `all`, `off`. - -## Audio messages (voice vs file) - -Telegram distinguishes **voice notes** (round bubble) from **audio files** (metadata card). -OpenClaw defaults to audio files for backward compatibility. - -To force a voice note bubble in agent replies, include this tag anywhere in the reply: - -- `[[audio_as_voice]]` — send audio as a voice note instead of a file. - -The tag is stripped from the delivered text. Other channels ignore this tag. - -For message tool sends, set `asVoice: true` with a voice-compatible audio `media` URL -(`message` is optional when media is present): + Message action example: ```json5 { @@ -463,12 +464,11 @@ For message tool sends, set `asVoice: true` with a voice-compatible audio `media } ``` -## Video messages (video vs video note) + ### Video messages -Telegram distinguishes **video notes** (round bubble) from **video files** (rectangular). -OpenClaw defaults to video files. + Telegram distinguishes video files vs video notes. -For message tool sends, set `asVideoNote: true` with a video `media` URL: + Message action example: ```json5 { @@ -480,65 +480,31 @@ For message tool sends, set `asVideoNote: true` with a video `media` URL: } ``` -(Note: Video notes do not support captions. If you provide a message text, it will be sent as a separate message.) + Video notes do not support captions; provided message text is sent separately. -## Stickers + ### Stickers -OpenClaw supports receiving and sending Telegram stickers with intelligent caching. + Inbound sticker handling: -### Receiving stickers + - static WEBP: downloaded and processed (placeholder ``) + - animated TGS: skipped + - video WEBM: skipped -When a user sends a sticker, OpenClaw handles it based on the sticker type: + Sticker context fields: -- **Static stickers (WEBP):** Downloaded and processed through vision. The sticker appears as a `` 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.emoji` + - `Sticker.setName` + - `Sticker.fileId` + - `Sticker.fileUniqueId` + - `Sticker.cachedDescription` -Template context field available when receiving stickers: + Sticker cache file: -- `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 + - `~/.openclaw/telegram/sticker-cache.json` -### Sticker cache + Stickers are described once (when possible) and cached to reduce repeated vision calls. -Stickers are processed through the AI's vision capabilities to generate descriptions. Since the same stickers are often sent repeatedly, OpenClaw caches these descriptions to avoid redundant API calls. - -**How it works:** - -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. - -**Cache location:** `~/.openclaw/telegram/sticker-cache.json` - -**Cache entry format:** - -```json -{ - "fileId": "CAACAgIAAxkBAAI...", - "fileUniqueId": "AgADBAADb6cxG2Y", - "emoji": "👋", - "setName": "CoolCats", - "description": "A cartoon cat waving enthusiastically", - "cachedAt": "2026-01-15T10:30:00.000Z" -} -``` - -**Benefits:** - -- 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 - -The cache is populated automatically as stickers are received. There is no manual cache management required. - -### Sending stickers - -The agent can send and search stickers using the `sticker` and `sticker-search` actions. These are disabled by default and must be enabled in config: + Enable sticker actions: ```json5 { @@ -552,7 +518,7 @@ The agent can send and search stickers using the `sticker` and `sticker-search` } ``` -**Send a sticker:** + Send sticker action: ```json5 { @@ -563,15 +529,7 @@ The agent can send and search stickers using the `sticker` and `sticker-search` } ``` -Parameters: - -- `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. - -**Search for stickers:** - -The agent can search cached stickers by description, emoji, or set name: + Search cached stickers: ```json5 { @@ -582,219 +540,157 @@ The agent can search cached stickers by description, emoji, or set name: } ``` -Returns matching stickers from the cache: + -```json5 -{ - ok: true, - count: 2, - stickers: [ - { - fileId: "CAACAgIAAxkBAAI...", - emoji: "👋", - description: "A cartoon cat waving enthusiastically", - setName: "CoolCats", - }, - ], -} -``` + + Telegram reactions arrive as `message_reaction` updates (separate from message payloads). -The search uses fuzzy matching across description text, emoji characters, and set names. + When enabled, OpenClaw enqueues system events like: -**Example with threading:** + - `Telegram reaction added: 👍 by Alice (@alice) on msg 42` -```json5 -{ - action: "sticker", - channel: "telegram", - to: "-1001234567890", - fileId: "CAACAgIAAxkBAAI...", - replyTo: 42, - threadId: 123, -} -``` + Config: -## Streaming (drafts) + - `channels.telegram.reactionNotifications`: `off | own | all` (default: `own`) + - `channels.telegram.reactionLevel`: `off | ack | minimal | extensive` (default: `minimal`) -Telegram can stream **draft bubbles** while the agent is generating a response. -OpenClaw uses Bot API `sendMessageDraft` (not real messages) and then sends the -final reply as a normal message. + Notes: -Requirements (Telegram Bot API 9.3+): + - `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 -- **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. + `allowed_updates` for polling/webhook include `message_reaction` automatically. -Config: + -- `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. -- Optional (only for `streamMode: "block"`): - - `channels.telegram.draftChunk: { minChars?, maxChars?, breakPreference? }` - - defaults: `minChars: 200`, `maxChars: 800`, `breakPreference: "paragraph"` (clamped to `channels.telegram.textChunkLimit`). + + Channel config writes are enabled by default (`configWrites !== false`). -Note: draft streaming is separate from **block streaming** (channel messages). -Block streaming is off by default and requires `channels.telegram.blockStreaming: true` -if you want early Telegram messages instead of draft updates. + Telegram-triggered writes include: -Reasoning stream (Telegram only): + - group migration events (`migrate_to_chat_id`) to update `channels.telegram.groups` + - `/config set` and `/config unset` (requires command enablement) -- `/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](/concepts/streaming). - -## Retry policy - -Outbound Telegram API calls retry on transient network/429 errors with exponential backoff and jitter. Configure via `channels.telegram.retry`. See [Retry policy](/concepts/retry). - -## 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](/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 - -**How reactions work:** -Telegram reactions arrive as **separate `message_reaction` events**, not as properties in message payloads. When a user adds a reaction, OpenClaw: - -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 - -The agent sees reactions as **system notifications** in the conversation history, not as message metadata. - -**Configuration:** - -- `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 - -- `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 - -**Forum groups:** Reactions in forum groups include `message_thread_id` and use session keys like `agent:main:telegram:group:{chatId}:topic:{threadId}`. This ensures reactions and messages in the same topic stay together. - -**Example config:** + Disable: ```json5 { channels: { telegram: { - reactionNotifications: "all", // See all reactions - reactionLevel: "minimal", // Agent can react sparingly + configWrites: false, }, }, } ``` -**Requirements:** + -- 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` + + Default: long polling. -## Delivery targets (CLI/cron) + Webhook mode: -- Use a chat id (`123456789`) or a username (`@name`) as the target. -- Example: `openclaw message send --channel telegram --target 123456789 --message "hi"`. + - 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: + +```bash +openclaw message send --channel telegram --target 123456789 --message "hi" +openclaw message send --channel telegram --target @name --message "hi" +``` + + + ## Troubleshooting -**Bot doesn’t respond to non-mention messages in a group:** + + -- If you set `channels.telegram.groups.*.requireMention=false`, Telegram’s Bot API **privacy mode** must be disabled. - - BotFather: `/setprivacy` → **Disable** (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) + - 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:** + -- 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") + -**Bot responds to mentions but not `/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 -- The `/activation` command updates session state but doesn't persist to config -- For persistent behavior, add group to `channels.telegram.groups` with `requireMention: false` + -**Commands like `/status` don't work:** + -- Make sure your Telegram user ID is authorized (via pairing or `channels.telegram.allowFrom`) -- Commands require authorization even in groups with `groupPolicy: "open"` + - 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` -**Long-polling aborts immediately on Node 22+ (often with proxies/custom fetch):** + -- 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. + -**Bot starts, then silently stops responding (or logs `HttpError: Network request ... failed`):** + - 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: -- 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. +```bash +dig +short api.telegram.org A +dig +short api.telegram.org AAAA +``` -## Configuration reference (Telegram) + + -Full configuration: [Configuration](/gateway/configuration) +More help: [Channel troubleshooting](/channels/troubleshooting). -Provider options: +## Telegram config reference pointers -- `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..groupPolicy`: per-group override for groupPolicy (`open | allowlist | disabled`). - - `channels.telegram.groups..requireMention`: mention gating default. - - `channels.telegram.groups..skills`: skill filter (omit = all skills, empty = none). - - `channels.telegram.groups..allowFrom`: per-group sender allowlist override. - - `channels.telegram.groups..systemPrompt`: extra system prompt for the group. - - `channels.telegram.groups..enabled`: disable the group when `false`. - - `channels.telegram.groups..topics..*`: per-topic overrides (same fields as group). - - `channels.telegram.groups..topics..groupPolicy`: per-topic override for groupPolicy (`open | allowlist | disabled`). - - `channels.telegram.groups..topics..requireMention`: per-topic mention gating override. -- `channels.telegram.capabilities.inlineButtons`: `off | dm | group | all | allowlist` (default: allowlist). -- `channels.telegram.accounts..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). +Primary reference: -Related global options: +- [Configuration reference - Telegram](/gateway/configuration-reference#telegram) -- `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`. +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` + +## Related + +- [Pairing](/channels/pairing) +- [Channel routing](/channels/channel-routing) +- [Troubleshooting](/channels/troubleshooting)