> ## Documentation Index
> Fetch the complete documentation index at: https://docs.firstflow.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Slack

> Post a channel message or DM from a flow's slack_message node OAuth-connected, server-side, and fail-open.

A **Slack message** node lets a flow post into a channel or DM a workspace member when execution reaches it route an escalation, alert on a survey answer, or ping a channel when an experience fires. Connecting takes one OAuth flow per workspace; after that, every agent in the workspace can drop the node into a flow.

The node runs **server-side**, inside the same flow walk that composes the widget. The browser never sees the Slack bot token, and the send happens whether or not the end user has a widget open.

## Connect the workspace

Connection is a standard OAuth v2 install. In the dashboard, open **Connections**, choose **Slack**, and complete the authorization. The backend mints a CSRF-signed `state`, redirects you to `https://slack.com/oauth/v2/authorize`, and Slack returns to `/integrations/slack/callback`. There the backend exchanges the code at `oauth.v2.access` for a **bot token**, encrypts it with AES-256-GCM, and stores it against your organization.

The install requests exactly these bot scopes:

| Scope           | Why it's needed                                                           |
| --------------- | ------------------------------------------------------------------------- |
| `chat:write`    | Post messages (channel and DM).                                           |
| `channels:read` | List public channels for the channel picker.                              |
| `groups:read`   | List private channels. Some workspaces require admin approval.            |
| `users:read`    | List members for the DM target picker.                                    |
| `im:write`      | Open a DM channel (`conversations.open`) before sending a direct message. |

Once connected, the connection card shows the workspace name, who installed it, and which agents currently reference a `slack_message` node. The bot token does not expire; FirstFlow validates it with `auth.test` rather than refreshing it.

<Note>
  The bot must be a **member of any channel you post to**. Slack returns `not_in_channel` for a channel the app hasn't joined invite the app to the channel first. Public-channel listing works without joining, but posting does not.
</Note>

## How a Slack node runs

Eligibility, classification, and flow navigation are evaluated server-side; the `slack_message` node is one node the [flow walker](/concepts/flows-and-nodes) executes during that walk. When the walker hits the node it loads the org's encrypted credentials, interpolates the message, and calls the Slack Web API. The send is **fail-open**: a failure is logged and the flow continues by default, so a Slack outage never blocks the user-facing experience.

```mermaid theme={null}
flowchart LR
  A[Flow reaches<br/>slack_message node] --> B{Eager or<br/>deferred?}
  B -->|"guide flow (eager)"| C[Send now]
  B -->|"client-paced (deferred)"| D[Queue for the<br/>step the user reaches]
  D -->|"flow.next"| C
  C --> E[Decrypt bot token<br/>interpolate message]
  E --> F{Channel or DM?}
  F -->|channel| G["chat.postMessage(channelId)"]
  F -->|dm| H["conversations.open → chat.postMessage"]
  G --> I{ok?}
  H --> I
  I -->|yes| J[Continue flow]
  I -->|no| K[Log + fail-open<br/>continue flow]

  style C fill:#fde68a,stroke:#d97706,color:#000
  style E fill:#fde68a,stroke:#d97706,color:#000
  style J fill:#bbf7d0,stroke:#16a34a,color:#000
  style K fill:#fecaca,stroke:#dc2626,color:#000
```

**Eager vs deferred.** In a single-shot guide flow the node fires the moment the walk reaches it. In a client-paced multi-step flow the node is *deferred*: it's recorded against the content step it precedes and fires only when the user advances to that step with `flow.next`. This keeps "step 3 posts to Slack" honest the post happens when the user actually arrives at step 3, not when the flow is first composed.

## Configure the node

Add a **Slack message** node in the flow editor (it carries the runtime icon `slack_message`). The node config persists these fields:

<ParamField path="slackActionType" type="&#x22;channel&#x22; | &#x22;dm&#x22;" default="&#x22;channel&#x22;">
  Whether to post to a channel or direct-message a member. Anything other than `"dm"` is treated as `"channel"`.
</ParamField>

<ParamField path="slackChannelId" type="string">
  Target channel ID (e.g. `C0123456789`), required when `slackActionType` is `"channel"`. Pick it from the channel list the node sends the channel **ID**, never the name.
</ParamField>

<ParamField path="slackUserId" type="string">
  Target member ID (e.g. `U0123456789`), required when `slackActionType` is `"dm"`. The backend opens (or reuses) the IM channel for that user, then posts.
</ParamField>

<ParamField path="slackMessageText" type="string" required>
  The message body. Slack mrkdwn is on, so `*bold*`, `_italic_`, and `` `code` `` render. Empty (after interpolation and trim) is a no-op failure. Truncated to **4000 characters**.
