Skip to main content
Channel plugin · listing tai42/channel-telegram

Install

Permissions

Provides

telegram

Channel — Deliver ask questions to a Telegram chat and bridge the typed reply back. Bridge a Telegram bot to an agent, or deliver ask questions into a chat. A message runs an agent turn and the answer comes back in the same chat.

Enable it

manifest.yml

Configuration

The CHANNEL_TELEGRAM_ env group. CHANNEL_TELEGRAM_REDIS_URL (and the inherited connection-tuning fields) point the plugin-owned correlation store. CHANNEL_TELEGRAM_BOT_TOKEN, CHANNEL_TELEGRAM_WEBHOOK_SECRET and CHANNEL_TELEGRAM_PUBLIC_BASE_URL are required, and so is at least one of CHANNEL_TELEGRAM_DEFAULT_RECIPIENT and CHANNEL_TELEGRAM_ALLOWED_RECIPIENTS: the startup hook refuses to boot without them, naming the missing variable.

Webhook registration is automatic

Unlike the other channels, Telegram’s webhook is registered automatically: on startup the plugin calls setWebhook (idempotent) to point Telegram at {PUBLIC_BASE_URL}/api/channels/telegram/inbound with the secret token. You do not configure it by hand. A failed setWebhook call, including an unreachable Bot API, aborts startup.

Create a bridge route

A Telegram route’s our_identity is the bot’s numeric id — the digits before the : in the bot token. client_address is the numeric chat id, so each chat holds its own conversation:
In Studio, pick telegram as the route’s Channel; the Our identity field then shows this hint.

Rich messages

A notification’s rich shapes map onto native Bot API sends: tappable options render as an inline keyboard (reply options as callback buttons, link options as url buttons), sections degrade to grouped buttons with each title as a text header (Telegram has no native sectioned list), a media header rides the body as its caption when it fits (else a separate message), a footer is a trailing italic line, file media sends by kind (sendPhoto/sendDocument/sendVideo/sendAudio), and a location is sendLocation (or sendVenue with a name and address). Telegram has no vendor templates and no ask-less form notifications, so the channel declines those capabilities rather than silently dropping them. A bridged tap on an option with an author-set id carries that id back as params.reply_id (and a sectioned row’s description as params.reply_description) — opaque enrichment a tool target can read from the turn’s params. Entry params are bounded at the door (at most 16 keys, each value at most 512 characters, the whole set at most 2048 bytes). Because Telegram redelivers a failed webhook, this door drops rather than refuses: an over-cap value is dropped on its own, an over-bound set is dropped whole, and the participant’s message still bridges. Every drop is logged at WARNING (naming the door and the bound, never a value) and counted on tai_entry_params_dropped_total{door="telegram"}.

Inbound media

A message carrying media is fetched from Telegram and ingested into the platform’s served media. A photo (largest size), document, audio, voice, video, video_note, animation, or sticker has its bytes pulled — via getFile and the file host — through the ingestion seam, which caps, sniffs, stores, and mints a served reference. The message then bridges as one turn whose text is the caption when present, else a non-blank placeholder ([voice message], [document: <name>], or the bracketed kind), carrying BOTH a typed attachment (the served MediaItem a tool consumer reads as structured content) and the parity media_* entry-params: media_kind (image/document/audio/video/sticker), media_id (the served media reference, not the raw Telegram file_id), media_mime_type (the sniffed-and-agreed type), media_size (the actual bytes stored), and, where they apply, media_filename (the sanitised display name), media_voice (a voice note), and sticker_animated. The placeholder label and media_filename both use the sanitised name, never the raw vendor filename. A media message that replies to a pending question answers it just as a text reply does: the ask is resolved with the caption (or the placeholder) as the answer text and the media_* parameters, while the typed attachment rides only a fresh-turn bridge (the answer carries text and parameters). A media reply that cannot be received leaves the ask pending. If the media cannot be received, the participant gets one generic notice and a platform event records why — too_large (over the operator’s per-kind cap), unsupported_type (a disallowed or mismatched type), or could_not_receive (a gone/expired file, a store outage, or an unfetchable source). A caption on a rejected media is still bridged as a text-only turn, so the participant’s words are never lost. A transient failure (a timeout, a 5xx, or a torn read) is not acked, so Telegram redelivers the update — the update id dedupes a redelivery that later succeeds. Content Telegram can send that this channel does not map to a media kind — a poll, dice, venue, contact, location, or game — is not bridged: the participant gets one generic “unsupported” notice and a platform event records the drop, so nothing is silently lost.

Message ids and addresses

The conversation bridge records every message this channel sends to a participant. These are the ids and addresses it records:
  • An inbound message passes its message_id as the medium message id.
  • resolve_address returns the bot’s numeric id and the target as the caller names it, which may be an @username. Telegram resolves the target only when it sends: a completed send reports the numeric chat.id from the Bot API response, the id an inbound message from that chat carries, and the message_id of each message sent. A send to an @username is therefore recorded in the thread of its numeric chat.

Replies

A message sent as a reply carries reply_to_message.message_id, the id of the message it quotes in the same chat. The channel hands that id to the platform beside the message, and the platform resolves it against the conversation’s own records into a typed reply_to ({message_id, status, text}): a tool target reads it in its payload, an agent target through start_expr. An inline-keyboard tap is a reply to the message holding the keyboard. A message posted in a forum topic, whose reply_to_message Telegram fills with the topic’s first message, is a reply only when the participant replied to another message. A reply to a message of another chat (external_reply) names no message of this chat, so it quotes none. When the reply answers a pending question, the asker receives the same reply_to beside the unchanged answer, and the reply’s entry parameters (a tap’s reply_id / reply_description, a media reply’s media_*) as its params.

Working-on-it signal

While a message runs its turn, the platform keeps Telegram’s typing action alive: it re-issues sendChatAction "typing" ahead of the action’s ~5 s lifetime until the reply is sent, so the chat shows the bot typing for the whole wait rather than for a single burst at receipt. The refresh is bounded by the CONVERSATIONS_WORKING_SIGNAL_MAX_SECONDS ceiling (0 disables it entirely), with CONVERSATIONS_WORKING_SIGNAL_REFRESH_MARGIN_SECONDS the margin it refreshes ahead of each lapse.

The bot token stays out of logs

The Bot API carries the token in every request path (/bot<id>:<secret>/…), and httpx logs each request line at INFO. Loading the channel registers a token mask with the kit’s transport-log redaction (tai42_kit.logging.register_transport_redaction), so every httpx/httpcore record shows /bot<redacted>/… before any handler formats it.

See also