Skip to main content
All articles
· Updated February 15, 2026

Send Discord Notifications from GitHub Actions CI/CD

Learn how to send Discord webhook notifications from GitHub Actions workflows. Includes examples for build status, deployment alerts, and custom embeds.

github actionsci/cdwebhookautomationgithub actions discord 2026ci cd notificationsdevops discord
Send Discord Notifications from GitHub Actions CI/CD

Why Send Discord Notifications from GitHub Actions?

GitHub Actions is a powerful CI/CD platform, but its native notifications are limited to email and GitHub’s UI. Discord webhooks let you send real-time build status, deployment alerts, and test results directly to your team’s Discord server.

This guide shows you how to integrate Discord webhooks into your GitHub Actions workflows with practical examples you can copy and adapt.

Prerequisites

You need:

  • A GitHub repository with Actions enabled
  • A Discord webhook URL (Server Settings → Integrations → Webhooks → New Webhook)
  • Basic familiarity with YAML workflow syntax

Method 1: Using curl (No Dependencies)

The simplest approach uses curl to send a POST request directly from your workflow:

name: Build and Notify

on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Build project
        run: npm run build
      
      - name: Notify Discord
        if: always()
        run: |
          curl -H "Content-Type: application/json" \
               -d '{"content": "Build completed for `${{ github.repository }}`"}' \
               ${{ secrets.DISCORD_WEBHOOK_URL }}

Important: Store your webhook URL as a GitHub secret (Settings → Secrets and variables → Actions → New repository secret). Never hardcode webhook URLs in your workflow files.

Method 2: Send Build Status with Embeds

Plain text notifications work, but embeds provide richer formatting with colors, fields, and status indicators:

name: CI Pipeline

on:
  push:
    branches: [main, develop]
  pull_request:

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Run tests
        id: tests
        run: npm test
      
      - name: Discord notification
        if: always()
        env:
          DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK_URL }}
        run: |
          if [ "${{ steps.tests.outcome }}" == "success" ]; then
            COLOR=5763719
            STATUS="Success"
            EMOJI=":white_check_mark:"
          else
            COLOR=15548997
            STATUS="Failed"
            EMOJI=":x:"
          fi
          
          curl -H "Content-Type: application/json" \
               -d "{
                 \"embeds\": [{
                   \"title\": \"$EMOJI CI Pipeline - $STATUS\",
                   \"description\": \"Tests completed for \`${{ github.repository }}\`\",
                   \"color\": $COLOR,
                   \"fields\": [
                     {\"name\": \"Branch\", \"value\": \"\`${{ github.ref_name }}\`\", \"inline\": true},
                     {\"name\": \"Commit\", \"value\": \"\`${{ github.sha }}\`\", \"inline\": true},
                     {\"name\": \"Author\", \"value\": \"${{ github.actor }}\", \"inline\": true}
                   ],
                   \"url\": \"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"
                 }]
               }" \
               $DISCORD_WEBHOOK

This workflow:

  • Runs tests on every push and pull request
  • Sends a green embed on success, red on failure
  • Includes branch, commit SHA, and author information
  • Links directly to the workflow run

Method 3: Using a Dedicated Action

For cleaner workflows, use a pre-built action like sarisia/actions-status-discord:

name: Deploy Production

on:
  push:
    tags:
      - 'v*'

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Deploy to production
        run: |
          echo "Deploying version ${{ github.ref_name }}..."
          # Your deployment commands here
      
      - name: Discord notification
        if: always()
        uses: sarisia/actions-status-discord@v1
        with:
          webhook: ${{ secrets.DISCORD_WEBHOOK_URL }}
          status: ${{ job.status }}
          title: "Production Deployment"
          description: "Version `${{ github.ref_name }}` deployed"
          color: 0x5865F2
          username: "Deploy Bot"

This action automatically handles:

  • Status detection (success/failure/cancelled)
  • Color coding based on status
  • Timestamp formatting
  • Error handling

Method 4: Conditional Notifications (Failures Only)

To reduce noise, send notifications only when builds fail:

name: Continuous Integration

on: [push, pull_request]

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      
      - name: Install dependencies
        run: npm ci
      
      - name: Build
        run: npm run build
      
      - name: Test
        run: npm test
      
      - name: Notify on failure
        if: failure()
        run: |
          curl -H "Content-Type: application/json" \
               -d "{
                 \"content\": \"@here Build failed!\",
                 \"embeds\": [{
                   \"title\": \":x: Build Failed\",
                   \"description\": \"The build for \`${{ github.repository }}\` has failed.\",
                   \"color\": 15548997,
                   \"fields\": [
                     {\"name\": \"Branch\", \"value\": \"\`${{ github.ref_name }}\`\", \"inline\": true},
                     {\"name\": \"Triggered by\", \"value\": \"${{ github.actor }}\", \"inline\": true}
                   ],
                   \"url\": \"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"
                 }]
               }" \
               ${{ secrets.DISCORD_WEBHOOK_URL }}

