All integrations

Slack

SLACK · MESSAGING

Connect Slack to your AI app with scoped access.

Connect supported Slack workflows to your AI app through Fabriq Developer. Make the token type, approved scopes and available actions explicit before your agent calls a tool.

Connect Slack with Fabriq Developer

Your user consents at Slack. The credential lands in a vault your agent cannot read, and the tool list it gets back is the intersection of that user’s grant and the agent’s own scopes.

provider “slack”
from af_sdk import AgenticFabriq

af = AgenticFabriq()                      # reads AF_APP_ID / AF_APP_SECRET

url = await af.initiate_connection(request, provider="slack")   # consent at Slack
return redirect(url)

async with await af.for_user(request) as session:
    tools = await session.get_tools()     # only what this user granted

Provider names are surfaces, not vendors, and the authoritative list is the capabilities endpoint rather than any page including this one — an unknown name returns 400 with the valid list. Fabriq Developer is free to start and needs no provider paperwork. Start free or read the quickstart.

Ship the Slack integration

Every route, header, field name and error string below is the gateway’s own, and every tool id and scope comes from the generated schema behind the action list further down this page.

Prerequisites

Before the redirect above can do anything, four things have to be in place on the Fabriq side and one or two on the Slack side.

  • A Fabriq Developer app, and its AF_APP_ID and AF_APP_SECRET. They ride on every app-scoped call as X-App-Id and X-App-Secret.
  • A Redirect URL saved on the agent’s Connect & webhooks page. It is checked before anything else, so leaving it empty fails every connect with “B2B2C OAuth callback URL not configured” and hides whatever else is wrong.
  • An external user id of your own, one per real person, created with POST /api/v1/apps/{app_id}/external-users. The call is idempotent and a 409 means the id already exists.
  • Grants on the agent itself. The tool list a user gets back is the intersection of that user’s grant and the agent’s own scopes, so an agent with no overlapping grant sees nothing even after a clean consent.
  • A Slack workspace your user can authorize in. The connector’s channel, private-channel and DM reads are documented in its own schema as bot scopes, and Slack describes a bot token (xoxb-) as tied to your app rather than to a user’s identity, while a user token (xoxp-) acts on behalf of a member.
  • The bot added to anything private you expect to read. slack_get_group_messages requires the bot to be invited to the private channel first, and slack_get_dm_messages only sees DM conversations the bot is part of.
Create the user
# AF_BASE is your gateway base URL, from the Fabriq Developer console.
export AF_APP_ID=...        # sent as X-App-Id
export AF_APP_SECRET=...    # sent as X-App-Secret

# One external user id per real person. Idempotent; 409 means it exists.
curl -X POST "$AF_BASE/api/v1/apps/$AF_APP_ID/external-users" \
  -H "X-App-Id: $AF_APP_ID" -H "X-App-Secret: $AF_APP_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{"external_user_id": "user_8412"}'

Connect an account

The route is /integrations/slack-connector but the gateway provider id is slack. Use slack in the path; the slug is a URL, not an identifier.

Post the initiate, redirect the user to oauth_url, and let them authorize in their workspace. The scopes this connector declares are channels:history, channels:read, chat:write, groups:history, groups:read, im:history, im:read and users:read.

Start the grant
# 1. Ask the gateway whether this deployment can connect slack at all.
#    This endpoint is the authoritative list, not any page including this one.
curl "$AF_BASE/api/v1/apps/$AF_APP_ID/external-users/providers/capabilities" \
  -H "X-App-Id: $AF_APP_ID" -H "X-App-Secret: $AF_APP_SECRET"
#    Route on oauth || key_connect. The other flags describe or explain.

# 2. Start the grant, then redirect the user to oauth_url.
curl -X POST \
  "$AF_BASE/api/v1/apps/$AF_APP_ID/external-users/user_8412/oauth/slack/initiate" \
  -H "X-App-Id: $AF_APP_ID" -H "X-App-Secret: $AF_APP_SECRET" \
  -H 'Content-Type: application/json' \
  -d '{"connection_id": "default"}'
#    Response fields: oauth_url, state, connection_id, credential_source.

Make one tool call

slack_list_channels is the first call worth making: a GET at /api/v1/tools/slack/channels, no required arguments, one scope (channels:read), and it returns the channel ids every other Slack tool wants as input.

slack_get_connection_info is cheaper still and declares no scope at all — it answers which workspace this connection points at, and whether the connection is live, before you spend a scope on anything.

Call one tool
# A user-scoped token, good for 900 seconds. Cache it per user; re-mint
# roughly 60s before expiry, because these tokens cannot be refreshed in place.
TOKEN=$(curl -s -X POST \
  "$AF_BASE/api/v1/apps/$AF_APP_ID/external-users/user_8412/token" \
  -H "X-App-Id: $AF_APP_ID" -H "X-App-Secret: $AF_APP_SECRET" \
  -H 'Content-Type: application/json' -d '{}' | jq -r .access_token)

