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/XXXXXXXXXXXXXXXXXXXXXXXXStep 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.
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.
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
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:
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.
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.
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.
Pipetrics Slack Integration for GitHub Actions Notifications
Configure the Pipetrics Slack integration for GitHub Actions workflow notifications, channel rules, outcome filters, and multiple Slack workspaces.
GitHub Actions Grafana Dashboard
Import the Pipetrics GitHub Actions Grafana dashboard. Configure PostgreSQL access and investigate runtime, failures, queue delays, and rounded minutes.