The if: failure() condition ensures the notification step only runs when a previous step fails.

Method 5: Multi-Job Workflow with Summary

For complex workflows with multiple jobs, send a summary notification at the end:

name: Full Pipeline

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm test
  
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: npm run lint
  
  build:
    runs-on: ubuntu-latest
    needs: [test, lint]
    steps:
      - uses: actions/checkout@v4
      - run: npm run build
  
  notify:
    runs-on: ubuntu-latest
    needs: [test, lint, build]
    if: always()
    steps:
      - name: Send summary
        env:
          TEST_STATUS: ${{ needs.test.result }}
          LINT_STATUS: ${{ needs.lint.result }}
          BUILD_STATUS: ${{ needs.build.result }}
        run: |
          curl -H "Content-Type: application/json" \
               -d "{
                 \"embeds\": [{
                   \"title\": \"Pipeline Summary\",
                   \"description\": \"Full pipeline completed for \`${{ github.repository }}\`\",
                   \"color\": 5793266,
                   \"fields\": [
                     {\"name\": \"Tests\", \"value\": \"$TEST_STATUS\", \"inline\": true},
                     {\"name\": \"Linting\", \"value\": \"$LINT_STATUS\", \"inline\": true},
                     {\"name\": \"Build\", \"value\": \"$BUILD_STATUS\", \"inline\": true}
                   ]
                 }]
               }" \
               ${{ secrets.DISCORD_WEBHOOK_URL }}

This pattern:

  • Runs test, lint, and build jobs in parallel (test and lint) or sequentially (build waits for both)
  • Collects results from all jobs
  • Sends a single summary notification with all job statuses

Best Practices

Use GitHub Secrets: Always store webhook URLs in repository secrets, never in workflow files.

Add if: always(): Without this condition, notification steps won’t run if previous steps fail.

Include Links: Add the workflow run URL so team members can quickly jump to logs.

Avoid Spam: Use if: failure() or if: github.ref == 'refs/heads/main' to limit notifications to important events.

Test Locally: Use the Discord Webhook Builder to design and test your embed layouts before adding them to workflows.

Troubleshooting

Webhook returns 400 Bad Request: Check your JSON syntax. Use a JSON validator or test the payload with curl locally first.

Notifications not appearing: Verify the webhook URL is correct and the webhook hasn’t been deleted in Discord.

Rate limits: Discord allows 30 requests per minute per webhook. If you hit this limit, add delays between notifications or batch multiple updates into a single message.

Next Steps

You now have multiple methods for sending Discord notifications from GitHub Actions. For more complex embed designs, try our free Discord Webhook Builder to visually create messages, then copy the JSON into your workflows.

discord-webhook.com also offers scheduled messages, thread and forum support, polls, and interactive buttons with actions — all configurable through the visual builder without writing code.

Pull Request Notifications

Beyond push-based builds, you can trigger Discord notifications on pull request events such as opened, synchronized, or merged. This is useful for keeping your team informed about code review activity without requiring everyone to watch GitHub constantly. The workflow below fires on PR events and sends an embed containing the PR title, author, and diff statistics, linking directly to the pull request for quick access.

name: PR Notification

on:
  pull_request:
    types: [opened, synchronize, closed]

jobs:
  notify-pr:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Get diff stats
        id: diff
        run: |
          STATS=$(git diff --shortstat origin/${{ github.base_ref }}...HEAD)
          echo "stats=$STATS" >> $GITHUB_OUTPUT

      - name: Notify Discord
        run: |
          if [ "${{ github.event.action }}" == "closed" ] && [ "${{ github.event.pull_request.merged }}" == "true" ]; then
            COLOR=5763719
            ACTION="Merged"
          elif [ "${{ github.event.action }}" == "opened" ]; then
            COLOR=5793266
            ACTION="Opened"
          else
            COLOR=16776960
            ACTION="Updated"
          fi

          curl -H "Content-Type: application/json" \
               -d "{
                 \"embeds\": [{
                   \"title\": \"PR $ACTION: ${{ github.event.pull_request.title }}\",
                   \"description\": \"${{ steps.diff.outputs.stats }}\",
                   \"color\": $COLOR,
                   \"fields\": [
                     {\"name\": \"Author\", \"value\": \"${{ github.event.pull_request.user.login }}\", \"inline\": true},
                     {\"name\": \"Base\", \"value\": \"\`${{ github.base_ref }}\`\", \"inline\": true},
                     {\"name\": \"Head\", \"value\": \"\`${{ github.head_ref }}\`\", \"inline\": true}
                   ],
                   \"url\": \"${{ github.event.pull_request.html_url }}\"
                 }]
               }" \
               ${{ secrets.DISCORD_WEBHOOK_URL }}

The color-coded status changes based on the PR action — green for merged, yellow for updates, and blue for newly opened pull requests. This gives reviewers immediate visual feedback in the Discord channel.

Scheduled Reports

