Skip to content

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.

[[notifier]]
id = "tg-oncall"
type = "telegram"
bot_token_env = "RUNWISP_TG_TOKEN"
chat_id = "-1001234567890"
parse_mode = "HTML"
KeyTypeRequiredWhat it does
bot_tokenstringone-of (see below)Inline bot token.
bot_token_envstringone-ofName of an env var holding the token.
bot_token_filestringone-ofPath to a file containing the token. Relative to the data dir.
chat_idstringyesChat ID. Stored as a string so negative IDs (group chats) round-trip cleanly.
parse_modeenumno"HTML" (default — matches the embedded template) or "MarkdownV2".
template_pathstringnoOverride 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.

Open Telegram and message @BotFather:

  1. Send /newbot.
  2. Pick a display name (humans see this).
  3. Pick a username — it must end in bot, e.g. runwisp_ops_bot.
  4. 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.

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.

Message your bot from your account, then visit:

https://api.telegram.org/bot<TOKEN>/getUpdates

Look for "chat": { "id": 123456789, … } in the JSON. That number is your personal chat_id.

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.

Add the bot as an administrator (with “Post Messages” permission), post a message, hit getUpdates. Channel IDs also start with -100.

Same options as Slack — env, file, or (don’t) inline:

/etc/runwisp/runwisp.env
RUNWISP_TG_TOKEN=123456789:AAEa-PXxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Terminal window
sudo install -d -o runwisp -g runwisp -m 0700 /etc/runwisp/secrets
echo '123456789:AAEa-PXxx...' \
| sudo install -m 0600 -o runwisp -g runwisp /dev/stdin /etc/runwisp/secrets/tg-oncall.token
[[notifier]]
id = "tg-oncall"
type = "telegram"
bot_token_env = "RUNWISP_TG_TOKEN"
chat_id = "-1001234567890"
parse_mode = "HTML" # default — matches the embedded template

parse_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"

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"]

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.

Terminal window
runwisp exec smoke-test # something that exits non-zero

Telegram messages usually arrive in 1–2 seconds. If yours doesn’t:

  • Check the in-app bell for a notify.delivery_failed event with the underlying Telegram API error.
  • Common causes: wrong chat_id (Telegram answers 400 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.

Caught at config load:

  • parse_mode = "MarkdownV2" set without a template_path — the embedded template is HTML, so leaving it active emits literal <b>…</b> tags.
  • All of the common provider rules apply too.