What you get
A Telegram message (private chat or group) when an Actions job fails: repo, branch, and a link to the run.
No server of your own. A Telegram bot, two secrets stored in GitHub, and a few more lines in your GitHub Actions setup are enough.
- A job fails in Actions
- A step calls sendMessage
- Telegram shows the alert
If bot/token/chat_id are still fuzzy, read Alerts with a Telegram bot.
Requirements
- A GitHub repo. You do not need existing workflows: this is where you create your first
- A Telegram account — the bot is created inside the chat itself, in a couple of minutes
- Permission to add secrets in that repo, or someone who can add them for you
Create the bot
- In Telegram, open @BotFather (opens in a new tab).
/newbot→ name and a username that ends inbot.- Copy the token.
- BotFather gives you a
t.me/…link: open it, tap Start (or sendhello). If you use a group, add the bot and allow it to send messages; groupids are usually negative.
Get the chat_id
Paste the BotFather token where it says paste-your-token-here. Pick your OS in the block.
TOKEN="paste-your-token-here"
curl -sS "https://api.telegram.org/bot${TOKEN}/getUpdates"Look for a fragment like this (your numbers will differ):
"chat": {
"id": 123456789,
"type": "private"
}That "id" is the chat_id. If result is empty, send another message to the bot and try again. In PowerShell, with $r from above, you can also check $r.result.message.chat.id (or the first item in result if it comes as a list).
You can verify the destination before touching Actions (also replace 123456789 with your chat_id):
TOKEN="paste-your-token-here"
CHAT_ID="123456789"
curl -sS -X POST "https://api.telegram.org/bot${TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "{\"chat_id\":${CHAT_ID},\"text\":\"Test from the integration\"}"From here the OS tabs disappear: workflows run on GitHub's machine, which is Linux, so the commands are always the same even if you are on Windows.
Store them in GitHub Secrets
In the repo: Settings → Secrets and variables → Actions:
| Name | Value |
|---|---|
TELEGRAM_BOT_TOKEN | Token from BotFather |
TELEGRAM_CHAT_ID | The chat_id you got |
If the names do not match, the workflow below will not see them.
Add the workflow
Create .github/workflows/telegram-on-failure.yml:
name: Telegram alert on CI failure
on:
workflow_dispatch:
jobs:
# Fails on purpose so you can verify the alert.
# Swap this for your real job later.
fail-on-purpose:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Force failure (test only)
run: exit 1
notify-telegram:
needs: fail-on-purpose
if: failure()
runs-on: ubuntu-latest
steps:
- name: Notify Telegram
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
run: |
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
echo "Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID."
exit 1
fi
text=$(printf 'GitHub Actions failed\n%s · branch %s\n%s/%s/actions/runs/%s' \
"${{ github.repository }}" \
"${{ github.ref_name }}" \
"${{ github.server_url }}" \
"${{ github.repository }}" \
"${{ github.run_id }}")
payload=$(jq -n \
--arg chat "$TELEGRAM_CHAT_ID" \
--arg text "$text" \
'{chat_id: $chat, text: $text}')
curl -sS -X POST \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "$payload"What is worth noticing in that block:
| Piece | Role |
|---|---|
needs: + if: failure() | The pair that does the trick: needs says which job to wait for, if: failure() that it should only run when that one failed. Without this it would alert every time |
| The two secrets | Token and destination. Never written in the file: the YAML is visible in the repo |
jq + curl | Build the message and call sendMessage. Both come installed on GitHub's machine, nothing to add |
Try it
- Create the file in the repo: Add file → Create new file → path `.github/workflows/telegram-on-failure.yml` → paste the YAML → Commit (or push with git).
- In Actions, open Telegram alert on CI failure → Run workflow.
- Wait for fail-on-purpose to fail (expected) and notify-telegram to finish green.
- Check Telegram: message with repo, branch, and link.
Wire it into your CI
When the test alert works, drop fail-on-purpose. Below are paste-ready YAML blocks: adapt the job command; the secrets stay the same.
One job (usual)
name: Telegram alert on CI failure
on:
workflow_dispatch:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
notify-telegram:
needs: test
if: failure()
runs-on: ubuntu-latest
steps:
- name: Notify Telegram
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
run: |
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
echo "Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID."
exit 1
fi
text=$(printf 'GitHub Actions failed\n%s · branch %s\n%s/%s/actions/runs/%s' \
"${{ github.repository }}" \
"${{ github.ref_name }}" \
"${{ github.server_url }}" \
"${{ github.repository }}" \
"${{ github.run_id }}")
payload=$(jq -n \
--arg chat "$TELEGRAM_CHAT_ID" \
--arg text "$text" \
'{chat_id: $chat, text: $text}')
curl -sS -X POST \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "$payload"needs: test = wait for test.
if: failure() = run only if test failed.
If test is green, Telegram stays quiet.
Several jobs
The alert waits on several jobs and fires if any of them fails:
name: Telegram alert on CI failure
on:
workflow_dispatch:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run build
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run lint
notify-telegram:
needs: [test, build, lint]
if: failure()
runs-on: ubuntu-latest
steps:
- name: Notify Telegram
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
run: |
if [ -z "$TELEGRAM_BOT_TOKEN" ] || [ -z "$TELEGRAM_CHAT_ID" ]; then
echo "Missing TELEGRAM_BOT_TOKEN or TELEGRAM_CHAT_ID."
exit 1
fi
text=$(printf 'GitHub Actions failed\n%s · branch %s\n%s/%s/actions/runs/%s' \
"${{ github.repository }}" \
"${{ github.ref_name }}" \
"${{ github.server_url }}" \
"${{ github.repository }}" \
"${{ github.run_id }}")
payload=$(jq -n \
--arg chat "$TELEGRAM_CHAT_ID" \
--arg text "$text" \
'{chat_id: $chat, text: $text}')
curl -sS -X POST \
"https://api.telegram.org/bot${TELEGRAM_BOT_TOKEN}/sendMessage" \
-H "Content-Type: application/json" \
-d "$payload"When it runs
This is not a third flow. It is the on: block in the same file.
What you put in on: | Effect |
|---|---|
workflow_dispatch only | You start it by hand (like the test) |
push to main only | Runs only on push to main |
| Both | Manual tests and real alerts on main |
The “one job” / “several jobs” examples already include both. If you are still on the test YAML (manual only), add push when the message looks right.
Plain text
The examples use plain text (no parse_mode). That is the safest choice with repo/branch names that contain _ or /. If you later use HTML or MarkdownV2, escape anything that comes from GitHub.
Security
- A bot dedicated to CI limits the blast radius.
- This flow does not verify GitHub signatures: anyone with the token can write to the
chat_id. - Avoid spam: one message per failed job is usually enough.
Common mistakes
| What you see | Likely cause | What to do |
|---|---|---|
No chat_id in getUpdates | You did not Start the bot | Open the chat, Start, try again |
Unauthorized | Token pasted wrong | Re-enter the secret |
chat not found | chat_id from another chat or bot | Re-fetch the id with getUpdates |
notify-telegram never runs | Previous job did not fail | Check needs + if: failure() |
| Actions green, Telegram silent | Wrong chat, or the watched job did not fail | Confirm chat_id and the run graph |
| Duplicate message | You re-ran the workflow | Expected here; there is no deduping |
| 400 from Telegram | Malformed JSON or parse_mode without escaping | Use the example block with jq and plain text |
Alternatives
- Another chat Incoming Webhook
- Same CI alert, different destination (e.g. Discord or Slack).
- GitHub email
- Zero setup; easier to ignore.
- Telegram group vs private chat
- Group for the team; private for alerts only you see.