Cron-triggered workflows let you send periodic summaries to Discord without any manual intervention. This is ideal for daily standups, weekly activity digests, or repository health dashboards. Using GitHub’s schedule event, you can collect repository statistics via the GitHub API and format them into a structured embed that arrives at a consistent time each day or week.

name: Weekly Repo Summary

on:
  schedule:
    - cron: '0 9 * * 1'  # Every Monday at 9:00 UTC

jobs:
  weekly-report:
    runs-on: ubuntu-latest
    steps:
      - name: Gather stats
        id: stats
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          COMMITS=$(gh api repos/${{ github.repository }}/commits --jq 'length' -q 'since=7 days ago' 2>/dev/null || echo "0")
          OPEN_ISSUES=$(gh api repos/${{ github.repository }}/issues --jq '[.[] | select(.pull_request == null)] | length')
          OPEN_PRS=$(gh api repos/${{ github.repository }}/pulls --jq 'length')
          echo "commits=$COMMITS" >> $GITHUB_OUTPUT
          echo "issues=$OPEN_ISSUES" >> $GITHUB_OUTPUT
          echo "prs=$OPEN_PRS" >> $GITHUB_OUTPUT

      - name: Send report
        run: |
          curl -H "Content-Type: application/json" \
               -d "{
                 \"embeds\": [{
                   \"title\": \"Weekly Repository Summary\",
                   \"description\": \"Activity report for \`${{ github.repository }}\`\",
                   \"color\": 5793266,
                   \"fields\": [
                     {\"name\": \"Recent Commits\", \"value\": \"${{ steps.stats.outputs.commits }}\", \"inline\": true},
                     {\"name\": \"Open Issues\", \"value\": \"${{ steps.stats.outputs.issues }}\", \"inline\": true},
                     {\"name\": \"Open PRs\", \"value\": \"${{ steps.stats.outputs.prs }}\", \"inline\": true}
                   ],
                   \"footer\": {\"text\": \"Generated on $(date -u +%Y-%m-%d)\"}
                 }]
               }" \
               ${{ secrets.DISCORD_WEBHOOK_URL }}

Use embed formatting to customize the report layout with additional fields like top contributors, merged PRs, or release notes. Scheduled reports reduce the need for manual check-ins and ensure your team stays aligned on project progress.

Matrix Builds with Notifications

When your CI pipeline tests across multiple operating systems, language versions, or configurations using a matrix strategy, you often want a single aggregated notification rather than one per combination. The pattern below uses a separate notify job that depends on the matrix job, collects results, and posts a consolidated summary to Discord.

name: Matrix CI

on:
  push:
    branches: [main]

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      fail-fast: false
      matrix:
        os: [ubuntu-latest, windows-latest, macos-latest]
        node: [18, 20, 22]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: ${{ matrix.node }}
      - run: npm ci && npm test

  notify:
    runs-on: ubuntu-latest
    needs: [test]
    if: always()
    steps:
      - name: Aggregate and notify
        run: |
          RESULT="${{ needs.test.result }}"
          if [ "$RESULT" == "success" ]; then
            COLOR=5763719
            MSG="All matrix combinations passed."
          else
            COLOR=15548997
            MSG="One or more matrix combinations failed."
          fi

          curl -H "Content-Type: application/json" \
               -d "{
                 \"embeds\": [{
                   \"title\": \"Matrix Build: $RESULT\",
                   \"description\": \"$MSG\",
                   \"color\": $COLOR,
                   \"fields\": [
                     {\"name\": \"OS Targets\", \"value\": \"ubuntu, windows, macos\", \"inline\": true},
                     {\"name\": \"Node Versions\", \"value\": \"18, 20, 22\", \"inline\": true},
                     {\"name\": \"Combinations\", \"value\": \"9 total\", \"inline\": true}
                   ],
                   \"url\": \"${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}\"
                 }]
               }" \
               ${{ secrets.DISCORD_WEBHOOK_URL }}

Using fail-fast: false ensures all combinations run even if one fails, giving you a complete picture. The notify job then reports the overall matrix result. Combine this with automation patterns to route failure notifications to specific channels based on which platform or version failed.

Security Considerations

Webhook URLs are effectively bearer tokens — anyone with the URL can post messages to your channel. Follow these practices to keep your pipeline secure:

Protect webhook secrets: Always store webhook URLs in GitHub repository or organization secrets. Never print them in logs. Use add-mask in workflow steps if you dynamically construct URLs to prevent accidental exposure in action output.

Minimize GITHUB_TOKEN permissions: When your notification step accesses the GitHub API (for example, to fetch PR details or repository stats), use the permissions key at the job level to restrict the token to read-only access on only the resources it needs.

Audit and rotate: Periodically review which workflows use your webhook secrets and rotate webhook URLs if team members leave or if you suspect a URL has been exposed. Discord lets you regenerate a webhook URL without deleting the webhook itself, preserving channel configuration.

Restrict workflow triggers: Use branch protection rules alongside on.push.branches filters to ensure only trusted code paths can trigger notification workflows, preventing forks or unauthorized branches from posting to your channels.