What you get
A Discord channel that shows a red message when a workflow fails: repo, branch, and a link to the GitHub run.
No server of your own, and no Discord bot. Two things are enough: an Incoming Webhook — a secret address Discord gives you to post into a channel, with no other permissions — and a few more lines in your GitHub Actions setup.
- A job fails in Actions
- A step POSTs to the webhook
- Discord shows the message
If webhooks are still fuzzy, read Introduction to webhooks first.
Requirements
- A GitHub repo. You do not need existing workflows: this is where you create your first
- A Discord channel where you are an admin — admins are the ones who can create webhooks
- Permission to add secrets in that repo, or someone who can add it for you
Create the Discord webhook
- Open the channel where you want the alert.
- Channel gear → Integrations → Webhooks → New Webhook (labels vary slightly by client).
- Give it a clear name, e.g.
CI failures. - Copy Webhook URL.
That URL is a secret. Anyone who has it can post to the channel. Do not paste it into chat, an issue, or the repo.
Test the webhook locally
Before Actions, confirm the URL posts to the channel. Replace the entire quoted value with the URL Discord gave you (it starts with https://discord.com/api/webhooks/ — do not paste it on top of a prefix).
WEBHOOK_URL="paste-your-full-webhook-url"
curl -sS -o /dev/null -w "%{http_code}\
" -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-d "{\"content\":\"Test from the integration\"}"Expected: 204 — which in Discord means “received, nothing to return” — and the message visible in the channel.
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 it in GitHub Secrets
- In the repo: Settings → Secrets and variables → Actions.
- New repository secret.
- Exact name:
DISCORD_WEBHOOK_URL. - Value: the URL you copied. Save.
If the secret has another name, the workflow below will not see it.
Add the workflow
Create .github/workflows/discord-on-failure.yml (filename is up to you) with:
name: Discord 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-discord:
needs: fail-on-purpose
if: failure()
runs-on: ubuntu-latest
steps:
- name: Notify Discord
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
run: |
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
echo "Missing secret DISCORD_WEBHOOK_URL."
exit 1
fi
payload=$(jq -n \
--arg repo "${{ github.repository }}" \
--arg branch "${{ github.ref_name }}" \
--arg url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
'{
embeds: [{
title: "GitHub Actions failed",
description: ("**" + $repo + "** · branch `" + $branch + "`"),
url: $url,
color: 15158332
}]
}')
curl -sS -X POST \
-H "Content-Type: application/json" \
-d "$payload" \
"$DISCORD_WEBHOOK_URL"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 |
DISCORD_WEBHOOK_URL | Pulls the URL from GitHub Secrets. Never written in the file: the YAML is visible in the repo |
jq + curl | Build the message and send it. Both come installed on GitHub's machine, nothing to add |
color: 15158332 | The red stripe down the side of the message, in decimal. Change it if you prefer another |
Try it
- Create the file in the repo: Add file → Create new file → path `.github/workflows/discord-on-failure.yml` → paste the YAML → Commit (or push with git).
- In Actions, open Discord alert on CI failure → Run workflow.
- Wait for fail-on-purpose to fail (expected) and notify-discord to finish green.
- Check Discord: you should see the red embed 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: just adapt your job command (or job names); the secret stays the same.
One job (usual)
Swap npm test for your real command. You can paste this whole block:
name: Discord 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-discord:
needs: test
if: failure()
runs-on: ubuntu-latest
steps:
- name: Notify Discord
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
run: |
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
echo "Missing secret DISCORD_WEBHOOK_URL."
exit 1
fi
payload=$(jq -n \
--arg repo "${{ github.repository }}" \
--arg branch "${{ github.ref_name }}" \
--arg url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
'{
embeds: [{
title: "GitHub Actions failed",
description: ("**" + $repo + "** · branch `" + $branch + "`"),
url: $url,
color: 15158332
}]
}')
curl -sS -X POST \
-H "Content-Type: application/json" \
-d "$payload" \
"$DISCORD_WEBHOOK_URL"needs: test = wait for test.
if: failure() = run only if test failed.
If test is green, Discord stays quiet.
Several jobs
Same idea, but the alert waits on several jobs. Discord fires if any of them fails:
name: Discord 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-discord:
needs: [test, build, lint]
if: failure()
runs-on: ubuntu-latest
steps:
- name: Notify Discord
env:
DISCORD_WEBHOOK_URL: ${{ secrets.DISCORD_WEBHOOK_URL }}
run: |
if [ -z "$DISCORD_WEBHOOK_URL" ]; then
echo "Missing secret DISCORD_WEBHOOK_URL."
exit 1
fi
payload=$(jq -n \
--arg repo "${{ github.repository }}" \
--arg branch "${{ github.ref_name }}" \
--arg url "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
'{
embeds: [{
title: "GitHub Actions failed",
description: ("**" + $repo + "** · branch `" + $branch + "`"),
url: $url,
color: 15158332
}]
}')
curl -sS -X POST \
-H "Content-Type: application/json" \
-d "$payload" \
"$DISCORD_WEBHOOK_URL"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:
on:
workflow_dispatch:
push:
branches: [main]Security
- Keep the secret only in GitHub Secrets (or your org’s secret store).
- This flow does not verify GitHub signatures: Discord trusts whoever knows the URL.
- Do not fire the alert from every noisy step; one message per failed job is usually enough. Discord may return
429if you spam it.
Common mistakes
| What you see | Likely cause | What to do |
|---|---|---|
notify-discord never runs | Previous job did not fail, or it is not in needs | Check the run graph; if: failure() only runs after a failure |
Step fails with a weird URL / non-zero curl | Secret pasted wrong (spaces, quotes, truncated URL) | Re-enter the whole secret; do not “fix” it from the log |
| 404 from Discord | Webhook deleted or stale URL | Create a new webhook and update the secret |
| 401 / odd auth errors | Less common on Incoming Webhooks; truncated URLs misbehave | Regenerate the URL |
| Actions green, Discord silent | Wrong channel, or the watched job did not fail | Confirm the webhook’s channel and the run graph |
| Duplicate message | You re-ran the workflow | Expected here; there is no deduping |
Alternatives
- GitHub email
- It arrives, then gets lost. Weaker for a shared team channel.
- GitHub webhook with Discord’s /github suffix
- Repo events (push, PR, and so on); not the same control as “this Actions job failed”.
- Telegram bot + Actions
- Same CI alert to a chat, with token and chat_id.
- Slack Incoming Webhook
- Same pattern (POST with a secret URL), different destination.