Skip to main content

Telegram

WednesdayAI connects to Telegram via the Bot API using long polling (default) or webhooks. Setup requires a bot token from BotFather — there is no QR scanning or phone-number linking. The channel runs via grammY with per-chat sequencing.

Prerequisites

  • A Telegram bot token from BotFather
  • Node.js 24+ on the gateway host
  • Outbound HTTPS access to api.telegram.org from the gateway

Quick setup

1

Create a bot in BotFather

Open Telegram and message @BotFather. Run /newbot, follow the prompts, and save the bot token (format: 123456789:AABBCCxyz...).
2

Configure the bot token and access policy

// ~/.openclaw/openclaw.json
{
  channels: {
    telegram: {
      enabled: true,
      botToken: "123456789:AABBCCxyz...",
      dmPolicy: "pairing",           // pairing | allowlist | open | disabled
      allowFrom: ["123456789"],       // numeric Telegram user IDs
      groupPolicy: "allowlist",
      groups: {
        "*": { requireMention: true },
      },
    },
  },
}
Env fallback (default account only): TELEGRAM_BOT_TOKEN=...To read the token from a file instead: channels.telegram.tokenFile: "/run/secrets/telegram-token".
3

Start the gateway

openclaw gateway run
Long polling starts automatically. No webhook URL is needed for the default mode.
4

Approve the first pairing request

openclaw pairing list telegram
openclaw pairing approve telegram <CODE>
Pairing codes expire after 1 hour.

Finding Telegram user IDs

Allowlists require numeric Telegram user IDs, not usernames.
# Method 1: DM your bot and watch the logs
openclaw logs --follow
# Look for "from.id" in the output

# Method 2: Bot API
curl "https://api.telegram.org/bot<TOKEN>/getUpdates"

# Method 3: third-party bots
# @userinfobot or @getidsbot (less private)
@username entries in allowFrom or groupAllowFrom are not valid at runtime. If your config has them, run openclaw doctor --fix to resolve them to numeric IDs (requires a live bot token). openclaw doctor --fix can also recover allowlist entries from pairing-store files when migrating to allowlist mode.

Access control

DM policy

dmPolicyEffect
"pairing"New senders receive a one-time pairing code; ignored until approved (default)
"allowlist"Only IDs in allowFrom can DM (must have at least one entry)
"open"Any sender allowed (requires allowFrom: ["*"])
"disabled"Bot ignores all DMs
dmPolicy: "allowlist" with empty allowFrom is a config validation error — it would silently block all DMs. Per-account override: channels.telegram.accounts.<id>.dmPolicy and channels.telegram.accounts.<id>.allowFrom override channel-level defaults for that account. Named accounts do not inherit accounts.default.allowFrom — they inherit channels.telegram.allowFrom when account-level values are unset.

Group policy

{
  channels: {
    telegram: {
      groupPolicy: "allowlist",            // open | allowlist | disabled
      groupAllowFrom: ["123456789"],        // numeric user IDs
      groups: {
        "*": { requireMention: true },
        "-1001234567890": {
          requireMention: false,
          groupPolicy: "open",
        },
      },
    },
  },
}
Two controls apply together:
  1. Group membership (channels.telegram.groups) — if present, acts as an allowlist; "*" allows all groups. If absent with groupPolicy: "open", all groups pass.
  2. Sender policy (groupPolicy + groupAllowFrom) — if groupAllowFrom is unset, falls back to allowFrom.
Group sender auth does not inherit DM pairing-store approvals (since v2026.2.25). Pairing is DM-only. For groups, set groupAllowFrom or per-group allowFrom explicitly.
If channels.telegram is completely absent from config, the runtime group-policy fallback is "allowlist" (fail-closed), even if channels.defaults.groupPolicy is set.

Mention gating

Group messages require a mention by default. To disable per group or globally:
{ channels: { telegram: { groups: { "*": { requireMention: false } } } } }
Mention sources: native @botusername, configured mentionPatterns, or reply-to-bot. Session-level toggle (owner-gated): /activation always or /activation mention.

BotFather settings

Telegram bots default to Privacy Mode — they only see messages that mention them or start with /. If the bot must see all group messages:
  • BotFather: /setprivacy → select your bot → Disable
  • Or make the bot a group admin
Remove and re-add the bot in each group after changing privacy mode. openclaw channels status warns when Privacy Mode may be causing missed messages.
  • /setjoingroups — allow or deny the bot from being added to groups
  • /setprivacy — toggle group message visibility
  • /setcommands — the gateway sets this automatically when commands.native is enabled

Native and custom commands

{
  commands: {
    native: "auto",   // enable native command menu for Telegram
  },
  channels: {
    telegram: {
      customCommands: [
        { command: "briefing", description: "Daily briefing" },
        { command: "status", description: "Check status" },
      ],
    },
  },
}
Custom commands are menu entries only — they do not auto-implement behavior. Plugin and skill commands can still be used when typed even if not in the Telegram menu. Command names must be lowercase a-z, 0-9, _, length 1–32. setMyCommands failed at startup means outbound DNS/HTTPS to api.telegram.org is blocked.

Live streaming

{
  channels: {
    telegram: {
      streaming: "partial",   // off | partial | block | progress
    },
  },
}
ModeBehavior
partial (default)DMs: native draft streaming via sendMessageDraft. Groups: preview message + in-place edits.
blockLegacy preview mode (full block send with edits).
progressMaps to partial on Telegram.
offNo streaming; final reply only.
Telegram native draft streaming (sendMessageDraft) is available for all bots since Bot API 9.5 (March 2026). If it is unavailable or rejected, WednesdayAI falls back to sendMessage + editMessageText automatically.

Webhook mode

