Telegram
The Telegram driver posts via Telegram’s Bot API. One bot token plus
one chat_id maps to one destination — a personal chat, a group, or
a channel. The setup is the same shape as the
Slack provider; the one thing that
catches people out the first time is finding their chat_id.
Fields
Section titled “Fields”[[notifier]]id = "tg-oncall"type = "telegram"bot_token = "${RUNWISP_TG_TOKEN}"chat_id = "-1001234567890"id, type, bot_token, and chat_id are required. Use
${...} substitution to pull the token
from an env var or a file instead of writing it inline —
storing the secret.
| Key | Required | What it does |
|---|---|---|
bot_token | yes | The bot token — inline, ${VAR}, or ${file:path}. |
chat_id | yes | Chat ID. Stored as a string so negative IDs (groups) round-trip cleanly. |
template_path | no | Override the embedded HTML message template. |
1. Create the bot
Section titled “1. Create the bot”Open Telegram and message @BotFather:
- Send
/newbot. - Pick a display name (humans see this).
- Pick a username — it must end in
bot, e.g.runwisp_ops_bot. - BotFather replies with an HTTP API token like
123456789:AAEa-PXxxxxxxxxxxxxxxxxxxxxxxxxxxxxx. Save it.
Treat the token as a secret. With it, anyone can post as your bot.
2. Find the chat ID
Section titled “2. Find the chat ID”The chat_id identifies where the bot will send messages — a
personal chat, a group, or a channel.
A direct chat with the bot
Section titled “A direct chat with the bot”Message your bot from your account, then visit:
https://api.telegram.org/bot<TOKEN>/getUpdatesLook for "chat": { "id": 123456789, … } in the JSON. That number
is your personal chat_id.
A group chat
Section titled “A group chat”Add the bot to the group, send any message in the group, hit the
same getUpdates URL. Group IDs are negative — they look like
-1001234567890. RunWisp stores chat_id as a string so the
negative number round-trips through TOML cleanly.
A channel
Section titled “A channel”Add the bot as an administrator with “Post Messages” permission, post
a message, hit getUpdates. Channel IDs also start with -100.
3. Store the token
Section titled “3. Store the token”Pick where the token lives and reference it with
${...} substitution.
The simplest option for any deployment — Docker, systemd, bare metal. Set the variable in whatever already manages your environment.
export RUNWISP_TG_TOKEN=123456789:AAEa-PXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxrunwisp daemon[[notifier]]id = "tg-oncall"type = "telegram"bot_token = "${RUNWISP_TG_TOKEN}"chat_id = "-1001234567890"Useful when a secrets manager (Vault agent, sops, Docker
secrets at /run/secrets/...) writes the token to a known
path for you. Relative paths resolve next to runwisp.toml;
~/ works too.
mkdir -p ~/.config/runwispchmod 0700 ~/.config/runwispprintf '%s\n' '123456789:AAEa-PXxx...' > ~/.config/runwisp/tg-oncall.tokenchmod 0600 ~/.config/runwisp/tg-oncall.token[[notifier]]id = "tg-oncall"type = "telegram"bot_token = "${file:~/.config/runwisp/tg-oncall.token}"chat_id = "-1001234567890"The notifier accepts the literal token directly. Avoid it — config files are often committed to git or shared in chat.
[[notifier]]id = "tg-oncall"type = "telegram"bot_token = "123456789:AAEa-PXxx..."chat_id = "-1001234567890"4. Route failures to it
Section titled “4. Route failures to it”Identical to Slack — the routing layer does not care which driver is on the other end:
# On one task:[tasks.backup-postgres]notify_on_failure = ["tg-oncall"]# …
# Or in a notification rule:[[notification_route]]match = { kind = ["run.failed", "run.timeout", "run.crashed"] }notify = ["tg-oncall"]You can list two notifiers in the same array — the router sends to both, and an outage of one channel does not stop delivery on the other:
[tasks.critical-job]notify_on_failure = ["slack-ops", "tg-oncall"]5. Test it
Section titled “5. Test it”runwisp exec smoke-test # something that exits non-zeroTelegram messages usually arrive in 1–2 seconds. If yours doesn’t:
- Check the bell for a
notify.delivery_failedevent with the underlying Telegram API error. - Common causes: wrong
chat_id(Telegram answers400 Bad Request: chat not found), bot not added to the group, bot lacking permission to post in a channel. - Token sanity check:
curl https://api.telegram.org/bot<TOKEN>/getMe— returns the bot’s profile if the token is valid.
What a message looks like
Section titled “What a message looks like”The default Telegram template renders one message per event in a single shape — only the emoji, headline verb, and sentence change between kinds. A failure looks like:
❌ backup-postgres failed
Exited with code 1 after 0.3s.Manually triggered via API · 14 May, 17:11.
> Error: connection refused> dial tcp 127.0.0.1:5432: connect:> connection refused
🔗 View full run
from runwisp · bright-falcon- The captured-output tail (the
<blockquote>block) appears onrun.failedandrun.timeoutonly, capped at three lines / 300 bytes. A missing log file collapses the block silently. - The View run link points at
<external_url>/tasks/<task>/<id>, taking from[daemon] external_url. Whenexternal_urlis unset the link line is omitted — no broken anchors. - The fingerprint footer (
from runwisp · bright-falcon) names the specific daemon that emitted the event. Single-daemon operators can ignore it; multi-daemon operators finally know which box screamed.
template_path overrides the embedded template if you need a different
layout. Copy
telegram.tmpl.html
as your starting point. The helpers statusEmoji, statusVerb,
humanTime, humanDuration, runDuration, triggerPhrase,
eventSentence, eventTrigger, linkLabel, runURL, taskURL,
outputTail, and fingerprint are available inside it.
What the loader rejects
Section titled “What the loader rejects”- A missing or empty
bot_tokenorchat_id. - An
idcontaining:(reserved for inline target overrides) or equal to"inapp"(reserved).