</ParamField>

<ParamField path="slackContinueOnFailure" type="boolean" default="true">
  Keep walking the flow when the send fails. Set `false` to halt the walk on a failed send use this only when the downstream steps genuinely depend on the notification.
</ParamField>

<ParamField path="slackRetryOnFailure" type="boolean" default="false">
  Retry a failed send. When `true` the backend makes up to **4 attempts** with a short increasing backoff before giving up.
</ParamField>

### Template variables

`slackMessageText` supports the same two substitutions as the [API Call node](/integrations/api-calls): `{{userId}}` and `{{content}}`. `{{userId}}` is the end user's id; `{{content}}` is the triggering message content. Both are interpolated before the 4000-character truncation.

```text theme={null}
🚨 Escalation from {{userId}}

"{{content}}"

Picked up automatically please respond in-thread.
```

A channel post for the config below resolves to a `chat.postMessage` call against the channel ID, with the bot token attached server-side:

```json theme={null}
{
  "slackActionType": "channel",
  "slackChannelId": "C0123456789",
  "slackMessageText": "New high-priority signup: {{userId}}",
  "slackContinueOnFailure": true,
  "slackRetryOnFailure": true
}
```

For a DM, switch the target and supply a member ID:

```json theme={null}
{
  "slackActionType": "dm",
  "slackUserId": "U0123456789",
  "slackMessageText": "You were assigned a new escalation from {{userId}}."
}
```

## Verify

Save the flow and trigger the experience end to end. On success the backend logs `slack_message node <id> sent` and the message lands in the channel or DM. On a non-fatal failure you'll see `slack_message node <id> not sent: <reason>` common reasons are `not_connected`, `missing_target`, `empty_message`, and `send_failed`. None of these surface to the end user; the reason exists for diagnostics only.

You can also smoke-test the connection independently of any flow with the test-send endpoint, which posts to the connection's default channel:

```bash theme={null}
curl -X POST https://api.firstflow.app/integrations/slack/send-message \
  -H "Authorization: Bearer <dashboard-session>" \
  -H "Content-Type: application/json" \
  -d '{ "text": "FirstFlow Slack connection works." }'
```

A `200` with `{ "data": { "ok": true, "ts": "1700000000.000100" } }` confirms the token, scopes, and channel membership are all good.

## Specific cases and limits

<AccordionGroup>
  <Accordion title="Posting to a channel returns not_in_channel">
    The bot can *see* public channels via `channels:read` but cannot *post* to a channel it hasn't joined. Invite the app to the channel (`/invite @YourApp`) and retry. The backend maps this to a clear "invite the app to the channel first" error.
  </Accordion>

  <Accordion title="Private channels or members don't appear in the picker">
    Private channels need `groups:read` and members need `users:read`. If a scope was added after install, reconnect the workspace so Slack re-grants it a `missing_scope` error means the installed token predates the scope.
  </Accordion>

  <Accordion title="DM target rules">
    The DM flow calls `conversations.open` then `chat.postMessage`. Bots, deactivated accounts, and the `USLACKBOT` system user are filtered out of the picker, and Slack rejects DMs to other bots with `cannot_dm_bot`.
  </Accordion>

  <Accordion title="Message length and formatting">
    Messages are truncated to 4000 characters after interpolation. mrkdwn is enabled, so Slack formatting applies; the node sends plain text, not Block Kit. There is no thread-reply option.
  </Accordion>

  <Accordion title="Token revocation">
    Slack bot tokens don't expire, but they can be revoked by uninstalling the app. FirstFlow exposes a `POST /integrations/slack/events` endpoint (signature-verified with `SLACK_SIGNING_SECRET`) that acknowledges `app_uninstalled` and `tokens_revoked` events. After a revoke, sends fail with `invalid_auth`/`token_revoked` and you must reconnect.
  </Accordion>
</AccordionGroup>

### Self-hosting

Slack is optional and degrades gracefully. If `SLACK_CLIENT_ID`, `SLACK_CLIENT_SECRET`, and `SLACK_SIGNING_SECRET` aren't set, the provider simply isn't available and nothing else breaks. For local development, set `SLACK_BOT_TOKEN` to a static bot token to skip the OAuth dance entirely the callback recognizes a synthetic code and adopts the token directly. The callback's redirect target comes from `BACKEND_PUBLIC_URL`. See [Configuration](/self-hosting/configuration).

## Next

For HTTP delivery beyond Slack use a [webhook node](/integrations/webhooks); to call an external API mid-flow and feed the result downstream use an [API call node](/integrations/api-calls); for where action nodes sit in a flow see [Flows & nodes](/concepts/flows-and-nodes).
