Telegram Bot
This page shows you how to set up the Dispatch Telegram bot, what commands and notifications it provides, and how to manage credentials and the tunnel.
The Dispatch Telegram bot gives you a full command-and-control terminal for your agent fleet inside Telegram. The server pushes alerts the moment a phase blocks, a budget threshold fires, or an epic completes. You can then query, triage, and act using 16 commands. Blocked phases arrive with inline keyboard buttons so you can approve, deny, or kill without opening a browser.
If your agents run headless on a remote machine, this is how you stay in the loop.
Prerequisites
Section titled “Prerequisites”- A Telegram account
- A bot token from
@BotFather cloudflaredinstalled (required only for inbound commands, not for push notifications)- Node.js 20 or later
- Dispatch server running (
npm run dev)
You do not need your chat ID before starting. The bot captures it automatically when you send /start.
Quick Setup: Settings UI (Primary)
Section titled “Quick Setup: Settings UI (Primary)”This is the recommended path for local and development machines. No manual curl commands required.
1. Create a bot via @BotFather
Section titled “1. Create a bot via @BotFather”Open Telegram, search for @BotFather, and send /newbot. Follow the prompts and copy the token it gives you.
2. Start the Dispatch server
Section titled “2. Start the Dispatch server”npm run dev3. Enter credentials in Settings
Section titled “3. Enter credentials in Settings”- Open the Dispatch UI → Settings (gear icon) → Connectors tab.
- Scroll to the Tunnel card.
- Paste your bot token into Bot Token. The value is written directly to
.envand never appears in browser memory as React state. - Click Generate next to Webhook Secret. The server creates a 32-byte random secret, saves it to
.env, and activates it immediately. No server restart required.
4. Start the tunnel
Section titled “4. Start the tunnel”In the Tunnel card, click Start Tunnel. The status badge transitions from stopped → starting → running. Once running:
- The Public URL row shows your assigned
https://...trycloudflare.comaddress. - The Webhook row shows
Registered. Dispatch called Telegram’ssetWebhookAPI automatically.
The Settings UI polls tunnel status periodically while starting, and stops polling once stable.
5. Send /start to your bot
Section titled “5. Send /start to your bot”Open Telegram, find your bot by username, and send /start.
The bot replies:
Dispatch HQ — Town Crier─────────────────────Your chat is now registered. Push notifications enabled.
Send /help for the command grimoire.Setup is complete. The bot registered your chat ID, enabled push notifications, and installed a 6-button quick-action keyboard at the bottom of the chat.
Verify: Settings → Connectors tab should show Status: Connected, Chat ID: your numeric ID, Webhook: Configured.
Alternative: Headless Setup
Section titled “Alternative: Headless Setup”Use this when the Dispatch Settings UI is not available (Docker containers, CI, scripts, or any environment where you need to configure credentials without a browser).
1. Create a bot via @BotFather
Section titled “1. Create a bot via @BotFather”Same as above.
2. Write environment variables
Section titled “2. Write environment variables”Create or update .env at the project root:
TELEGRAM_BOT_TOKEN=your_token_hereTELEGRAM_WEBHOOK_SECRET=your_generated_secret_hereGenerate the webhook secret:
openssl rand -hex 32Set file permissions:
chmod 600 .env3. Start the server
Section titled “3. Start the server”npm run dev4. Start cloudflared manually
Section titled “4. Start cloudflared manually”In a second terminal:
cloudflared tunnel --url http://localhost:<port>Replace <port> with the port Dispatch is running on (check the URL printed in the terminal). Copy the https://...trycloudflare.com URL it prints.
5. Register the webhook
Section titled “5. Register the webhook”curl -X POST "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/setWebhook" \ -H "Content-Type: application/json" \ -d '{ "url": "<YOUR_CF_URL>/telegram/webhook", "secret_token": "<YOUR_WEBHOOK_SECRET>" }'Expected response: {"ok":true,"result":true,"description":"Webhook was set"}
6. Send /start
Section titled “6. Send /start”Find your bot in Telegram and send /start.
Tunnel Management
Section titled “Tunnel Management”Dispatch runs cloudflared as a supervised child process. You do not need a separate terminal.
State machine
Section titled “State machine”stopped → starting → runningstarting → restarting (crash or URL not received within the timeout)restarting → starting (after backoff)restarting → failed (circuit breaker trips after repeated failures)failed → stopped (after manual Reset)any → stopping → stopped (on Stop Tunnel)Auto-restart and backoff
Section titled “Auto-restart and backoff”When cloudflared exits unexpectedly, Dispatch restarts it automatically with exponential backoff plus jitter. The Auto-restarts counter in the Tunnel card tracks how many restarts have occurred.
Circuit breaker
Section titled “Circuit breaker”After repeated restarts in a short window, the circuit breaker trips and the state changes to failed. Automatic restarts stop. To recover, click Reset in the Tunnel card. This resets the restart counter and starts fresh.
Automatic webhook re-registration
Section titled “Automatic webhook re-registration”Each time cloudflared starts and gets a URL, Dispatch calls setWebhook automatically. You do not need to re-register after a crash or restart. The Webhook row shows Registered when this succeeded.
Why the tunnel does not auto-start
Section titled “Why the tunnel does not auto-start”Starting a tunnel creates a public HTTPS endpoint into your local machine. Dispatch requires explicit user action (Start Tunnel button) to avoid creating unintended public ingress.
Status API
Section titled “Status API”Use the Settings → Connectors tab to check tunnel status from the UI. The status shows the current state, public URL, restart count, and last error, without exposing any secrets.
Control actions available from the UI or the server API:
| Action | Effect |
|---|---|
| Check status | Current state (no secrets returned) |
| Start | Start the tunnel (no-op if already running) |
| Stop | Stop the tunnel |
| Reset | Reset the circuit breaker and restart |
Credential Storage
Section titled “Credential Storage”What goes where
Section titled “What goes where”| Secret | Location | Notes |
|---|---|---|
TELEGRAM_BOT_TOKEN | .env at project root | Never in config.json |
TELEGRAM_WEBHOOK_SECRET | .env at project root | Never in config.json |
TELEGRAM_CHAT_ID | ~/.dispatch/config.json | Captured automatically by /start |
File security
Section titled “File security”Dispatch writes .env using an atomic rename (write to .env.tmp, then rename), with mode 0o600 enforced. On POSIX systems, .env is always owner-read/write only after any write by Dispatch.
What each secret does
Section titled “What each secret does”TELEGRAM_BOT_TOKEN authenticates every Telegram Bot API call. It is part of every API URL. Revoke it via @BotFather if compromised.
TELEGRAM_WEBHOOK_SECRET is a 64-character hex string that Telegram sends in X-Telegram-Bot-Api-Secret-Token on every webhook delivery. The server validates it with timingSafeEqual and returns 403 on mismatch. This prevents fake updates from reaching your server even if someone discovers your tunnel URL.
What is never returned
Section titled “What is never returned”- The secrets API responds with key names only, never the values.
- The webhook-secret generator saves the new secret and confirms success. The generated value is never returned in the response.
- The logger scrubs any token-shaped string from every log line before writing it.
Environment Variables
Section titled “Environment Variables”| Variable | Required | Description |
|---|---|---|
TELEGRAM_BOT_TOKEN | Yes | Authenticates Bot API calls. From @BotFather. Never logged or returned in HTTP responses. |
TELEGRAM_WEBHOOK_SECRET | Yes | Telegram sends this in X-Telegram-Bot-Api-Secret-Token on every webhook update. The server compares it with timingSafeEqual. Returns 403 on mismatch. |
TELEGRAM_CHAT_ID | No | Override the chat ID set by /start. Leave unset and let /start handle it automatically. |
Command Reference
Section titled “Command Reference”Telegram shows all commands in autocomplete when you type /.
| Command | Returns |
|---|---|
/status | Fleet HUD: live/warning/idle counts, active epics, session cost, top active agent. |
/agents | Per-agent liveness list sorted by urgency. Paginated with inline nav buttons. |
/agent <name> | Detailed card: liveness, RPG class/level, current task, tokens, cost. Partial name match. |
/warnings | All agents in warning or error state. Returns “All clear” if none. |
Epics and Tasks
Section titled “Epics and Tasks”| Command | Returns |
|---|---|
/epics | Active and recently-completed epics with progress bars. |
/epics paused | ON_HOLD epics only. |
/epics all | Every epic regardless of status. |
/epic <name> | Phase-by-phase breakdown with status, persona, and blocking reason. |
/tasks | Every phase currently IN_PROGRESS across all epics. |
/blocked | All BLOCKED/ON_HOLD phases and warning/error agents. |
Cost and Tokens
Section titled “Cost and Tokens”| Command | Returns |
|---|---|
/cost | Session total with $5 reference bar, per-project breakdown. |
/tokens | Input/output/cache-read totals, cache hit rate, cost by top 3 models. |
Sessions and Projects
Section titled “Sessions and Projects”| Command | Returns |
|---|---|
/sessions | 10 most recent sessions: project, branch, cost, turn count. |
/projects | All projects: epic count, active/blocked phase counts, completion bar. |
/runs | 8 most recent research wave runs with agent ratios. |
Reports and Setup
Section titled “Reports and Setup”| Command | Returns |
|---|---|
/forum | Forum topic mapping list, or setup instructions if none configured. |
/report | Sends dispatch-report-YYYY-MM-DD.txt file attachment with full fleet snapshot. |
/help | All commands grouped by category. |
Push Notifications
Section titled “Push Notifications”The server pushes notifications automatically when fleet events occur. No polling required.
| Event | Default |
|---|---|
agent.error / agent.warning | On |
phase.blocked | On (arrives with inline Approve / Deny / Kill keyboard) |
phase.completed / epic.completed | On |
budget.exceeded ($1/$5/$10/$25/$50) | On |
agent.level_up / achievement.unlocked | On |
session.started / session.ended | Off |
Dedup: Duplicate events within a short window are suppressed.
Phase blocked inline keyboard
Section titled “Phase blocked inline keyboard”| Button | Action |
|---|---|
| [Approve] | Sets phase to IN_PROGRESS. Agent continues. |
| [Deny] | Sets phase to ON_HOLD. Phase paused. |
| [Kill] | Sets phase to CANCELLED. Shows confirmation popup. |
Blocked notifications are also pinned in the chat. The bot keeps at most one pinned notification. When a new one is pinned, the previous is unpinned automatically. Tapping a button removes both the inline keyboard and the pin.
Pinning requires bot admin rights. In a private (direct message) chat it works automatically. If the bot lacks permission, the notification still delivers normally and the pin is silently skipped.
Daily digest
Section titled “Daily digest”At 08:00 local server time, a morning briefing is posted automatically:
Morning Briefing — Tuesday, April 7─────────────────────Live: 3 Warning: 0 Idle: 2Nothing blockedActive Epics: 2 Total Cost: $0.0247─────────────────────Send /status for live HUD or /report for full snapshotFires at most once per calendar day.
Forum Supergroup Topics
Section titled “Forum Supergroup Topics”Route different projects to different topic threads in a Telegram supergroup.
Prerequisites: The chat must be a supergroup with Topics enabled.
Find a topic ID: Right-click the topic header → Copy Link. The last number in the URL (e.g. 42 in .../c/1234567890/42) is the topic ID.
/forum set <projectName> <topicId>/forum clear <projectName>/forumOnce mapped, every notification for that project posts to its topic thread.
Inline Mode
Section titled “Inline Mode”Query Dispatch from any Telegram chat by typing @YourBotName followed by a query.
Enable: Go to @BotFather → /setinline → select your bot → set a placeholder.
| Query | Result |
|---|---|
(empty) or status | Fleet status card with counts and cost |
agents | Up to 5 agents as selectable cards |
blocked | Up to 5 blocked phases |
| any agent/epic name | Finds the first matching agent or epic |
Quick Keyboard
Section titled “Quick Keyboard”After /start, a 6-button reply keyboard appears:
| Row | Buttons |
|---|---|
| 1 | /status · /agents · /blocked |
| 2 | /cost · /tasks · /epics |
Persistent across sessions. Send /start again to reinstall if dismissed.
Generic Webhook Connector
Section titled “Generic Webhook Connector”Post notifications to any HTTPS endpoint. Configure in ~/.dispatch/config.json:
{ "connectors": { "webhook": { "enabled": true, "url": "https://your-endpoint.example.com/dispatch-events", "authToken": "optional-bearer-token" } }}Only https: URLs are accepted.
Security
Section titled “Security”- Webhook secret compared with
timingSafeEqual(timing-safe). Returns403on mismatch. TELEGRAM_BOT_TOKENandTELEGRAM_WEBHOOK_SECRETare never logged or returned in any HTTP response. The logger scrubs every line before writing it./startregisters your chat ID; all other chats receiveUnauthorized.- The
cloudflaredchild process receives onlyPATHandHOME, with no secrets from the parent environment. - Webhook connector rejects non-HTTPS URLs before sending.
Rotate a compromised bot token: Revoke via @BotFather → /mybots → Revoke token → paste new token in Settings → Connectors → Tunnel card → restart tunnel.
Rotate a compromised webhook secret: Click Generate in Settings → Connectors → Tunnel card. If the tunnel is running, Dispatch re-registers the webhook automatically with the new secret.
Verifying the Webhook
Section titled “Verifying the Webhook”curl "https://api.telegram.org/bot<YOUR_BOT_TOKEN>/getWebhookInfo"A healthy response has "pending_update_count": 0 and no last_error_message.
Opening <YOUR_CF_URL>/telegram/webhook in a browser should return {"error":"Forbidden"} with HTTP 403. This confirms the server is up and the route is registered.
Troubleshooting
Section titled “Troubleshooting”Bot does not respond to commands
Send /start first. Without it the chat ID is not registered, so the bot cannot authorize commands or push notifications.
“Not configured” in Settings → Connectors
TELEGRAM_BOT_TOKEN or TELEGRAM_WEBHOOK_SECRET is missing from .env. Use the Tunnel card to set them, or check the file for typos and restart the server.
Tunnel state is failed
The circuit breaker tripped after repeated restarts. Click Reset in the Tunnel card. Check the Last error field for the underlying cause (binary not found, port conflict, network issue).
Tunnel shows running but Webhook shows Not registered
The setWebhook call failed after the tunnel started. Check the Last error field. Common causes: bot token not set, token revoked, Telegram API unreachable. Fix the cause and click Reset.
Quick tunnel URL changed (headless setup)
Re-run setWebhook with the new URL. With the managed tunnel (Settings UI), this is handled automatically. Switch to a named Cloudflare tunnel to avoid URL rotation entirely.
Reactions do not appear
setMessageReaction requires Bot API 7.0+. The bot silences errors from this call, so all other features continue normally.