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.

Flow
  1. A job fails in Actions
  2. A step calls sendMessage
  3. 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

  1. In Telegram, open @BotFather (opens in a new tab).
  2. /newbot → name and a username that ends in bot.
  3. Copy the token.
  4. BotFather gives you a t.me/… link: open it, tap Start (or send hello). If you use a group, add the bot and allow it to send messages; group ids are usually negative.

Get the chat_id

Paste the BotFather token where it says paste-your-token-here. Pick your OS in the block.

Language: bash
TOKEN="paste-your-token-here"
curl -sS "https://api.telegram.org/bot${TOKEN}/getUpdates"

Look for a fragment like this (your numbers will differ):

Language: json

"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):

Language: bash
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: SettingsSecrets and variablesActions:

NameValue
TELEGRAM_BOT_TOKENToken from BotFather
TELEGRAM_CHAT_IDThe 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:

Language: yaml

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:

PieceRole
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 secretsToken and destination. Never written in the file: the YAML is visible in the repo
jq + curlBuild the message and call sendMessage. Both come installed on GitHub's machine, nothing to add

Try it

  1. 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).
  2. In Actions, open Telegram alert on CI failure → Run workflow.
  3. Wait for fail-on-purpose to fail (expected) and notify-telegram to finish green.
  4. 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)

Language: yaml

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:

Language: yaml

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 onlyYou start it by hand (like the test)
push to main onlyRuns only on push to main
BothManual 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 seeLikely causeWhat to do
No chat_id in getUpdatesYou did not Start the botOpen the chat, Start, try again
UnauthorizedToken pasted wrongRe-enter the secret
chat not foundchat_id from another chat or botRe-fetch the id with getUpdates
notify-telegram never runsPrevious job did not failCheck needs + if: failure()
Actions green, Telegram silentWrong chat, or the watched job did not failConfirm chat_id and the run graph
Duplicate messageYou re-ran the workflowExpected here; there is no deduping
400 from TelegramMalformed JSON or parse_mode without escapingUse 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.

Resources