Skip to content

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.

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

KeyRequiredWhat it does
bot_tokenyesThe bot token — inline, ${VAR}, or ${file:path}.
chat_idyesChat ID. Stored as a string so negative IDs (groups) round-trip cleanly.
template_pathnoOverride the embedded HTML message template.

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 as your bot.

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

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

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.

Terminal window
export RUNWISP_TG_TOKEN=123456789:AAEa-PXxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
runwisp daemon
[[notifier]]
id = "tg-oncall"
type = "telegram"
bot_token = "${RUNWISP_TG_TOKEN}"
chat_id = "-1001234567890"

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

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 on run.failed and run.timeout only, 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. When external_url is 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.

  • A missing or empty bot_token or chat_id.
  • An id containing : (reserved for inline target overrides) or equal to "inapp" (reserved).