GitHub Actions Slack Notifications - Complete Tutorial
Learn how to add Slack notifications to GitHub Actions workflows. From simple alerts to cross-repo setups with reusable actions.
GitHub Actions Slack Notifications
Failed builds happen. How fast does your team find out? This tutorial covers Slack notifications for GitHub Actions workflows. You'll progress from basic alerts to production-ready cross-repo setups.
Prerequisites
Before starting, you need:
- A GitHub repository with Actions enabled
- A Slack workspace where you can install apps
- Basic familiarity with GitHub Actions YAML syntax
Creating a Slack App
First, create a Slack app to send notifications.
Step 1: Create the App
Go to api.slack.com/apps and click Create New App. Select From scratch. Name it "GitHub Notifications" and pick your workspace.
Step 2: Enable Incoming Webhooks
Navigate to Incoming Webhooks in the sidebar. Toggle Activate Incoming Webhooks to on.
Step 3: Add a Webhook URL
Click Add New Webhook to Workspace. Select the channel where you want notifications. Slack generates a URL like this:
https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXXCopy this URL. You'll add it to GitHub secrets next.
Step 4: Store the Webhook in GitHub
Open your repository on GitHub. Go to Settings → Secrets and variables → Actions. Click New repository secret.
Set the name to SLACK_WEBHOOK_URL and paste your webhook URL as the value.
Keep this URL private. Slack actively revokes leaked webhook URLs.
Example 1: Simple Failure Alert
Start with a single job that sends a Slack message when the workflow fails.
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test
- name: Notify Slack on failure
if: failure()
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "Build failed in ${{ github.repository }}"The if: failure() condition runs this step only when previous steps fail. The message includes the repository name.
Adding More Details
Plain text works, but Block Kit makes messages scannable. Here's an enhanced version:
- name: Notify Slack on failure
if: failure()
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "Build failed in ${{ github.repository }}"
blocks:
- type: "header"
text:
type: "plain_text"
text: "Build Failed"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Repository:*\n${{ github.repository }}"
- type: "mrkdwn"
text: "*Branch:*\n${{ github.ref_name }}"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Commit:*\n${{ github.sha }}"
- type: "mrkdwn"
text: "*Author:*\n${{ github.actor }}"
- type: "actions"
elements:
- type: "button"
text:
type: "plain_text"
text: "View Run"
url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"This message displays repository, branch, commit, and author. The button links directly to the failed run.
Example 2: Multiple Jobs with Catch-All Handler
Real workflows have multiple jobs. You want one notification regardless of which job fails. Add a dedicated notification job that runs after all others.
name: CI Pipeline
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run linter
run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- name: Build application
run: npm run build
notify:
runs-on: ubuntu-latest
needs: [lint, test, build]
if: failure()
steps:
- name: Send Slack notification
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "Pipeline failed in ${{ github.repository }}"
blocks:
- type: "header"
text:
type: "plain_text"
text: "Pipeline Failed"
- type: "section"
text:
type: "mrkdwn"
text: "One or more jobs failed in *${{ github.repository }}*"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Branch:*\n${{ github.ref_name }}"
- type: "mrkdwn"
text: "*Triggered by:*\n${{ github.actor }}"
- type: "actions"
elements:
- type: "button"
text:
type: "plain_text"
text: "View Pipeline"
url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"The notify job depends on all other jobs via the needs array. It runs only when any dependency fails.
Reporting Which Job Failed
The previous example tells you something failed but not what. Use job outputs to track individual statuses:
name: CI Pipeline
on:
push:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
outputs:
status: ${{ job.status }}
steps:
- uses: actions/checkout@v4
- name: Run linter
run: npm run lint
test:
runs-on: ubuntu-latest
outputs:
status: ${{ job.status }}
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test
build:
runs-on: ubuntu-latest
needs: [lint, test]
outputs:
status: ${{ job.status }}
steps:
- uses: actions/checkout@v4
- name: Build application
run: npm run build
notify:
runs-on: ubuntu-latest
needs: [lint, test, build]
if: always()
steps:
- name: Send Slack notification
if: contains(needs.*.result, 'failure')
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "Pipeline failed in ${{ github.repository }}"
blocks:
- type: "header"
text:
type: "plain_text"
text: "Pipeline Failed"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Lint:*\n${{ needs.lint.result }}"
- type: "mrkdwn"
text: "*Test:*\n${{ needs.test.result }}"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Build:*\n${{ needs.build.result }}"
- type: "mrkdwn"
text: "*Branch:*\n${{ github.ref_name }}"
- type: "actions"
elements:
- type: "button"
text:
type: "plain_text"
text: "View Pipeline"
url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"The if: always() on the job ensures it runs regardless of previous outcomes. The step condition contains(needs.*.result, 'failure') sends notifications only when a job failed. Each result appears in the message so you know where to look.
Example 3: Reusable Composite Action
Copying notification steps across repositories creates maintenance headaches. Extract them into a reusable composite action.
Create the Action
Create a repository called slack-notify-action. Add this file:
name: Slack Failure Notification
description: Send a formatted Slack notification when a workflow fails
inputs:
webhook-url:
description: Slack incoming webhook URL
required: true
repository:
description: Repository name
required: true
branch:
description: Branch name
required: true
actor:
description: User who triggered the workflow
required: true
run-url:
description: URL to the workflow run
required: true
custom-message:
description: Optional custom message
required: false
default: ""
runs:
using: composite
steps:
- name: Send Slack notification
uses: slackapi/[email protected]
with:
webhook: ${{ inputs.webhook-url }}
webhook-type: incoming-webhook
payload: |
text: "Build failed in ${{ inputs.repository }}"
blocks:
- type: "header"
text:
type: "plain_text"
text: "Build Failed"
- type: "section"
text:
type: "mrkdwn"
text: "${{ inputs.custom-message != '' && inputs.custom-message || format('A workflow failed in *{0}*', inputs.repository) }}"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Repository:*\n${{ inputs.repository }}"
- type: "mrkdwn"
text: "*Branch:*\n${{ inputs.branch }}"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Triggered by:*\n${{ inputs.actor }}"
- type: "actions"
elements:
- type: "button"
text:
type: "plain_text"
text: "View Run"
url: "${{ inputs.run-url }}"Use the Action
Reference your custom action in any workflow:
name: CI
on:
push:
branches: [main]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Run tests
run: npm test
- name: Notify on failure
if: failure()
uses: your-org/slack-notify-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
repository: ${{ github.repository }}
branch: ${{ github.ref_name }}
actor: ${{ github.actor }}
run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
custom-message: "Tests failed. Check the logs for details."Replace your-org/slack-notify-action@v1 with your repository path. Tag releases to pin versions.
Adding Job Status Support
Extend the action to display multiple job statuses:
name: Slack Pipeline Notification
description: Send a formatted Slack notification with job statuses
inputs:
webhook-url:
description: Slack incoming webhook URL
required: true
repository:
description: Repository name
required: true
branch:
description: Branch name
required: true
actor:
description: User who triggered the workflow
required: true
run-url:
description: URL to the workflow run
required: true
job-statuses:
description: JSON object with job names and statuses
required: true
runs:
using: composite
steps:
- name: Send Slack notification
uses: slackapi/[email protected]
env:
JOB_STATUSES: ${{ inputs.job-statuses }}
with:
webhook: ${{ inputs.webhook-url }}
webhook-type: incoming-webhook
payload: |
text: "Pipeline failed in ${{ inputs.repository }}"
blocks:
- type: "header"
text:
type: "plain_text"
text: "Pipeline Status"
- type: "section"
text:
type: "mrkdwn"
text: "*Repository:* ${{ inputs.repository }}\n*Branch:* ${{ inputs.branch }}\n*Triggered by:* ${{ inputs.actor }}"
- type: "section"
text:
type: "mrkdwn"
text: "*Job Results:*\n${{ inputs.job-statuses }}"
- type: "actions"
elements:
- type: "button"
text:
type: "plain_text"
text: "View Pipeline"
url: "${{ inputs.run-url }}"Pass job statuses as a formatted string:
notify:
runs-on: ubuntu-latest
needs: [lint, test, build]
if: always()
steps:
- name: Notify on failure
if: contains(needs.*.result, 'failure')
uses: your-org/slack-notify-action@v1
with:
webhook-url: ${{ secrets.SLACK_WEBHOOK_URL }}
repository: ${{ github.repository }}
branch: ${{ github.ref_name }}
actor: ${{ github.actor }}
run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
job-statuses: |
Lint: ${{ needs.lint.result }}
Test: ${{ needs.test.result }}
Build: ${{ needs.build.result }}Example 4: Cross-Repository Setup
Large organizations have many repositories. Managing secrets and actions across all of them requires centralization.
Organization-Level Secrets
Store the Slack webhook at the organization level instead of per-repository.
Go to your GitHub organization settings. Navigate to Secrets and variables → Actions. Click New organization secret. Name it SLACK_WEBHOOK_URL and set repository access.
Centralized Reusable Workflow
Create a reusable workflow that any repository can call. Store it in a central .github repository.
name: Slack Notification
on:
workflow_call:
inputs:
job-statuses:
description: Formatted string of job statuses
required: false
type: string
default: ""
custom-message:
description: Custom message to include
required: false
type: string
default: ""
secrets:
SLACK_WEBHOOK_URL:
required: true
jobs:
notify:
runs-on: ubuntu-latest
steps:
- name: Send Slack notification
uses: slackapi/[email protected]
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "Workflow notification from ${{ github.repository }}"
blocks:
- type: "header"
text:
type: "plain_text"
text: "Workflow Alert"
- type: "section"
text:
type: "mrkdwn"
text: "${{ inputs.custom-message != '' && inputs.custom-message || format('Notification from *{0}*', github.repository) }}"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Repository:*\n${{ github.repository }}"
- type: "mrkdwn"
text: "*Branch:*\n${{ github.ref_name }}"
- type: "section"
fields:
- type: "mrkdwn"
text: "*Triggered by:*\n${{ github.actor }}"
- type: "mrkdwn"
text: "*Event:*\n${{ github.event_name }}"
- type: "section"
text:
type: "mrkdwn"
text: "${{ inputs.job-statuses != '' && format('*Job Results:*\n{0}', inputs.job-statuses) || '' }}"
- type: "actions"
elements:
- type: "button"
text:
type: "plain_text"
text: "View Run"
url: "${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"Calling the Reusable Workflow
Any repository in the organization can now call this workflow:
name: CI
on:
push:
branches: [main]
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm run lint
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npm test
build:
runs-on: ubuntu-latest
needs: [lint, test]
steps:
- uses: actions/checkout@v4
- run: npm run build
notify-on-failure:
needs: [lint, test, build]
if: failure()
uses: your-org/.github/.github/workflows/notify-slack.yml@main
with:
job-statuses: |
Lint: ${{ needs.lint.result }}
Test: ${{ needs.test.result }}
Build: ${{ needs.build.result }}
custom-message: "CI pipeline failed. Please investigate."
secrets:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}Replace your-org/.github with your organization's central repository name.
Benefits of This Approach
Centralizing notifications provides several advantages:
- Single source of truth: Update the notification format once, all repositories benefit
- Consistent messaging: Every team gets the same notification style
- Easier secret management: Rotate the webhook URL in one place
- Reduced duplication: No copy-pasting YAML across repositories
Troubleshooting
Common issues and solutions when setting up Slack notifications.
Notifications Not Sending
Check these items:
- Verify the
SLACK_WEBHOOK_URLsecret exists and has the correct value - Confirm the
if: failure()condition matches your scenario - Check that the Slack app is still installed in your workspace
- Review the Actions logs for error messages from the Slack step
Invalid Webhook Errors
Slack may return invalid_payload or no_service errors. Causes include:
- Malformed YAML in the payload section
- Webhook URL was revoked due to being exposed publicly
- The channel was archived or the app was removed from it
Message Formatting Issues
Block Kit has strict requirements. Validate your payload with Block Kit Builder first.
Summary
You now have four approaches for GitHub Actions Slack notifications:
- Simple alert: Single step with
if: failure()for basic needs - Multi-job handler: Dedicated notification job that catches failures across the pipeline
- Reusable action: Composite action for consistent notifications across repositories
- Cross-repo setup: Organization-level secrets and reusable workflows for enterprise scale
Start with the simple approach. Upgrade to more sophisticated setups as your needs grow.