Pipetrics

GitHub Actions Slack Notifications Tutorial

Add Slack failure alerts to GitHub Actions, report job results, and share notifications through composite actions and reusable workflows.

GitHub Actions Slack Notifications

Send a Slack alert when CI fails so your team can open the run logs. This tutorial starts with one job, adds results from multiple jobs, then shares the notification code across repositories.

Prerequisites

You need a GitHub repository with Actions enabled and a Slack workspace where you can install an app. The complete CI examples assume a Node.js 22 project at the repository root, a committed package-lock.json matching package.json, and test, lint, and build scripts. Adjust the Node version and commands for your project. Each job installs its own dependencies because jobs use separate runners.

Creating a Slack App

Step 1: Create the App

Go to api.slack.com/apps, click Create New App, and select From scratch. Name the app "GitHub Notifications" and choose your workspace.

Step 2: Enable Incoming Webhooks

Open Incoming Webhooks and turn on Activate Incoming Webhooks.

Step 3: Add a Webhook URL

Click Add New Webhook to Workspace and select the channel. Copy the generated URL:

https://hooks.slack.com/services/T00000000/B00000000/XXXXXXXXXXXXXXXXXXXXXXXX

Step 4: Store the Webhook in GitHub

In the repository, open Settings → Secrets and variables → Actions. Create a repository secret named SLACK_WEBHOOK_URL with the URL as its value.

Keep it private. Slack revokes exposed webhook URLs. The webhook is tied to the channel selected during installation.

Pull Requests and Secrets

GitHub normally withholds repository secrets from pull_request runs originating in forks. Dependabot pull requests have similar restrictions. Tests can still run, but these examples skip sending when the webhook secret is empty. Do not switch to pull_request_target and execute untrusted pull request code just to gain access to secrets.

Secrets are also available to trusted workflow code, so review changes to workflows and dependencies accordingly. See GitHub's secrets guidance.

Example 1: Simple Failure Alert

This workflow sets up Node, installs dependencies, and runs tests. A failed setup or installation can trigger the alert too.

.github/workflows/ci.yml
name: CI

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

permissions:
  contents: read

jobs:
  test:
    runs-on: ubuntu-latest
    env:
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm
      - run: npm ci
      - run: npm test

      - name: Notify Slack on failure
        if: failure() && env.SLACK_WEBHOOK_URL != ''
        uses: slackapi/[email protected]
        with:
          webhook: ${{ env.SLACK_WEBHOOK_URL }}
          webhook-type: incoming-webhook
          errors: true
          payload: |
            {
              "text": ${{ toJSON(format('CI failed in {0}. {1}/{0}/actions/runs/{2}', github.repository, github.server_url, github.run_id)) }}
            }

failure() overrides the default success condition, allowing the notification step to run after an earlier step fails. GitHub does not allow secrets directly in if conditions, so the step checks a job environment variable instead. Keep the secret scoped to the notification job or step when adapting this to a larger workflow.

The payload is JSON inside a YAML block scalar. toJSON(format(...)) serializes the entire dynamic string, including quotes and newlines. Do not put extra quotes around the expression. This avoids inserting raw values into a quoted YAML payload, where multiline text can be folded or break parsing. No dynamic content is interpolated into shell commands.

errors: true makes delivery errors fail the notification step rather than only log a warning. It does not guarantee delivery. The examples retain slackapi/[email protected]; review action updates separately.

Example 2: Multiple Jobs with One Notification

List every job you want to report in needs. The notification job checks their results, including a build skipped because lint or tests failed.

.github/workflows/ci-multi-job.yml
name: CI Pipeline

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

permissions:
  contents: read

jobs:
  lint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm
      - run: npm ci
      - run: npm run lint

  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm
      - run: npm ci
      - run: npm test

  build:
    runs-on: ubuntu-latest
    needs: [lint, test]
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: "22"
          cache: npm
      - run: npm ci
      - run: npm run build

  notify:
    runs-on: ubuntu-latest
    needs: [lint, test, build]
    if: ${{ !cancelled() && contains(needs.*.result, 'failure') }}
    env:
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
    steps:
      - name: Send Slack notification
        if: env.SLACK_WEBHOOK_URL != ''
        uses: slackapi/[email protected]
        with:
          webhook: ${{ env.SLACK_WEBHOOK_URL }}
          webhook-type: incoming-webhook
          errors: true
          payload: |
            {
              "text": ${{ toJSON(format('CI failed in {0}. Lint: {1}; Test: {2}; Build: {3}. {4}/{0}/actions/runs/{5}', github.repository, needs.lint.result, needs.test.result, needs.build.result, github.server_url, github.run_id)) }}
            }