Default is long polling. To use webhooks:
{
  channels: {
    telegram: {
      webhookUrl: "https://your-public-domain.com/telegram-webhook",
      webhookSecret: "a-long-random-secret",      // required
      webhookPath: "/telegram-webhook",            // default
      webhookHost: "127.0.0.1",                    // default; set 0.0.0.0 for direct ingress
      webhookPort: 8787,                           // default
    },
  },
}
The local webhook listener binds to webhookHost:webhookPort. If your public endpoint differs, place a reverse proxy in front and point webhookUrl at the public URL. Restart the gateway for webhook changes to take effect.

Multi-account setup

{
  channels: {
    telegram: {
      dmPolicy: "allowlist",
      accounts: {
        personal: { botToken: "111:AAA", allowFrom: ["123456789"] },
        work:     { botToken: "222:BBB", allowFrom: ["987654321"] },
      },
    },
  },
}
Account selection default: default if present, otherwise the first configured account id (sorted). Per-account dmPolicy, allowFrom, groupAllowFrom, and capabilities overrides are supported. Named accounts inherit channels.telegram.allowFrom (not accounts.default.allowFrom) when unset.

Inline buttons

{
  channels: {
    telegram: {
      capabilities: {
        inlineButtons: "allowlist",  // off | dm | group | all | allowlist (default)
      },
    },
  },
}
Callback button clicks are delivered to the agent as text: callback_data: <value>.

Reaction notifications

{
  channels: {
    telegram: {
      reactionNotifications: "own",   // off | own | all (default: own)
      reactionLevel: "minimal",       // off | ack | minimal | extensive (default: minimal)
    },
  },
}
own = reactions to messages the bot sent (best-effort via sent-message cache). Reaction events still respect dmPolicy/allowFrom/groupPolicy — unauthorized senders are dropped.

Actions and config writes

{
  channels: {
    telegram: {
      actions: {
        reactions: true,
        sendMessage: true,
        deleteMessage: true,
        sticker: false,               // default: false
      },
      configWrites: true,             // default: true — allow group migration writes etc.
    },
  },
}
The editMessage and createForumTopic actions default to true but can be disabled: actions: { editMessage: false, createForumTopic: false }

Delivery settings

{
  channels: {
    telegram: {
      textChunkLimit: 4000,           // chars per outbound chunk
      chunkMode: "length",            // length | newline (paragraph-aware)
      mediaMaxMb: 5,                  // inbound media cap (MB)
      linkPreview: true,              // enable link previews
      timeoutSeconds: 30,             // Telegram API client timeout
      replyToMode: "off",             // off | first | all
      retry: {
        attempts: 3,
        minDelayMs: 500,
        maxDelayMs: 5000,
      },
    },
  },
}
DM and group history injection:
{
  channels: {
    telegram: {
      historyLimit: 50,               // group context history (0 = disabled)
      dmHistoryLimit: 0,              // DM history injection
    },
  },
}

Network and proxy

{
  channels: {
    telegram: {
      proxy: "socks5://user:pass@proxy-host:1080",   // route Bot API calls through proxy
      network: {
        autoSelectFamily: true,        // Node 22+ default; set false on WSL2
        dnsResultOrder: "ipv4first",   // ipv4first | verbatim
      },
    },
  },
}
Environment overrides (temporary): OPENCLAW_TELEGRAM_DISABLE_AUTO_SELECT_FAMILY=1, OPENCLAW_TELEGRAM_DNS_RESULT_ORDER=ipv4first.

Disabling without removing config

{ channels: { telegram: { enabled: false } } }
Restart the gateway: systemctl --user restart openclaw-gateway.

Troubleshooting

  1. Check BotFather: /setprivacy — must say Disabled (or make the bot a group admin)
  2. Remove and re-add the bot in the group after changing privacy mode
  3. channels.telegram.groups must include the group ID (or "*")
  4. requireMention: false if you want responses without mentions
Check channels.telegram.groups — the group must be listed or "*" used. Get the group ID from openclaw logs --follow (field chat.id). Run openclaw doctor for duplicate key issues in openclaw.json.
Confirm the sender’s numeric ID is in allowFrom. Command authorization applies even when groupPolicy: "open". setMyCommands failed means DNS/HTTPS to api.telegram.org is blocked.
Check for IPv6 routing issues: dig +short api.telegram.org AAAA. If broken, force IPv4:
{ channels: { telegram: { network: { dnsResultOrder: "ipv4first" } } } }
On VPS hosts with unstable egress, route through a proxy:
{ channels: { telegram: { proxy: "socks5://user:pass@proxy-host:1080" } } }
Forum topics use session key agent:<id>:telegram:group:<chatId>:topic:<threadId>. Check that replyToMode is set correctly and that the topic has the right per-topic overrides under groups.<chatId>.topics.<threadId>.

Configuration reference

Full field index for channels.telegram.*:
  • Auth: enabled, botToken, tokenFile, accounts.*
  • Access: dmPolicy, allowFrom, groupPolicy, groupAllowFrom, groups, groups.*.topics.*
  • Commands: commands.native, commands.nativeSkills, customCommands
  • Streaming: streaming
  • Delivery: textChunkLimit, chunkMode, linkPreview, mediaMaxMb, timeoutSeconds, replyToMode, retry.*
  • History: historyLimit, dmHistoryLimit, dms.<id>.historyLimit
  • Actions: actions.reactions, actions.sendMessage, actions.deleteMessage, actions.sticker
  • Capabilities: capabilities.inlineButtons
  • Reactions: reactionNotifications, reactionLevel
  • Network: proxy, network.autoSelectFamily, network.dnsResultOrder
  • Webhook: webhookUrl, webhookSecret, webhookPath, webhookHost, webhookPort
  • Misc: configWrites, defaultTo
  • Reference: config