🏠 Home ← Docs Index
Open Beta 📖 Interactive Reader Raw .md llms.txt

Telegram Channel — Configuration Guide

Integrate GoClaw with Telegram so users can talk to the agent in a private chat or in a group.

Telegram works with no public URL: long polling is the default, so a gateway on 127.0.0.1 or behind NAT can run a live bot.

Prerequisites

1. Create the bot

  1. Open @BotFather in Telegram
  2. Send /newbot, then pick a display name and a username ending in bot
  3. Copy the token it returns — it looks like 123456789:AAE...

2. Configure GoClaw

channels:
  telegram:
    enabled: true
    token: "${TELEGRAM_BOT_TOKEN}"
    mode: "polling"

Store the token outside the config file with a secret reference (${TELEGRAM_BOT_TOKEN} from the environment, or ${secret:TELEGRAM_BOT_TOKEN} from the encrypted vault).

Restart the gateway. The startup log confirms the bot identity:

[telegram] Channel started: @your_bot (mode=polling, require_mention=true)
[telegram] Long polling started (timeout=30s)

Verify the token independently with:

goclaw doctor

3. Talk to it

Search your bot's @username in Telegram and send a message. The first message from an unknown user goes through the normal pairing flow — see Pairing — unless channels.dm_policy is open. The same applies in groups: an unapproved member who addresses the bot gets the pairing reply in the group.

An edited message is handled as a new message, so correcting a question gets it answered again.

Inbound modes

polling (default) webhook
Public URL not needed required (HTTPS)
Setup token only token + secret + URL
Latency ~instant (30s long poll) instant
Best for dev, NAT, single instance production, multiple replicas

Webhook mode

channels:
  telegram:
    enabled: true
    token: "${TELEGRAM_BOT_TOKEN}"
    mode: "webhook"
    webhook_secret: "${TELEGRAM_WEBHOOK_SECRET}"
    webhook_url: "https://bot.example.com"

webhook_secret is required. Telegram echoes it back in the X-Telegram-Bot-Api-Secret-Token header, and GoClaw compares it in constant time; without it, anyone who discovers the hook path could inject fake messages. The channel refuses to start if it is missing. Telegram accepts 1–256 characters, only A-Z, a-z, 0-9, _ and -; with any other character setWebhook fails and the channel does not start.

When webhook_url is set, GoClaw calls setWebhook itself on startup and mounts the receiver at /hook/telegram (or /hook/<slug>/telegram when gateway.webhook_secret_slug is configured). Leave webhook_url empty to register the endpoint yourself. The registration discards messages that queued up while the gateway was down, so they are not answered after a restart.

Switching back to polling is safe: Telegram refuses getUpdates while a webhook is registered, so the channel clears the stale registration on startup.

Group chats

Add the bot to a group and, by default, it stays quiet until addressed:

Set require_mention: false to answer every message in the group — only sensible for a small, dedicated room. All members of a group share one conversation with the agent.

Telegram's privacy mode limits what a bot can see in groups, and it is on by default. For require_mention: false, or if the bot receives nothing, send /setprivacy to BotFather and choose Disable (or make the bot a group admin). Telegram applies the change to a group only after the bot is removed and added to it again.

Attachments

Inbound

Documents, photos, voice notes, audio and video are saved to uploads/ in the workspace (~/.goclaw/workspace/uploads/, or under GOCLAW_STATE_DIR when it is set) and referenced in the prompt, so the agent's document tools can read them and voice_input can transcribe voice notes. The Bot API lets a bot download files of up to 20 MB, so a larger file fails to save and the agent is told so. Filenames are sanitised before they touch disk.

Outbound

Files the agent produces are routed by type:

Condition Method Why
Raster image within Telegram's photo limits sendPhoto Shows an inline preview
Everything else sendDocument Keeps the original bytes and filename

The Bot API caps sendPhoto at 10 MB, a width + height sum of 10000, and a 20:1 ratio. Images outside those bounds — a tall full-page screenshot, a large render — are sent as documents instead of being rejected. Dimensions are read locally before upload; if Telegram still refuses the photo, the file is retried as a document rather than lost. sendDocument accepts up to 50 MB; anything larger fails with an explicit error instead of a silent drop.

A single file with a short caption (≤1024 characters) carries the text on the file bubble. Longer text, or text accompanying several files, is sent as its own message first.

Formatting

Replies are converted from Markdown to Telegram's HTML parse mode — more forgiving than MarkdownV2, which rejects a whole message over a single unescaped character. If Telegram still rejects the markup, the chunk is resent as plain text rather than dropped.

Markdown Telegram
**bold**, *italic*, ~~strike~~ <b>, <i>, <s>
# Heading bold line (Telegram has no heading tag)
`code`, ```lang <code>, <pre><code class="language-…">
> quote <blockquote>
[text](url) <a href="…">
tables aligned <pre> block

Tables

Telegram has no table tag and renders messages in a proportional font, so a raw Markdown table arrives as unreadable pipe soup. Tables are re-rendered as fixed-width text inside <pre>, where the columns line up and the block scrolls horizontally:

Date   Provider  Tokens
─────  ────────  ───────
10-01  gemini    120,450
10-01  openai    18,200

Column alignment (:---, :---:, ---:) is honoured, and widths are measured in characters so Cyrillic headers align correctly. Full-width CJK text still misaligns — one character occupies two monospace cells.

Columns only work while the table fits the screen. A phone shows roughly 40–45 monospace characters, and a table whose cells hold sentences blows well past that — one observed reply padded out to 141 characters, where the separator rule alone wrapped over four lines. Tables wider than that are laid out as one record per row instead, which reads correctly at any width:

payments-api
Check: "error rate over the last hour"
Result: ❌ Above the threshold, 4.2% of requests failed

The row label is bolded and the column names label their values. Because this layout is ordinary text rather than a <pre> block, inline formatting inside cells is converted properly; in the aligned form it is stripped, since <pre> cannot carry tags and the markers would otherwise show as literal asterisks.

Long messages

Messages longer than 4096 characters (the Bot API's unit, not bytes) are split on line boundaries. When a split lands inside a fenced code block, the fence is closed and reopened across the boundary so both halves keep their monospace formatting and language hint.

Pieces are sized by their rendered length, not by the Markdown they came from. Telegram measures the value it receives — the HTML, tags included — so sizing the source at 4096 overshoots on tag-heavy content: a 4090-character source of ordinary bold-and-prose renders to 4438 characters and is rejected. Because that rejection falls back to plain text, one oversized piece would otherwise turn an entire reply into raw Markdown. The same rule applies to file captions.

Configuration reference

Key Default Description
enabled false Turn the channel on
token — BotFather token (required)
mode polling polling or webhook
webhook_secret — Required in webhook mode
webhook_url — Public HTTPS base URL; enables auto-setWebhook
require_mention true In groups, only answer when addressed
bot_name from getMe Display name shown in the dashboard
text_chunk_limit 4096 Outbound chunk size

Troubleshooting

token check failed: telegram API 401 — the token is wrong or was revoked. Re-issue it with /token in BotFather.

Bot is silent in a group — it is not being addressed, or Telegram's privacy mode is hiding messages. Mention it explicitly, or disable privacy mode via BotFather's /setprivacy.

getUpdates failed (retry in 5s): telegram API 409: Conflict: … — two processes are polling the same token. Only one gateway instance can poll a bot; use webhook mode to run several.

Nothing arrives in webhook mode — check that Telegram can reach your URL and that webhook_secret matches on both sides. getWebhookInfo reports the last delivery error:

curl "https://api.telegram.org/bot<TOKEN>/getWebhookInfo"