Telegram (Bot API)¶
Status: production-ready for bot DMs + groups via grammY. Long polling is the default mode; webhook mode is optional.
Quick setup¶
@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).
Pairing codes expire after 1 hour.
channels.telegram.groups and groupPolicy to match your access model.
TELEGRAM_BOT_TOKEN only applies to the default account.
Telegram side settings¶
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 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 Telegram user IDs. `telegram:` / `tg:` prefixes are accepted and normalized.
The onboarding wizard accepts `@username` input and resolves it to numeric IDs.
If you upgraded and your config contains `@username` allowlist entries, run `openclaw doctor --fix` to resolve them (best-effort; requires a Telegram bot token).
### 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:
Third-party method (less private): `@userinfobot` or `@getidsbot`.
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`.
`groupAllowFrom` entries must be numeric Telegram user IDs.
Example: allow any member in one specific group:
{
channels: {
telegram: {
groups: {
"-1001234567890": {
groupPolicy: "open",
requireMention: false,
},
},
},
},
}
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:
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 (
sendReadReceiptsdoes not apply).
Feature reference¶
Requirement:
- `channels.telegram.streamMode` is not `"off"` (default: `"partial"`)
Modes:
- `off`: no live preview
- `partial`: frequent preview updates from partial text
- `block`: chunked preview updates using `channels.telegram.draftChunk`
`draftChunk` defaults for `streamMode: "block"`:
- `minChars: 200`
- `maxChars: 800`
- `breakPreference: "paragraph"`
`maxChars` is clamped by `channels.telegram.textChunkLimit`.
This works in direct chats and groups/topics.
For text-only replies, OpenClaw keeps the same preview message and performs a final edit in place (no second message).
For complex replies (for example media payloads), OpenClaw falls back to normal final delivery and then cleans up the preview message.
`streamMode` is separate from block streaming. When block streaming is explicitly enabled for Telegram, OpenClaw skips the preview stream to avoid double-streaming.
Telegram-only reasoning stream:
- `/reasoning stream` sends reasoning to the live preview while generating
- final answer is sent without reasoning text
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`.
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).
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>`
- `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_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.
- 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.
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:
Send sticker action:
Search cached stickers:
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.
ackReaction sends an acknowledgement emoji while OpenClaw is processing an inbound message.
Resolution order:
- `channels.telegram.accounts.<accountId>.ackReaction`
- `channels.telegram.ackReaction`
- `messages.ackReaction`
- agent identity emoji fallback (`agents.list[].identity.emoji`, else "👀")
Notes:
- Telegram expects unicode emoji (for example "👀").
- Use `""` to disable the reaction for a channel or account.
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:
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.
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:
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 numeric `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:
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).openrequires"*".openclaw doctor --fixcan resolve legacy@usernameentries to IDs.channels.telegram.groupPolicy:open | allowlist | disabled(default: allowlist).channels.telegram.groupAllowFrom: group sender allowlist (numeric Telegram user IDs).openclaw doctor --fixcan resolve legacy@usernameentries to 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 whenfalse.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) ornewlineto 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(live stream preview).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 (requireschannels.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 (default127.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:ownwhen not set).-
channels.telegram.reactionLevel:off | ack | minimal | extensive— control agent's reaction capability (default:minimalwhen not set).
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(preview),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