Telegram
Connect an agent to Telegram: BotFather setup, long-polling and slash commands.
Triggers turn an agent into a long-lived service that waits for events, acts on them, delivers the result, and goes idle. The telegram trigger connects that loop to Telegram over Bot API long-polling — outbound HTTPS only, so the agent needs no public endpoint. It watches chats and replies to the messages that wake it. (A second transport, webhook, trades the always-on poll for an inbound HTTPS endpoint — see Transports.)
triggers:
- type: telegram
# chats: ["-1001234567890"] # chat ids, group titles, or @usernames; omit for all
# require_mention: true # only respond when @-mentioned (private chats always respond)
# token_env: TELEGRAM_BOT_TOKEN (default)
# from_users: ["gtchax"] # only handle these authors (user ids or usernames)
# reply_chat: "-100987..." # post replies here instead of the source chat
# allow_silence: true # a reply of exactly __NO_REPLY__ posts nothingOptions
Every key is optional; the defaults give you a bot that answers everyone, in every chat it can see.
| Key | Default | What it does |
|---|---|---|
chats | all chats | Chat ids, group titles, or public @usernames to listen in. This filter applies to private chats too: a private chat's id is the sender's user id, so list that id to keep DMs. |
require_mention | false | Only respond when the bot is @-mentioned. Gates groups only; a private chat always addresses the bot. Pair it with privacy mode (see Setup). |
from_users | anyone | Only handle messages from these authors (user ids or usernames, no @). The filter runs before the model is called, so everyone else's messages cost no tokens. |
token_env | TELEGRAM_BOT_TOKEN | The env var holding the bot token from @BotFather. |
reply_chat | the source chat | Chat id to post replies into. Out-of-chat replies quote the triggering message, and link back to it when the source is a supergroup. |
allow_silence | false | Post nothing when the agent replies with exactly __NO_REPLY__, or with nothing at all. Tell the agent about the sentinel in purpose. |
transport | polling | How updates arrive: polling (long-poll getUpdates) or webhook (inbound HTTP registered via setWebhook). See Transports. |
public_url | — | Webhook transport only, required there: the externally reachable HTTPS base registered with Telegram (e.g. https://my-agent.fly.dev). |
path | /telegram | Webhook transport: the HTTP path Telegram POSTs updates to. |
port | 8080 | Webhook transport: the port to listen on. |
Transports
polling holds a long-poll request open against the Bot API — outbound only, no public endpoint, but the process must be running to receive anything. webhook inverts that: at startup the trigger calls setWebhook to register public_url + path (with a per-boot secret token; deliveries that don't echo it back in X-Telegram-Bot-Api-Secret-Token get a 401), then serves Telegram's POSTs. Everything after ingestion — chats, from_users, require_mention, reply_chat, allow_silence, voice, commands — behaves identically.
The trade-off: polling needs an always-on process; the webhook transport needs a public HTTPS endpoint, and in exchange the host can scale to zero. On platforms that stop idle machines and autostart them on inbound HTTP (Fly.io and friends), Telegram queues undelivered updates (~24h) and retries — the retry is exactly what boots the machine back up, so the webhook registration is deliberately left in place on shutdown. Switching back is safe too: the polling transport deletes any leftover webhook registration at startup, since Telegram refuses getUpdates while one exists.
triggers:
- type: telegram
transport: webhook
public_url: https://my-agent.fly.dev
# path: /telegram (default)
# port: 8080 (default)Multiple HTTP-serving triggers on one agent need distinct port/path combinations, the same rule as the webhook, github and email triggers.
Setup
This is the fastest channel to set up — about 2 minutes:
- Message @BotFather on Telegram, send
/newbot, and follow the prompts. - Copy the token it gives you and export it:
export TELEGRAM_BOT_TOKEN=... - Run the agent:
af run agent.yaml— then message your bot.
For group chats, one more thing: bots have privacy mode on by default, so in groups they only receive messages that @-mention them or reply to them. Either that's what you want (pair it with require_mention: true), or turn it off via BotFather's /setprivacy and re-add the bot to the group.
The agent replies in-chat to the triggering message; conversations are keyed per chat, and memory.scope: thread continues them (Memory). It ignores bots and empty messages; long replies split at Telegram's 4096-char limit. Private chats always address the bot — require_mention only gates groups.
Observer agents
from_users, reply_chat and allow_silence together turn the trigger from a chatbot into an observer: an agent that watches chats, reacts to specific people and reports elsewhere. Observer agents covers the pattern, with a full example.
Voice notes
With a top-level voice block in the agent file, voice notes work here too: the trigger fetches the audio, transcribes it and runs the transcript through the normal loop, and with tts configured it answers a voice note with a voice note. Voice has the setup and the fallbacks. One group-chat caveat: a voice note has no text to carry an @-mention, so with require_mention on, voice conversations live in private chats.
Slash commands
The trigger calls setMyCommands at startup, so Telegram's / menu lists the agent's slash commands with their descriptions. In groups Telegram addresses commands per-bot (/status@your_bot); the trigger strips the suffix before the command is parsed, so both forms work.
Are you an AI? Visit llms.txt — these docs as plain markdown.