# External-user tokens go to /mcp/external. Never /mcp: both routes answer 200
# for an external token, so the wrong one fails silently.
curl -X POST "$AF_BASE/mcp/external" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"jsonrpc": "2.0", "id": 1, "method": "tools/call",
       "params": {"name": "slack_list_channels", "arguments": {}}}'

Inspect the result

Scope to action, as the connector declares it: channels:read covers slack_list_channels, and channels:read with channels:history covers slack_get_messages. groups:read and groups:history cover the private-channel reads, im:read and im:history the DM reads, users:read slack_list_users and slack_get_user, and chat:write slack_send_message. slack_get_connection_info declares no scope.

That is ten actions, nine of them reads. This connector is not the Slack API; treat the list above as the whole surface rather than a sample of it.

  • tools/list has two gates: the connection has to exist, and the agent’s grants have to intersect the connection’s scopes. Connected with zero tools is a real state, so say “connected but no actions live” rather than showing the integration as on.
  • A listed connection is not a working one. The initiate writes the row before the user ever reaches the provider: status “configured” means the row exists and nothing else does, and it yields zero tools. Key every user-facing state off status == "connected".
  • Do not infer readiness from granted_scopes being non-empty. Some providers legitimately grant none.
  • The callback query parameters — status, tool, connection_id, external_user_id — are not signed. Treat them as a UI hint, resolve the user from your own session, and confirm by re-reading the connections list. The signed channel is the connection.completed webhook.
Read the connection back
# What this user can actually do right now.
curl -X POST "$AF_BASE/mcp/external" \
  -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
  -d '{"jsonrpc": "2.0", "id": 2, "method": "tools/list", "params": {}}'

# And the connection row behind it. Only status "connected" is usable.
curl "$AF_BASE/api/v1/apps/$AF_APP_ID/external-users/user_8412/connections" \
  -H "X-App-Id: $AF_APP_ID" -H "X-App-Secret: $AF_APP_SECRET"

# Shape of a connection row, using the field names the gateway documents:
# [
#   {
#     "tool": "slack",
#     "connection_id": "default",
#     "status": "connected",
#     "granted_scopes": ["channels:read"]
#   }
# ]

The payload above shows the shape of a response using the field names the gateway documents. It is illustrative: it was not captured from a live Slack account, so treat the values as placeholders and read your own response.

Disconnect

Deleting the connection is one app-scoped call, and it is the only way to take Slack away from an agent without taking the agent down.

  • The delete removes the stored credential and the connection row on the Fabriq side, so the user has to authorize again to reconnect.
  • Whether the authorization is still listed in the provider’s own connected-apps screen is the provider’s state, not Fabriq’s. If your flow needs the grant gone there too, send the user to the provider and check.
  • Drop any cached user token when you disconnect, and on a connection.needs_reauth webhook.
Delete the connection
# ?tool= is required and a 422 is what you get without it: connection_id
# defaults to "default" for every provider, so a user with two providers
# connected has two rows sharing that id and the delete would be ambiguous.
curl -X DELETE \
  "$AF_BASE/api/v1/apps/$AF_APP_ID/external-users/user_8412/connections/default?tool=slack" \
  -H "X-App-Id: $AF_APP_ID" -H "X-App-Secret: $AF_APP_SECRET"

Troubleshooting

  • Private-channel or DM reads return nothing even though the scopes are granted: the bot has to be in the conversation. slack_get_group_messages needs the bot invited to the channel, and slack_get_dm_messages shows only DMs involving the bot, not the user’s DMs with other people.
  • The message posted as the app rather than as the person: slack_send_message sends as the bot by default, and sending as the authenticated user means passing as_user=true, which the tool’s own schema says requires user scopes.
  • Decide the token type before you write the copy on your connect button. Slack’s own token documentation separates bot tokens, which are tied only to your app, from user tokens, which take actions on behalf of a member — and the two produce different author names on the same message.
  • Consent completed but tools/list comes back empty: diff the authorize URL’s scope= parameter against the connection row’s granted_scopes before blaming the console. The URL tells you what was actually requested, and a scope never requested can never be granted.
  • Every connect fails with “B2B2C OAuth callback URL not configured”: the Redirect URL on the agent’s Connect & webhooks page is empty. It is checked first, so it masks every other misconfiguration.
  • Calls return 200 and nothing happens: an external-user token was posted to /mcp instead of /mcp/external. Both answer 200, so this one fails silently — pin the route with a test.
  • An unknown provider name returns 400 with the list of valid names. Read that error; it is accurate. Provider names are surfaces rather than vendors.

Slack-side requirements checked against Slack token types (bot vs user tokens) (2026-10-29).

Connecting Slack in practice

