Pipetrics

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/XXXXXXXXXXXXXXXXXXXXXXXX

Copy this URL. You'll add it to GitHub secrets next.

Step 4: Store the Webhook in GitHub

Open your repository on GitHub. Go to SettingsSecrets and variablesActions. 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.

.github/workflows/ci.yml
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:

.github/workflows/ci.yml
- 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.

.github/workflows/ci-multi-job.yml
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:

.github/workflows/ci-detailed.yml
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:

action.yml
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:

.github/workflows/ci.yml
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:

action.yml
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:

.github/workflows/ci.yml
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 variablesActions. 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.

.github/workflows/notify-slack.yml
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:

.github/workflows/ci.yml
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:

  1. Verify the SLACK_WEBHOOK_URL secret exists and has the correct value
  2. Confirm the if: failure() condition matches your scenario
  3. Check that the Slack app is still installed in your workspace
  4. 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:

  1. Simple alert: Single step with if: failure() for basic needs
  2. Multi-job handler: Dedicated notification job that catches failures across the pipeline
  3. Reusable action: Composite action for consistent notifications across repositories
  4. 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.

On this page