needs.<job_id>.result provides success, failure, cancelled, or skipped. You do not need custom outputs based on job.status. Only direct dependencies appear in needs, so include them explicitly.

The !cancelled() status check lets the job evaluate after failed or skipped dependencies without running after workflow cancellation. The contains check limits alerts to failures. A run with only cancellations or skipped jobs sends no alert. A job using continue-on-error can have a successful result despite an underlying failure; these examples do not use that setting.

Example 3: Reusable Composite Action

Move the notification format into a repository named slack-notify-action. This action supports both single-job alerts and optional multiline job results, so you do not need a second version of the action for each case.

Create the Action

action.yml
name: Slack Failure Notification
description: Send a Slack failure alert with optional job results

inputs:
  webhook-url:
    description: Slack incoming webhook URL
    required: true
  repository:
    description: Repository name
    required: true
  branch:
    description: Branch or pull request ref name
    required: true
  run-url:
    description: URL to the workflow run
    required: true
  custom-message:
    description: Optional plain text message
    required: false
    default: ""
  job-statuses:
    description: Optional plain text job results, with one job per line
    required: false
    default: ""

runs:
  using: composite
  steps:
    - name: Send Slack notification
      uses: slackapi/[email protected]
      with:
        webhook: ${{ inputs.webhook-url }}
        webhook-type: incoming-webhook
        errors: true
        payload: |
          {
            "text": ${{ toJSON(format('CI failed in {0}. {1}', inputs.repository, inputs.run-url)) }},
            "blocks": [
              {
                "type": "section",
                "text": {
                  "type": "plain_text",
                  "text": ${{ toJSON(inputs.custom-message || format('CI failed in {0}', inputs.repository)) }}
                }
              },
              {
                "type": "section",
                "text": {
                  "type": "plain_text",
                  "text": ${{ toJSON(format('Repository: {0} | Ref: {1}', inputs.repository, inputs.branch)) }}
                }
              },
              {
                "type": "section",
                "text": {
                  "type": "plain_text",
                  "text": ${{ toJSON(inputs.job-statuses || 'Job results were not supplied. Open the run for details.') }}
                }
              },
              {
                "type": "actions",
                "elements": [
                  {
                    "type": "button",
                    "text": { "type": "plain_text", "text": "View Run" },
                    "url": ${{ toJSON(inputs.run-url) }}
                  }
                ]
              }
            ]
          }

job-statuses is a formatted text string, not a JSON object. toJSON preserves its line breaks when it becomes a JSON string in the payload. Both optional inputs have nonempty fallbacks, so omitting them cannot produce an empty Slack section. Supply meaningful text rather than whitespace-only values.

Dynamic sections use plain_text so custom messages and ref names are not interpreted as Slack markup or mentions. JSON serialization protects payload syntax, not Slack's size limits: keep each section text under 3,000 characters. See the section block reference.

Use the Action

Publish a v1 tag containing action.yml. Replace the notify job in Example 2 with this job, leaving the CI jobs, triggers, and permissions unchanged:

.github/workflows/ci-multi-job.yml (jobs fragment)
jobs:
  notify:
    runs-on: ubuntu-latest
    needs: [lint, test, build]
    if: ${{ !cancelled() && contains(needs.*.result, 'failure') }}
    env:
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
    steps:
      - name: Notify on failure
        if: env.SLACK_WEBHOOK_URL != ''
        uses: your-org/slack-notify-action@v1
        with:
          webhook-url: ${{ env.SLACK_WEBHOOK_URL }}
          repository: ${{ github.repository }}
          branch: ${{ github.ref_name }}
          run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
          custom-message: "CI failed. Check the logs for details."
          job-statuses: |
            Lint: ${{ needs.lint.result }}
            Test: ${{ needs.test.result }}
            Build: ${{ needs.build.result }}

Replace your-org with your organization. For a single job, replace the notification step in Example 1 with this action call, retain its failure() and secret checks, and omit job-statuses. The composite action does not decide when a workflow has failed; the caller controls that condition.

A remote composite action does not require checkout in the notification job. Its repository must be accessible to the caller, and the caller's Actions policy must allow it. Tags make releases convenient; a full commit SHA provides an immutable pin.

Example 4: Cross-Repository Setup

Use an organization secret and a reusable workflow when several repositories should share the same notification job.

Organization-Level Secrets and Repository Access

