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-signedstate, 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:
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.
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.How a Slack node runs
Eligibility, classification, and flow navigation are evaluated server-side; theslack_message node is one node the flow walker 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.
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 iconslack_message). The node config persists these fields:
Whether to post to a channel or direct-message a member. Anything other than
"dm" is treated as "channel".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.Target member ID (e.g.
U0123456789), required when slackActionType is "dm". The backend opens (or reuses) the IM channel for that user, then posts.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.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.Retry a failed send. When
true the backend makes up to 4 attempts with a short increasing backoff before giving up.Template variables
slackMessageText supports the same two substitutions as the API Call node: {{userId}} and {{content}}. {{userId}} is the end user’s id; {{content}} is the triggering message content. Both are interpolated before the 4000-character truncation.
chat.postMessage call against the channel ID, with the bot token attached server-side:
Verify
Save the flow and trigger the experience end to end. On success the backend logsslack_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:
200 with { "data": { "ok": true, "ts": "1700000000.000100" } } confirms the token, scopes, and channel membership are all good.
Specific cases and limits
Posting to a channel returns not_in_channel
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.Private channels or members don't appear in the picker
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.DM target rules
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.Message length and formatting
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.
Token revocation
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.Self-hosting
Slack is optional and degrades gracefully. IfSLACK_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.