Skip to content

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.

  • A Telegram account
  • A bot token from @BotFather
  • cloudflared installed (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.

This is the recommended path for local and development machines. No manual curl commands required.

Open Telegram, search for @BotFather, and send /newbot. Follow the prompts and copy the token it gives you.

Terminal window
npm run dev
  1. Open the Dispatch UI → Settings (gear icon) → Connectors tab.
  2. Scroll to the Tunnel card.
  3. Paste your bot token into Bot Token. The value is written directly to .env and never appears in browser memory as React state.
  4. 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.

In the Tunnel card, click Start Tunnel. The status badge transitions from stoppedstartingrunning. Once running:

  • The Public URL row shows your assigned https://...trycloudflare.com address.
  • The Webhook row shows Registered. Dispatch called Telegram’s setWebhook API automatically.

The Settings UI polls tunnel status periodically while starting, and stops polling once stable.

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.

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).

Same as above.

Create or update .env at the project root:

TELEGRAM_BOT_TOKEN=your_token_here
TELEGRAM_WEBHOOK_SECRET=your_generated_secret_here

Generate the webhook secret:

Terminal window
openssl rand -hex 32

Set file permissions:

Terminal window
chmod 600 .env
Terminal window
npm run dev

In a second terminal:

Terminal window
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.

Terminal window
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"}

Find your bot in Telegram and send /start.

Dispatch runs cloudflared as a supervised child process. You do not need a separate terminal.

stopped → starting → running
starting → 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)

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.

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.

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.

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.

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:

ActionEffect
Check statusCurrent state (no secrets returned)
StartStart the tunnel (no-op if already running)
StopStop the tunnel
ResetReset the circuit breaker and restart
SecretLocationNotes
TELEGRAM_BOT_TOKEN.env at project rootNever in config.json
TELEGRAM_WEBHOOK_SECRET.env at project rootNever in config.json
TELEGRAM_CHAT_ID~/.dispatch/config.jsonCaptured automatically by /start

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.

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.

  • 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.
VariableRequiredDescription
TELEGRAM_BOT_TOKENYesAuthenticates Bot API calls. From @BotFather. Never logged or returned in HTTP responses.
TELEGRAM_WEBHOOK_SECRETYesTelegram 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_IDNoOverride the chat ID set by /start. Leave unset and let /start handle it automatically.

Telegram shows all commands in autocomplete when you type /.

CommandReturns
/statusFleet HUD: live/warning/idle counts, active epics, session cost, top active agent.
/agentsPer-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.
/warningsAll agents in warning or error state. Returns “All clear” if none.
CommandReturns
/epicsActive and recently-completed epics with progress bars.
/epics pausedON_HOLD epics only.
/epics allEvery epic regardless of status.
/epic <name>Phase-by-phase breakdown with status, persona, and blocking reason.
/tasksEvery phase currently IN_PROGRESS across all epics.
/blockedAll BLOCKED/ON_HOLD phases and warning/error agents.
CommandReturns
/costSession total with $5 reference bar, per-project breakdown.
/tokensInput/output/cache-read totals, cache hit rate, cost by top 3 models.
CommandReturns
/sessions10 most recent sessions: project, branch, cost, turn count.
/projectsAll projects: epic count, active/blocked phase counts, completion bar.
/runs8 most recent research wave runs with agent ratios.
CommandReturns
/forumForum topic mapping list, or setup instructions if none configured.
/reportSends dispatch-report-YYYY-MM-DD.txt file attachment with full fleet snapshot.
/helpAll commands grouped by category.

The server pushes notifications automatically when fleet events occur. No polling required.

EventDefault
agent.error / agent.warningOn
phase.blockedOn (arrives with inline Approve / Deny / Kill keyboard)
phase.completed / epic.completedOn
budget.exceeded ($1/$5/$10/$25/$50)On
agent.level_up / achievement.unlockedOn
session.started / session.endedOff

Dedup: Duplicate events within a short window are suppressed.

ButtonAction
[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.

At 08:00 local server time, a morning briefing is posted automatically:

Morning Briefing — Tuesday, April 7
─────────────────────
Live: 3 Warning: 0 Idle: 2
Nothing blocked
Active Epics: 2 Total Cost: $0.0247
─────────────────────
Send /status for live HUD or /report for full snapshot

Fires at most once per calendar day.

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>
/forum

Once mapped, every notification for that project posts to its topic thread.

Query Dispatch from any Telegram chat by typing @YourBotName followed by a query.

Enable: Go to @BotFather/setinline → select your bot → set a placeholder.

QueryResult
(empty) or statusFleet status card with counts and cost
agentsUp to 5 agents as selectable cards
blockedUp to 5 blocked phases
any agent/epic nameFinds the first matching agent or epic

After /start, a 6-button reply keyboard appears:

RowButtons
1/status · /agents · /blocked
2/cost · /tasks · /epics

Persistent across sessions. Send /start again to reinstall if dismissed.

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.

  • Webhook secret compared with timingSafeEqual (timing-safe). Returns 403 on mismatch.
  • TELEGRAM_BOT_TOKEN and TELEGRAM_WEBHOOK_SECRET are never logged or returned in any HTTP response. The logger scrubs every line before writing it.
  • /start registers your chat ID; all other chats receive Unauthorized.
  • The cloudflared child process receives only PATH and HOME, 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.

Terminal window
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.

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.