slackapi/slack-github-action v4: Send a Slack Message From GitHub Actions
slackapi/slack-github-action v4.0.0 posts to Slack from a workflow with a bot token or an incoming webhook. YAML we ran against a real workspace, thread replies, chat.update, and the errors: true setting that stops a failed post from passing silently.
On this page
slackapi/slack-github-action is Slack's official GitHub Action for posting to Slack from a workflow. The latest release is v4.0.0 (15 July 2026, commit dcb1066). Add it as a step, give it either a bot token with method: chat.postMessage or an incoming webhook URL, both stored as repository secrets, and write the message in the payload input as YAML. Add if: failure() to post only when a build breaks, and errors: true so a Slack error fails the step: on 6 October we ran v4.0.0 against a wrong channel ID and, with the default errors: false, the step still exited 0.
How bot tokens differ from user tokens is in Slack bot token.
Four ways to post to Slack from GitHub Actions
| Way | Secret you store | Best for |
|---|---|---|
| slack-github-action with an API method | SLACK_BOT_TOKEN and a channel ID | Threads, message updates, files, any channel the bot is in |
| slack-github-action with an incoming webhook | SLACK_WEBHOOK_URL | One fixed channel, quickest setup |
curl and jq in a run step | SLACK_WEBHOOK_URL | Repos that allow no third-party actions |
| The GitHub app for Slack | None in the repo | Workflow run alerts with no YAML changes |
The action is maintained by Slack. To pin it to an exact build, as many security policies require, use the commit instead of the tag: uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0. Version 4.0.0 came out on 15 July 2026; its one breaking change is stricter YAML in payload, where a value that spans several lines must be indented under its key. Version 3 moved the action to the Node.js 24 runtime, so old self-hosted runners may need an update.
Option 1: bot token and chat.postMessage
This way can post to any channel the bot is in, reply in threads and edit its own messages. To send the same update to several channels, call it once per channel, as in posting one message to multiple Slack channels.
- Create a Slack app at api.slack.com/apps, or use an existing one.
- Under OAuth & Permissions, add the bot scope
chat:write. - Click Install to Workspace and copy the Bot User OAuth Token (it starts with
xoxb-). - In GitHub, open Settings > Secrets and variables > Actions and add
SLACK_BOT_TOKEN. Add the channel ID asSLACK_CHANNEL_ID; you find it at the bottom of the channel's details panel. - In Slack, run
/invite @your-appin that channel. Without it, the call fails withnot_in_channel.
Then add the step. This workflow runs tests on every push to main and posts only if they fail:
name: CI
on:
push:
branches: [main]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v5
- run: npm ci && npm test
- name: Tell Slack the build failed
if: failure()
uses: slackapi/slack-github-action@v4.0.0
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
errors: true
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: ":x: ${{ github.workflow }} failed on ${{ github.ref_name }}: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}"
We parsed this file and its payload block with a YAML parser to check the indentation that v4 requires.
What happened when we ran v4.0.0 against Slack
On 6 October 2026 we checked out the v4.0.0 tag and ran the action's own dist/index.js with Node.js, passing the same inputs the runner passes, against our test workspace with a throwaway bot token:
| Step we ran | Result |
|---|---|
chat.postMessage with the payload above | Posted. Outputs ok=true, ts, channel_id and time |
A second step with thread_ts: set to the first ts | Posted as a thread reply |
chat.update with ts: set to the first ts | Edited the message in place |
Wrong channel ID, default errors: false | Exit code 0, output ok=false; channel_not_found appeared only in the debug log |
Wrong channel ID, errors: true | Exit code 1, An API error occurred: channel_not_found |
A payload line at the wrong indent | Exit code 1, Invalid input! Failed to parse contents of the provided payload |
The bot had the chat:write.public scope as well as chat:write, so it posted to a public channel it had never joined. With only chat:write, invite it first. We deleted the messages and the test app afterwards.
Use if: always() to post on every run, and ${{ job.status }} in the text to say success or failure.
Option 2: incoming webhook
An incoming webhook posts to one channel, chosen when you create it. No channel ID or invite is needed. Uptime monitors use webhooks the same way; with Uptime Kuma's Slack notification our Down alert reached the channel 49 seconds after the test site stopped.
https://hooks.slack.com/services/T.../B.../..., into the repository secret SLACK_WEBHOOK_URL. - name: Post the build result to Slack
if: always()
uses: slackapi/slack-github-action@v4.0.0
with:
webhook: ${{ secrets.SLACK_WEBHOOK_URL }}
webhook-type: incoming-webhook
payload: |
text: "*${{ github.workflow }}*: ${{ job.status }}\n${{ github.event.head_commit.url }}"
A webhook cannot reply in threads or edit its message. For those, use Option 1. A third setting, webhook-type: webhook-trigger, starts a Slack Workflow Builder workflow instead of posting a message; ideas for those workflows are in Slack Workflow Builder examples.
Step-by-step setup of the webhook URL itself, including private channels and DMs, is in how to create a Slack webhook.
Option 3: curl, with no third-party action
Some organizations allow only GitHub's own actions. A run step with jq and curl, both installed on GitHub's Ubuntu runners, posts to an incoming webhook (the same call outside CI, in Python and Node too, is in Slack webhook curl example):
- name: Post to Slack with curl
if: failure()
env:
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
STATUS: ${{ job.status }}
run: |
jq -n --arg text "$GITHUB_WORKFLOW on $GITHUB_REF_NAME: $STATUS $GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID" '{text: $text}' |
curl -sS --fail -X POST -H 'Content-type: application/json' --data @- "$SLACK_WEBHOOK_URL"
jq builds the JSON, so quotes in a branch name or commit message do not break it. --fail makes the step fail if Slack rejects the post. We ran the command outside GitHub with the same variables: jq printed {"text": "CI on main: failure https://github.com/acme/app/actions/runs/123"}, and a made-up webhook URL made curl exit with an error on Slack's 404, as it should.
Never appear "away" on Slack again
Cloud-based. No downloads. Works 24/7 even when your laptop is off.
Threads, updates and files
With a bot token, each step returns the message's ts as an output, so later steps can reply in a thread or edit it. Give the first step an id:
- name: Announce the deploy
id: deploy_message
uses: slackapi/slack-github-action@v4.0.0
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
text: "Deploy of ${{ github.sha }} started :eyes:"
- run: ./deploy.sh
- name: Reply in the thread
uses: slackapi/slack-github-action@v4.0.0
with:
method: chat.postMessage
token: ${{ secrets.SLACK_BOT_TOKEN }}
payload: |
channel: ${{ secrets.SLACK_CHANNEL_ID }}
thread_ts: "${{ steps.deploy_message.outputs.ts }}"
text: "Deploy finished :rocket:"
Swap the second step's method for chat.update and use ts: instead of thread_ts: to edit the first message in place. method: files.uploadV2 with channel_id, file and filename uploads a test report; it needs the files:write scope and uses the 3-step upload that replaced files.upload. A report up to 1 GB goes through; our Slack file size limit test found that one byte more is refused.
Option 4: the GitHub app for Slack
If you only want to know when workflows finish, the GitHub app for Slack needs no YAML. Install it from the Slack Marketplace, then in a channel run:
/github subscribe owner/repo workflows:{event:"push" branch:"main"}
It posts each workflow run and updates the message as jobs finish. It cannot send custom text; for that, use one of the options above.
Common errors
| Error | Cause | Fix |
|---|---|---|
not_in_channel | The bot is not in the channel | /invite @your-app |
channel_not_found | Wrong channel ID, or a private channel the bot cannot see | Copy the ID from channel details, invite the bot |
missing_scope | The token lacks chat:write | Add the scope and reinstall the app |
invalid_auth | Wrong or revoked token | Copy the xoxb- token again into the secret |
YAML error in payload | Multi-line value not indented (v4 rule) | Indent continuation lines under the key |
| Nothing posts on failure | The step has no if: | Add if: failure() or if: always() |
| Step is green but nothing posted | errors defaults to false, so Slack errors do not fail the step | Add errors: true |
Secrets are not passed to workflows triggered by pull requests from forks, so the step fails there by design. Run it only on push or on pull requests from the same repository.
Long CI logs hit Slack text limits; the numbers and a split function are in Slack message character limit.
FAQ
What is the latest version of slack-github-action?
v4.0.0, released 15 July 2026, commit dcb1066f776dd043e64d0e8ba94ca15cc7e1875d. It requires multi-line payload values to be indented.
How do I send a Slack notification only when a GitHub workflow fails?
Add if: failure() to the Slack step. Use if: always() to post on every run.
Why is my Slack step green when nothing was posted?
The errors input defaults to false, so a Slack error such as channel_not_found does not fail the step. Set errors: true.
Can I send a Slack message from GitHub Actions without a third-party action? Yes. Use curl with a Slack incoming webhook URL in a run step, with jq to build the JSON.
Stop Jiggling Your Mouse.
Join hundreds of remote workers who never worry about their Slack status. Set it up once, stay green forever.
Related Articles
Slack Channel Welcome Message: A Tested Bot and the Free-Plan Catch
Slack has no built-in welcome message per channel, and Workflow Builder is closed to Free workspaces. A 30-line Bolt app that listens for member_joined_channel does the job on any plan. We ran it on 9 November 2026 and timed the welcome.
ngrok for a Slack Bot: What It Needs, and Two Free Alternatives Tested
A Slack bot on localhost needs a public HTTPS URL for events and slash commands, or Socket Mode. We tried ngrok 3.39.13 with no account (ERR_NGROK_4018), a Cloudflare quick tunnel and Socket Mode on 8 November 2026, timed the replies, and broke the tunnel URL on purpose.
Slack Bot with Ollama: A Local LLM Bot in 34 Lines, Tested
A Slack bot that answers @mentions with a model running in Ollama on your own server, in 34 lines of Bolt for Python over Socket Mode. We ran it on 9 November 2026 with qwen2.5:1.5b on a busy CPU-only box and timed every reply.