Guide · Verified October 31, 2026

Slack target guide

Short answer: A Hooks Slack destination posts either to an incoming webhook URL already bound to a channel, or to chat.postMessage with a bot token and channel id so one app can reach many channels.

Slack reports most failures as HTTP 200 with `"ok": false`, so the destination reads the body and not just the status. Templates render `text` and, in chat mode, a Block Kit `blocks` array; both use the same event fields as every other target.

1. Choose a mode and prepare Slack

Incoming webhook: create a Slack app, enable Incoming Webhooks, Add New Webhook to Workspace, and copy the URL bound to one channel. chat.postMessage: add the chat:write scope, install the app, copy the bot token (xoxb-...), invite the bot to the channel, and use the channel id (C0123456789) so renames do not break delivery.

2. Store the credential as a project secret

Settings → Security → Project secrets → add SLACK_WEBHOOK_URL (the full webhook URL) or SLACK_BOT_TOKEN. Configs accept only {{secret.NAME}} references; literal values are rejected, and the Authorization header is always recorded as [redacted].

Destination config (incoming webhook)

{
  "mode": "webhook",
  "webhookUrl": "{{secret.SLACK_WEBHOOK_URL}}",
  "text": "New {{event.type}}: {{event.subject.title}}\n{{event.subject.url}}"
}

3. Write text and Block Kit templates

`text` defaults to the event title and URL; in chat mode it doubles as the notification fallback when `blocks` is set. `blocks` renders a JSON array of Block Kit blocks — invalid JSON, or JSON that is not an array, fails the delivery with INVALID_SLACK_BLOCKS and nothing is sent. A channel override exists only in chat mode; incoming webhooks ignore it.

Destination config (chat.postMessage)

{
  "mode": "chat",
  "token": "{{secret.SLACK_BOT_TOKEN}}",
  "channel": "C0123456789",
  "text": "New {{event.type}}: {{event.subject.title}}",
  "blocks": "[{\"type\":\"section\",\"text\":{\"type\":\"mrkdwn\",\"text\":\"*{{event.subject.title}}*\\n{{event.subject.url}}\"}}]"
}

4. Read the response correctly

`{"ok": true}` and the plain `ok` body of incoming webhooks succeed. `{"ok": false, "error": "channel_not_found"}` fails permanently with SLACK_ERROR naming the Slack error, while `ratelimited` retries as SLACK_RATE_LIMITED. HTTP 429 honors Retry-After; 408, 425, and 5xx retry; 3xx is blocked as REDIRECT_BLOCKED. On success the attempt keeps Slack's response body, including `ts`.

Frequently asked questions

Incoming webhook or chat.postMessage?

Incoming webhooks are one channel per URL and need no bot setup. chat.postMessage needs a bot token and chat:write but lets one app post to any channel it was invited to, and supports Block Kit with a fallback text.

Why did a Slack delivery fail with HTTP 200?

Slack's Web API reports errors in the body: {"ok": false, "error": "..."}. Hooks checks the body, so a 200 with ok:false is recorded as failed (SLACK_ERROR) or retried when Slack says ratelimited.

What does INVALID_SLACK_BLOCKS mean?

The rendered `blocks` field was not a valid JSON array. Render only JSON from the template, escape quotes inside strings, and keep a plain `text` fallback.

Can one destination post to several channels?

Not directly — a destination has one config. Use chat mode and one destination per channel, or route to a lower-level HTTP target that posts to chat.postMessage itself.

Reference

Keep going