Telegram
The telegram driver delivers via Telegram’s Bot API. One bot token
plus one chat_id maps to one destination — a personal chat, a group,
or a channel. This page is both the field reference and the setup
walkthrough. The pattern is the same shape as the
Slack provider; the wrinkles are
around finding your chat_id and parse_mode = "MarkdownV2",
both of which trip people up the first time.
Fields
Section titled “Fields”[[notifier]]id = "tg-oncall"type = "telegram"bot_token_env = "RUNWISP_TG_TOKEN"chat_id = "-1001234567890"parse_mode = "HTML"| Key | Type | Required | What it does |
|---|---|---|---|
bot_token | string | one-of (see below) | Inline bot token. |
bot_token_env | string | one-of | Name of an env var holding the token. |
bot_token_file | string | one-of | Path to a file containing the token. Relative to the data dir. |
chat_id | string | yes | Chat ID. Stored as a string so negative IDs (group chats) round-trip cleanly. |
parse_mode | enum | no | "HTML" (default — matches the embedded template) or "MarkdownV2". |
template_path | string | no | Override the embedded message template. |
id and type are the
common fields
shared with every driver. Exactly one of bot_token,
bot_token_env, bot_token_file must be set; supplying two is a
config-load error, supplying none is also an error.
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 messages as your bot.
2. Find the chat ID
Section titled “2. Find the chat ID”This is the part nobody documents well. 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, then send any message in the group. Hit
the same getUpdates URL. Group chat_ids are negative — they
look like -1001234567890. RunWisp stores chat_id as a string
exactly so negative IDs round-trip cleanly through TOML.
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 outside runwisp.toml
Section titled “3. Store the token outside runwisp.toml”Same options as Slack — env, file, or (don’t) inline:
Env (recommended)
Section titled “Env (recommended)”RUNWISP_TG_TOKEN=123456789:AAEa-PXxxxxxxxxxxxxxxxxxxxxxxxxxxxxxsudo install -d -o runwisp -g runwisp -m 0700 /etc/runwisp/secretsecho '123456789:AAEa-PXxx...' \ | sudo install -m 0600 -o runwisp -g runwisp /dev/stdin /etc/runwisp/secrets/tg-oncall.token4. Declare the notifier
Section titled “4. Declare the notifier”[[notifier]]id = "tg-oncall"type = "telegram"bot_token_env = "RUNWISP_TG_TOKEN"chat_id = "-1001234567890"parse_mode = "HTML" # default — matches the embedded templateparse_mode defaults to "HTML" because the embedded message
template ships in HTML. Leave it unset and you get sane formatting.
For the file variant:
[[notifier]]id = "tg-oncall"type = "telegram"bot_token_file = "/etc/runwisp/secrets/tg-oncall.token"chat_id = "-1001234567890"5. Route failures to it
Section titled “5. Route failures to it”Identical to Slack — the routing layer doesn’t know or care which driver is on the other end:
# Per-task sugar:[tasks.backup-postgres]notify_on_failure = ["tg-oncall"]# …
# Or an explicit route:[[notification_route]]match = { kind = ["run.failed", "run.timeout", "run.crashed"] }notify = ["tg-oncall"]You can list both notifiers in the same array — the router delivers to each, and an outage of one channel doesn’t suppress delivery on the other:
[tasks.critical-job]notify_on_failure = ["slack-ops", "tg-oncall"]The parse_mode = "MarkdownV2" trap
Section titled “The parse_mode = "MarkdownV2" trap”If you switch parse_mode to "MarkdownV2", you must also set
a template_path — the embedded default template uses HTML, and
without your own template Telegram will render the literal <b>…</b>
tags in your messages.
[[notifier]]id = "tg-oncall"type = "telegram"bot_token_env = "RUNWISP_TG_TOKEN"chat_id = "-1001234567890"parse_mode = "MarkdownV2"template_path = "/etc/runwisp/templates/telegram-mdv2.tmpl"MarkdownV2 escaping is
notoriously painful — 18 characters require escaping with
backslashes, including dots and parentheses. Stay on HTML
unless you have a strong reason; HTML’s “escape <, >, &”
rule is much friendlier.
Smoke-test
Section titled “Smoke-test”runwisp exec smoke-test # something that exits non-zeroTelegram messages usually arrive in 1–2 seconds. If yours doesn’t:
- Check the in-app 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.
Telegram-specific validation
Section titled “Telegram-specific validation”Caught at config load:
parse_mode = "MarkdownV2"set without atemplate_path— the embedded template is HTML, so leaving it active emits literal<b>…</b>tags.- All of the common provider rules apply too.
Where to next
Section titled “Where to next”- Slack provider — same shape, in case you want both channels.
[[notification_route]]reference — more flexible routing than per-task sugar.- Notifications model — the cycle-break that prevents Telegram outages from triggering more Telegram alerts.