Using the agent
Telegram
Set up an allowlisted Telegram gateway, manage conversations and approvals, and deploy it on NixOS.
View as textOn this page
- Prerequisites
- Setup and first message
- Command-line reference
- Allowlisted users
- Groups and topics
- Edits, reactions and missing group updates
- Conversation controls
- Approvals and delivery
- Telegram tool payload reference
- Inline choice and approval recovery
- Recover failed work and delivery
- Checkpoints, dead letters and restoration
- Grant and remove group access
- Attachments
- Media fallback, transcription and cleanup
- NixOS deployment
- Diagnose Telegram
The Telegram gateway connects messages to durable agent sessions on your machine. It runs separately from the terminal interface. Only allowlisted Telegram users can interact with it; provider credentials and the bot token are separate secrets.
Prerequisites
- A Telegram bot created through BotFather and its bot token.
- Your numeric Telegram user ID—not your username.
- A configured model provider account on the gateway host.
- An existing project directory and the agent's Nix-managed environment.
From a repository checkout, enter a shell containing the packaged executable:
nix shell .#agent-telegramRun the following commands inside that shell. Keep the machine running while using the bot.
Setup and first message
Replace the path and numeric ID with your own values:
agent-telegram setup --provider openai --cwd /path/to/project \
--allowed-user 123456789
agent-telegram start
agent-telegram statusSetup prompts for the token without echoing it, validates it against Telegram, and stores it separately from non-secret configuration. Never paste the token into an agent conversation, commit it, or include it in a screenshot.
- Confirm that
statusreports the gateway running. - Open a private chat with the bot and send
/status. - Send
Explain the top-level files in this project. Do not edit anything. - Send
/sessionand retain the identifier if you need to diagnose the conversation.
A Telegram reply verifies delivery. A response that cites the intended project files verifies the configured working directory. These are separate checks.
Command-line reference
agent-telegram run stays in the foreground; no subcommand also runs the configured gateway. Use this under an existing supervisor or for diagnosis, not alongside a second background process for the same bot. start launches the background gateway, stop stops it and status inspects it. --help shows usage; --version reports the build.
| Setup option | Use |
|---|---|
--provider NAME | openai, xai, openrouter, gemini or claude-code. |
--model NAME | Override the provider's default model; verify availability for the service account. |
--cwd PATH | Project directory on the gateway host. |
--effort LEVEL | Optional model reasoning effort. |
--allowed-user ID | Repeat for numeric user IDs. |
--yolo | Automatically approve mutations. |
--deny-mutations | Deny mutations rather than prompting. Choose one approval policy; do not combine policy flags. |
--all-group-messages | Consider allowed-user ambient group messages. |
--workers N | Concurrent chat workers, 1–64; default 8. Work within one conversation remains ordered. |
--start | Start after setup completes. |
To change setup values, stop the gateway, rerun setup with the complete intended configuration, inspect status/configuration, then restart. For a NixOS instance, edit its declaration instead of competing with generated settings.
Allowlisted users
Repeat --allowed-user during setup to add multiple IDs. To inspect or change the local allowlist afterward:
agent-telegram users list
agent-telegram users add 123456789
agent-telegram users remove 987654321
agent-telegram stop
agent-telegram startLocal CLI allowlist edits require a gateway restart. In a group, an already allowed member can use /allow with a name, username, or replied-to message; /users lists allowed and observed people, and /deny removes access.
Allowlisting grants access to an agent operating on the gateway host. Grant it only to people authorized to use that project's files and connected services.
Chat commands and gateway tools update the running gateway's global allowlist, not a chat-only membership list. They save gateway state and attempt to update an existing gateway config.json with mode 0600. A configuration write failure is logged as allowlist_config_persist_failed; inspect it before restarting rather than assuming a successful chat response proves both files were updated. The last allowed user cannot be removed. With NixOS-managed configuration, also update the declarative allowedUsers setting: an in-chat edit does not edit your Nix configuration.
Groups and topics
By default, mention the bot, address a command to its username, or reply to one of its messages. Ambient group messages are ignored. Each private chat, group, and forum topic maps to a persisted session.
For ambient group participation, pass --all-group-messages during setup. Telegram must also deliver those messages: disable BotFather privacy mode and re-add the bot when required. This does not remove the user allowlist.
The bot remains in a group or channel only when an allowlisted Telegram administrator added it. An anonymous-admin add is accepted only when an allowlisted user is already an administrator of the chat.
Edits, reactions and missing group updates
New and edited messages still require an allowlisted sender and an authorized group. An edit can replace a pending message with the same chat/message identity; it does not undo a turn already run or reverse its side effects. To correct completed work, send an explicit follow-up naming what should change and inspect the result.
Only original, unforwarded text without voice/media can act as immediate stop control input. Quoted text, an edited message, a caption or forwarded stop is not an interrupt. For example, editing yesterday's message into /stop is not equivalent to sending a fresh /stop now. Commands explicitly addressed to another bot are ignored.
Reactions from allowlisted users are represented as text identifying the target message and new emoji/custom emoji, including a distinct reaction-removed message. Reaction updates use the chat's unthreaded key because their classification has no topic identifier; do not assume a reaction resumes the original forum topic's session. Reactions without an identifiable allowed user are ignored. A thumbs-up reaction is conversation input, not a substitute for an approval button.
If mentions work but ordinary group conversation does not, first check the configured ambient-response policy. Then check whether Telegram delivers those updates to the bot under its privacy and membership settings. The gateway cannot process an update Telegram never sends. Test with an explicit mention from an allowlisted user in the intended topic before broadening delivery or access. Ambient input allows the agent to remain silent rather than requiring a reply to every message.
Conversation controls
| Message | Effect |
|---|---|
/start | Show a short introduction and available conversation controls |
/new | Start a fresh session for this conversation |
/session | Show the persisted session identifier |
/status | Report current work |
/retry | Requeue the latest failed turn |
/stop or stop | Interrupt the current turn without shutting down the gateway |
In a group, reply stop to the bot or address /stop@your_bot_username. Cancellation is scoped to that chat or topic; other conversations keep running. The plain word is case-insensitive and may have surrounding whitespace.
Approvals and delivery
Mutating tools ask through inline approval buttons by default. --deny-mutations disables them; --yolo auto-approves them. Approval callbacks are bound to the originating conversation and an allowlisted user.
Updates and pending replies are persisted before processing. Pending work, retries, callback bindings, delivery checkpoints, and dead letters survive restarts. Messages in one conversation are processed in order; separate chats use a bounded worker pool.
If delivery is uncertain after a failure, inspect the conversation before retrying a request that could send a message, change a file, or modify an external service.
Telegram tool payload reference
These are model-facing tools available during managed Telegram turns with a gateway bridge, not Telegram slash commands or public HTTP endpoints. The current conversation supplies chat/topic and requesting-user context; none accepts a chat_id override. Examples describe payloads, not completed sends.
| Tool | JSON fields | Outcome and recovery |
|---|---|---|
send_telegram_document | Required path string; optional caption, filename strings. | Sends a downloadable file; success reports sent, usually with message ID. Inspect the chat before repeating an uncertain send. |
send_telegram_photo | Required path string; optional caption, filename strings. | Sends a Telegram photo. If Telegram rejects the format, explain the error and offer document delivery rather than claiming success. |
send_telegram_voice | Required path string; optional caption, filename strings. | Sends prepared audio as a voice reply; it does not synthesize audio from text or promise arbitrary codec support. |
react_to_telegram_message | Required emoji string; optional integer message_id. | One standard Telegram reaction emoji; defaults to triggering message. Missing/nonpositive target fails. Inspect target and emoji restrictions before retrying. |
ask_telegram_choice | Required question string and options array of strings. | Displays 1–8 short inline choices and waits for the authorized user's answer. Not approval to perform unrelated mutations. |
allow_telegram_user | Optional query string (name, @username or numeric ID), optional integer user_id. | Changes allowlist; use one unambiguous target. Omitting target uses replied-to user context. Inspect list afterward. |
deny_telegram_user | Optional query string, optional integer user_id. | Revokes allowlist membership. Verify exact identity and resulting list; do not infer success solely from a natural-language acknowledgment. |
list_telegram_users | Empty object. | Lists allowed and recently observed chat users, allowing ambiguity to be resolved before granting access. |
For all three send tools, path must be an absolute existing file under the private session temporary directory. Canonical path checks reject symlink escapes outside that root. Generate or copy the authorized output into the actual session directory; filename changes the download name, not the source path or destination chat.
{"path":"/actual/session-temp/report.pdf","caption":"Requested report","filename":"report.pdf"}{"question":"Which output do you want?","options":["Summary","Full report"]}{"emoji":"👍","message_id":123}{"user_id":123456789}Substitute real paths and IDs; ask the agent for the tool operation rather than pasting these objects as chat commands. Inspect its tool result and the actual Telegram message or allowlist. File transport/API errors are separate from generation success and may require a supported format or smaller artifact.
Inline choice and approval recovery
Choice labels are trimmed, empty labels removed, and only the first eight retained; display labels are capped at 48 characters. Choose distinct short labels to avoid ambiguous buttons. A question with no nonempty option fails. Bindings expire after 30 minutes and are tied to the requesting user and chat. Another user clicking the button cannot substitute their consent.
If a button is expired, from a previous turn, or no longer resolves after restart, inspect /status and the gateway journal first. Ask for a new question in the active conversation rather than repeatedly clicking or interpreting silence as approval. Approval and filesystem-access callbacks are host-mediated requests, not extra public send-tool fields. Check the requested tool/path and scope before choosing; an ordinary question answer grants no broad tool permission. For uncertain delivery, follow the recovery sequence below before issuing another send.
Recover failed work and delivery
- Send
/statusin the affected chat/topic. It reports the session, queued actions, retrying actions and failed turns available for retry. Inspect approval messages and host logs separately before submitting another request. - Inspect the host's gateway logs or systemd journal. Redact tokens, message text and attachment paths before sharing diagnostics.
- Verify remote side effects separately: a failed reply delivery does not establish that the preceding tool operation failed.
- When retry is appropriate, send
/retryin that conversation. It requeues the latest retained failed action, not an arbitrary earlier message.
The response distinguishes no failed turn, a failure without a retryable action, and a queued retry. Pending actions, checkpoints and dead letters are in ~/.haskell-agent/gateways/telegram/state.json. Preserve it during diagnosis; do not edit or delete it while the gateway is writing. A restart retains delivery state but cannot prove that an uncertain external mutation is safe to repeat.
Checkpoints, dead letters and restoration
A failed pending action is retried with persisted retry metadata. At five failures it moves to a dead letter containing its update ID, chat, error, failure timestamp and retained action; leaving a chat is exempt from this five-attempt cutoff. /retry selects the latest failure for the current conversation, removes it from the dead-letter list and queues the retained action under the new update ID. This may be a reply delivery rather than a model turn. It is not a general-purpose replay of any historical message.
Long text replies are split at 4096 rendered characters. Delivery checkpoints record the next chunk after each successful send or edit, keyed by chat, topic and update ID. Restarting the same pending delivery can skip its recorded chunks. A crash after Telegram accepts a send but before its checkpoint is saved can still duplicate a message. A manually requeued action has a new update ID, so do not assume its old chunk checkpoint suppresses duplicates.
- For suspected state corruption or data loss, stop the gateway and verify no other host is polling with the same bot token. Preserve the damaged state and logs with restricted access before attempting recovery.
- Restore a consistent backup of the gateway state and its corresponding session database using the isolated restore checklist. Do not restore only a checkpoint map or erase the queue to make an error disappear.
- Before reconnecting, compare the restored checkpoint time with Telegram conversation history and external tool effects. Messages or mutations after that time may already have succeeded and can be replayed from older state.
- Resume only one gateway after resolving those differences. Check
/status, logs and the destination conversation. Use/retryonly for a specifically reconciled retained failure.
There is no documented selective dead-letter purge or arbitrary checkpoint repair command. Do not improvise JSON edits against production state; retain a backup and escalate an unreconciled queue to a maintainer. Treat state, dead-letter payloads, session transcripts and backups as sensitive content. Apply your retention policy to all of them together rather than assuming temporary attachment cleanup erases the conversation or failed action.
Approval buttons must belong to the originating conversation and be pressed by an allowed user. A stale, already resolved or mismatched button is not permission to repeat a mutation. Inspect status and request a fresh operation if necessary.
Grant and remove group access
An already allowed group member can reply to the intended person's message with /allow, then inspect /users. Reply-based selection avoids guessing between similar display names. Names/usernames must resolve to the intended observed member; inspect the result before granting host access. Use /deny for that member to revoke access. For deterministic local administration use agent-telegram users add ID or users remove ID and restart.
Revocation changes subsequent admission; it is not a cancellation command for an already running turn and does not roll back completed tool effects. If active work must stop, an allowed user should interrupt the affected conversation separately, then inspect its result and any external changes. Treat a removed user's earlier instructions as potentially already executed.
Attachments
The gateway accepts photos, documents, audio, video, video notes, animations, stickers, locations, contacts, venues, polls, dice, edited messages, and reactions. Images are provided natively to multimodal providers; other files use Responses input_file content or a private local-path fallback.
Gateway tools can send documents, photos, and voice files, react to messages, ask inline-button questions, and manage users. Bot credentials remain in the parent gateway process and are not inherited by the agent child.
Inbound downloaded voice and file payloads are bounded to 20 MiB. Oversized input is rejected rather than silently read in full. Telegram delivery and the selected model can impose additional format/capability limits. For a first attachment check, send a small non-sensitive image or text document and ask the agent to identify it without modifying files. Confirm an actual attachment-backed result, not merely a plausible description.
For outbound files, identify the exact local file and intended conversation in your request, review any approval, and verify receipt before retrying. Photos, documents and voice are distinct Telegram delivery types. Provider file fallback may expose a private local path to the agent; it is not a public download URL.
Media fallback, transcription and cleanup
Telegram voice messages use the gateway's xAI transcription path before the coding turn. This is separate from choosing a model for the resulting text. An empty transcription is an error, not an empty successful instruction. If a voice message fails while text works, check xAI authentication and the transcription error first; changing the coding model alone does not replace this transcription path. Send the intended text explicitly if voice cannot be decoded, rather than asking the model to guess what was said.
Downloads are bounded to 20 MiB per payload and media batches use at most four concurrent downloads. Files are placed in the session temporary directory with update/index-based names. Missing MIME information falls back to application/octet-stream; a filename extension is not proof that the provider can decode its contents. Locations, contacts, venues, polls and dice can contribute descriptive content without a downloadable binary file.
For example, a small PDF sent through a Responses-capable connection can be supplied as file content; on a connection without that path, the agent may instead need to inspect its private local file using available tools. Ask it to identify which evidence it actually read. If a video or animated sticker cannot be understood, provide a supported still image or a text transcript; successful Telegram download does not guarantee model-level video or animation understanding.
Temporary voice downloads are removed after transcription, including failure. Partially downloaded media batches are cleaned up on failure. Do not treat temporary paths as a durable archive or assume cleanup of a download removes transcript text, conversation history, backups or copies made by tools. Keep original files separately if they must survive session cleanup, and apply your own retention policy to gateway state and backups.
NixOS deployment
Add the agent flake as an input, import its module, and declare an instance. The example below is a NixOS module fragment; haskell-agent is the flake input.
{ pkgs, ... }: {
imports = [ haskell-agent.nixosModules.telegram ];
services.haskell-agent.telegram.instances.assistant = {
enable = true;
workingDirectory = "/srv/project";
tokenFile = "/run/secrets/telegram-bot-token";
allowedUsers = [ 123456789 ];
provider = "openai";
yolo = false;
extraPackages = with pkgs; [ nix ripgrep ];
};
}The module creates a dedicated user and private home, loads the token with systemd credentials, supplies Bash/Git/PostgreSQL 18, and manages an isolated PostgreSQL cluster. The token file stays outside the Nix store. Provision provider credentials separately, using the instance's login state or environmentFiles.
Instance names start with a lowercase letter, use lowercase letters, digits, and hyphens, and have at most 16 characters. Paths must be absolute. The project must exist before startup; if automatic approvals are enabled, it must be writable by the service user. Do not enable automatic approvals simply to work around directory ownership.
See the complete NixOS option reference for model/effort overrides, worker-host environment, PostgreSQL, credentials and declarative MCP ownership.
systemctl status haskell-agent-telegram-assistant.service
journalctl -u haskell-agent-telegram-assistant.service -n 100 --no-pagerThe default instance home is /var/lib/haskell-agent-telegram-assistant. Its .haskell-agent/gateways/telegram/ directory contains configuration and delivery state; .haskell-agent/postgres/ contains the managed database. Back up database contents with PostgreSQL tools, not an ordinary copy of a live data directory. Back up gateway state.json separately and protect secret recovery.
Diagnose Telegram
| Symptom | Check |
|---|---|
| Setup rejects token | Use the BotFather token for this bot and check host connectivity to Telegram; never paste the token into a support report |
| Private messages ignored | Confirm the gateway is running and the sender's numeric ID is allowlisted |
| Group messages ignored | Mention or reply to the bot; check privacy mode only if ambient messages are intentionally enabled |
| Bot leaves the group | Check whether an allowlisted Telegram administrator added it |
| Provider request fails | Configure the model account for the gateway's service user; a valid Telegram token is not a model credential |
| Turn waits without progressing | Check for an unanswered inline approval; use /status and cancel with /stop if appropriate |
| NixOS service cannot start | Inspect the journal, token path, existing working directory, ownership, and provider environment file |