Slack’s Marketplace guidelines put the scopes an autonomous agent most wants, search:read and the admin.* family, on the list a non-partner app is unlikely to be approved for, and route files:read and every *:history scope into an enhanced review that is separate from the normal app review. With token rotation enabled, access tokens last twelve hours and each refresh token is single use, so the token store has to be transactional rather than best effort.

Scopes that draw scrutiny

  • search:read Listed among the scopes a non-partner Marketplace app is unlikely to be approved for, which removes the obvious way to answer "find where we discussed this".
  • admin "Administer a workspace". admin.* scopes are generally prohibited for Marketplace apps.
  • channels:history Every *:history scope triggers Slack’s enhanced review, and message history is what a retrieval agent is actually after.
  • files:read Enhanced review, because it reaches every file shared in the conversations the app can see.
  • groups:write As a user token it lets the bot add itself to private conversations, so it is called out for enhanced review.
  • users:read.email Exposes member email addresses, which is the join key most integrations want and most security teams query.
  • auditlogs:read Reads events across all workspaces, channels and users in the org.

Rate limits

  • Web API methods are tiered: Tier 1 is 1+ per minute, Tier 2 20+, Tier 3 50+, Tier 4 100+.
  • chat.postMessage sits in a special tier at roughly one message per second per channel, with a separate workspace-wide limit Slack does not put a number on.
  • Slack states the tier limits are subject to change, so treating a tier number as a contract is a mistake.

Who has to approve

By default members can install apps without approval from a Workspace Owner. Once an owner turns on "Only allow pre-approved apps" in App Management Settings, every install becomes a request. On Enterprise Grid an admin resolves those with admin.apps.approve, which needs the admin.apps:write scope and targets either a team_id or an enterprise_id but not both, and an app approved at the org level has to be re-approved there on any scope change.

Worth knowing

  • With token rotation enabled, access tokens come back with expires_in of 43,200 seconds, that is twelve hours.
  • Refresh tokens are single use and are revoked after a short grace period once redeemed through oauth.v2.access. Slack does not publish the length of that grace period, so a retry that reuses a refresh token can lock the connection out.
  • At most two tokens are active in a rotation chain at once, and older ones are revoked automatically, so a blue-green deploy that keeps a stale token will lose it.
  • identity.*, triggers:* and workflow.steps:execute are also on the prohibited list, so an agent design that leans on Slack workflow triggers will not clear review.

Checked against Slack Marketplace requirements (2026-09-29), Slack OAuth scopes (2026-09-29), Web API rate limits (2026-09-29), Token rotation (2026-09-29), Manage app approval (2026-09-29), admin.apps.approve (2026-09-29)

Acts as the person, not as itself

Each user connects their own account. Every call carries both identities — the agent and the person it is acting for — so the agent can never reach past what that individual can already do.

Credentials never touch the agent

Tokens live in the vault and attach server-side at call time. The agent holds a session, not a secret, and revoking access does not mean rotating a key.

Every call on the record

Who asked, which agent acted, which action ran, and the verdict that let it through — one audit trail across every integration, not one per vendor.

What an agent can do

Each action is granted on its own. An agent allowed to read is not thereby allowed to write, and the scope beside each row is what the acting user must have connected for it to run at all.

slack_get_connection_infoREAD

Get Slack connection information including workspace name, bot user ID, and connection status.

no provider scope
slack_get_dm_messagesREAD

Get recent messages from a direct message (DM) conversation the bot is part of. Requires BOT scopes (im:history, im:read). Only DMs involving the bot are visible — not the user's DMs with other people.

im:historyim:read
slack_get_group_messagesREAD

Get recent messages from a private channel the bot has been added to. Requires BOT scopes (groups:history, groups:read). The bot must be invited to the channel first.

groups:historygroups:read
slack_get_messagesREAD

Get recent messages from a public Slack channel the bot can access. Uses BOT scopes (channels:read, channels:history); the bot auto-joins public channels on demand. Cannot read private channels or DMs the bot has not been added to — use slack_get_group_messages or slack_get_dm_messages for those.

channels:readchannels:history
slack_get_userREAD

Get information about a Slack user by their user ID.

users:read
slack_list_channelsREAD

List public Slack channels in the workspace. Uses BOT scopes (channels:read). Returns channel IDs, names, and metadata.

channels:read
slack_list_dm_channelsREAD

List direct message (DM) channels the bot participates in. Requires BOT scope (im:read).

im:read
slack_list_groupsREAD

List private channels the bot has been added to. Requires BOT scope (groups:read).

groups:read
slack_list_usersREAD

List users in the Slack workspace.

users:read
slack_send_messageWRITE

Send a message to a Slack channel or DM. By default sends as bot; set as_user=true to send as the authenticated user (requires user scopes). Returns the timestamp of the sent message.

chat:write

Put Slack behind one governed endpoint.

Same permissions, same audit trail, whatever else you connect next.