In organization settings, open Secrets and variables → Actions and create SLACK_WEBHOOK_URL. Grant access to the caller repositories that need it. Organization secrets are subject to your GitHub plan and repository visibility; for example, GitHub Free does not make organization secrets available to private repositories.

Create the reusable workflow below in a central repository, such as your-org/.github. This repository name does not automatically grant access. For a private shared repository, configure Settings → Actions → General → Access to permit eligible callers. Repository visibility and organization or enterprise Actions policies must also allow reuse. Apply the same checks to the separate composite action repository. Public callers cannot use a workflow stored in a private repository. See GitHub's reusable workflow access rules.

Centralized Reusable Workflow

The workflow calls the composite action from Example 3 rather than duplicating its payload. Publish that action before using this workflow. It skips delivery if the caller cannot supply the webhook.

.github/workflows/notify-slack.yml (central repository)
name: Slack Notification

on:
  workflow_call:
    inputs:
      job-statuses:
        description: Plain text job results, with one job per line
        required: false
        type: string
        default: ""
      custom-message:
        description: Optional plain text message
        required: false
        type: string
        default: ""
    secrets:
      SLACK_WEBHOOK_URL:
        required: false

permissions:
  contents: read

jobs:
  notify:
    runs-on: ubuntu-latest
    env:
      SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
    steps:
      - name: Send Slack notification
        if: env.SLACK_WEBHOOK_URL != ''
        uses: your-org/slack-notify-action@v1
        with:
          webhook-url: ${{ env.SLACK_WEBHOOK_URL }}
          repository: ${{ github.repository }}
          branch: ${{ github.ref_name }}
          run-url: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
          custom-message: ${{ inputs.custom-message }}
          job-statuses: ${{ inputs.job-statuses }}

The secret is optional in the interface so fork runs with no secret can skip delivery. For trusted runs, an empty or misconfigured secret also means no notification; verify secret access during setup. The github context in a reusable workflow describes the caller, so the message links to the originating run.

Calling the Reusable Workflow

Publish a v1 tag in the central repository too. In Example 2, keep the CI jobs and replace only notify with the following job. A reusable workflow is called at job level, without runs-on or steps in the calling job.

.github/workflows/ci-multi-job.yml (jobs fragment)
jobs:
  notify:
    needs: [lint, test, build]
    if: ${{ !cancelled() && contains(needs.*.result, 'failure') }}
    uses: your-org/.github/.github/workflows/notify-slack.yml@v1
    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 }}

The repeated .github is intentional: the first is the repository name, the second is its workflow directory. Replace the owner and repository as needed. Secrets are not automatically inherited from the central repository; this explicit mapping passes the caller's repository or organization secret.

Rotating a shared organization secret updates access for its permitted callers. Format changes still depend on version references: callers pinned to a commit or an older release will not automatically receive a new release. Update those references through your normal review process.

Troubleshooting

Notifications Not Sending

Check the notification job's condition and the results of its direct dependencies. A successful or cancelled workflow will not send an alert with these conditions. If the sending step was skipped, check whether SLACK_WEBHOOK_URL is available to that caller and event. Fork and Dependabot pull requests normally cannot use the repository secret.

If a reusable workflow or composite action cannot be loaded, check its path, version reference, repository visibility, and Actions access settings before investigating Slack.

Webhook or Payload Errors

Review the Slack step logs without printing the webhook. Slack may reject a revoked URL, an archived channel, or an invalid payload. Keep errors: true if you want delivery failures to fail the sending step. Fix JSON structure and Slack field limits before retrying.

Use Block Kit Builder to inspect a sample payload with fake values, never your webhook or sensitive build data. Preserve toJSON around dynamic strings. Pasting multiline input directly into a quoted YAML payload can fold line breaks or produce invalid syntax.

Summary

Use a failure step for one job or a notification job with needs for a pipeline. A composite action shares the message format, while a reusable workflow shares the whole notification job. Test the chosen setup in a controlled repository before relying on it for alerts.

Managed GitHub Actions Slack notifications with Pipetrics

Workflow YAML is a good fit when each repository owns its notification behavior. At organization scale, duplicated steps, reusable workflows, secrets, channel rules, and message changes still require maintenance.

Pipetrics Slack integration manages repository, workflow, channel, and outcome rules outside workflow YAML. Use it when teams want centralized notification configuration after connecting the read-only Pipetrics GitHub App.

To investigate why a notified workflow is slow or expensive, continue with GitHub Actions performance monitoring or GitHub Actions cost monitoring.

On this page