Skip to content

Telegram ​

Telegram works in both modes:

ModeWhat it does
NotificationSingle-direction sendMessage from the alert pipeline.
IM bridgeTwo-way chat with the agent via getUpdates long-poll.

The same bot token (8…:AA…, from @BotFather) drives both — Telegram authenticates by token-in-path. There's no separate signing scheme.

Designed for users outside Feishu / DingTalk territory

Telegram is the IM channel Ongrid recommends for non-China teams and for hybrid deployments where the operators happen to live on Telegram. The provider was added in ADR-031.

Notification mode ​

A notify.Sender POSTs {chat_id, text} to https://api.telegram.org/bot<TOKEN>/sendMessage. The credential is the bot token (in the path); the chat_id (numeric) is the destination chat.

go
// internal/pkg/notify/webhook.go
NewTelegramSender(name, endpoint, chatID, client)

Where endpoint is the literal …/bot<TOKEN>/sendMessage URL and chatID is the target chat (a user's numeric ID for a DM, or the negative number Telegram gives you for a group).

Setup:

  1. DM @BotFather → /newbot → pick a username → copy the token.
  2. Add the bot to the target chat (group / channel) or DM it once so it knows about you.
  3. Grab the chat ID. The quickest way: send any message in the chat, then curl https://api.telegram.org/bot<TOKEN>/getUpdates and read result[0].message.chat.id.
  4. In Ongrid: Settings → Channels → New → Provider = telegram → Endpoint = https://api.telegram.org/bot<TOKEN>/sendMessage, chat_id = the number from step 3.

IM bridge mode (two-way) ​

Inbound is long-polled from the manager's outbound HTTPS connection. Because the call is outbound, Telegram works behind NAT, firewalls, and HTTPS proxies. setWebhook (the alternative) would require Telegram to reach the manager — that's incompatible with most private-cloud deployments and unreliable from mainland China.

stream is the only mode ​

The validator rejects anything else:

telegram only supports stream mode

Webhook mode is not exposed in the UI for Telegram.

Credential mapping ​

The im_apps row reuses existing columns — no schema changes:

im_apps columnTelegram meaning
provider"telegram"
mode"stream" (validator pins this)
app_idBot username (e.g. ongridbot). Display + dedupe.
app_secretBotFather token (8…:AA…). Encrypted at rest.
allow_fromRequired comma-separated numeric user IDs.
verify_tokenUnused.
encrypt_keyUnused.

allow_from ​

A non-empty list of numeric Telegram user IDs that may converse with the bot. The validator drops non-numeric tokens (so a typo'd alice lands as a clean required-error, not a silently-empty allowlist) and rejects negative tokens (those are group / supergroup chat IDs and don't belong in a sender allowlist).

telegram requires allow_from — at least one numeric Telegram user ID (the bot is publicly reachable; an empty allowlist would let anyone command the agent)

How to find a numeric user ID:

  • DM @userinfobot and it replies with your numeric ID.
  • Or send a message in the chat after registering, run curl https://api.telegram.org/bot<TOKEN>/getUpdates, and read result[0].message.from.id.

telegram: / tg: prefixes are silently stripped (OpenClaw compatibility), so telegram:123456789 and 123456789 are the same entry.

Silent drop on miss ​

Non-allowlisted senders are dropped with a WARN log:

text
telegram inbound from non-allowlisted sender — ignored
  user_id=42  user_name=alice  chat_id=42

There is no reply. There is no placeholder. The bot does not even confirm it exists. This mirrors OpenClaw allowFrom — a reply would leak that an agent-backed bot lives at this username and tempt people to probe it.

Setup ​

  1. DM @BotFather → /newbot → choose a name and a unique username ending in bot. Copy the token.
  2. (Optional) @BotFather → /setprivacy → Disable if you want the bot to see messages in groups (default is mention-only).
  3. Find your numeric user ID via @userinfobot.
  4. In Ongrid: Settings → IM bridge → New → Provider = telegram → Mode = stream → App ID = the bot username → App secret = the BotFather token → allow_from = your numeric ID (and any teammates'). Save and Enable.
  5. DM the bot from an allowlisted account. The bot replies with a placeholder, then edits it in place as the agent reasons.

The proxy story ​

The Telegram API host (api.telegram.org) is reachable on most networks but blocked from mainland China. The provider uses a zero-value http.Client so it honors HTTPS_PROXY / HTTP_PROXY / NO_PROXY from the manager's environment — the same proxy that carries getUpdates carries sendMessage.

In docker-compose deployments, persist the proxy in docker-compose.override.yml:

yaml
services:
  manager:
    environment:
      HTTPS_PROXY: http://your-proxy:8080
      NO_PROXY: localhost,127.0.0.1,manager,mysql,prometheus,loki,tempo,grafana,qdrant

Don't put the proxy in the main docker-compose.yml

The main docker-compose.yml is shipped with every release. An override file is per-environment and survives make package / install script reruns.

Quirks worth knowing ​

message is not modified is harmless ​

Telegram returns HTTP 400 (message is not modified) when an editMessageText payload exactly matches the current message text. Progressive streaming can repeat the same chunk on a throttled tick or on the final flush. The client swallows exactly this error string and returns nil — anything else (400, 403, 5xx) still propagates. See EditMessageText.

Only one poller per bot ​

Telegram rejects concurrent getUpdates calls. The StreamSupervisor enforces one client per im_app row. If you duplicate a Telegram app row, only one will poll successfully — the other will see 409 Conflict and back off.

Rate-limit retries ​

429 Too Many Requests is retried up to 3 times. The wait honors Telegram's parameters.retry_after, capped at 60s. 5xx errors retry with 1s / 2s / 4s backoff. Hard 4xx (400/401/403) does not retry — it bubbles to the supervisor and likely indicates a token / chat-id issue. See maxCallRetries.

Long-poll timeout ​

The server-side long poll waits 25s (pollTimeoutSec) with a 10s buffer on the client side. Slow networks just see longer poll cycles; the supervisor isn't doing busy-wait reconnects.

Text-only ​

Stickers, photos, voice notes, and files are dropped silently. Only message.text events drive an agent turn. The bridge is intentionally S1 (text in / text out); rich media is a future expansion.