workbench Docs

Other services

Slack

Connect Slack so an agent can post, search, read history, and handle files as the connecting user.

The Slack integration acts as the person who connected it, not as a bot. Messages it posts show that user's name. slack_search_all searches what that user can see. Channel history covers the private channels and DMs that user belongs to. This is a deliberate choice, because several of these tools have no bot-token equivalent, and it drives the whole app setup.

At a glance#

Plugin idslack
AuthOAuth 2.0, user token
Tools18
Authorization URLhttps://slack.com/oauth/v2/authorize
Token URLhttps://slack.com/api/oauth.v2.access
Proxy basehttps://slack.com/api

The server sends the manifest's scopes in the user_scope parameter, and reads the resulting token from authed_user.access_token in the response.

Set up the OAuth app#

The console steps are Slack's UI, not this server's

Menu names, labels, and page URLs below come from Slack's console and change without notice. If what you see differs, follow Slack's own documentation — the values this server needs (the callback URL and the scopes in the tables below) are unaffected.

Create the app#

api.slack.com/appsCreate New AppFrom scratch. Name it and pick a development workspace.

Add the redirect URL#

OAuth & PermissionsRedirect URLsAdd New Redirect URL:

text
https://<your-workbench-host>/api/auth/plugin/slack/callback

For local development add http://localhost:3000/api/auth/plugin/slack/callback. Save URLs.

Add all 16 scopes under User Token Scopes#

On the same page, scroll to Scopes. There are two boxes. Put every scope from the table below in User Token Scopes. Leave Bot Token Scopes empty.

Copy the credentials#

Settings → Basic Information → App Credentials: Client ID, and Client Secret via Show.

Distribute, if others will connect#

Settings → Manage Distribution → work through the checklist → Activate Public Distribution. Until then only your development workspace can install.

Bot Token Scopes is the wrong box

This is the failure that costs the most time. The manifest's scopes are sent as user_scope, so every scope below belongs in the console's User Token Scopes list. Put them in the bot list instead, and the two sets do not match. The install then fails. The error Slack returns does not name which box is at fault. Slack's OAuth documentation covers the bot/user token split.

Scopes#

All 16 are user scopes.

ScopeWhat it is for
chat:writePost, edit, and delete messages as the user
channels:readList public channels
channels:writeJoin a public channel
groups:readList private channels the user belongs to
im:readList direct-message conversations
mpim:readList group direct messages
channels:historyRead public channel history
groups:historyRead private channel history
im:historyRead direct-message history
mpim:historyRead group direct-message history
reactions:writeAdd and remove reaction emoji
users:readList users and read profiles
users:read.emailRead email addresses — needed to look a user up by email
files:writeUpload files
files:readRead file metadata and download file contents
search:readSearch messages and files

The manifest pairs users:read.email with users:read. It requests search:read as a user scope, which is one of the reasons this integration uses a user token. Slack defines what each scope grants, and which are available as bot rather than user scopes. See Slack's scope reference.

Server configuration#

bash
SLACK_CLIENT_ID=...
SLACK_CLIENT_SECRET=...

Connect#

Portal: Connections → Connect on the Slack card → approve in Slack.

Agent:

text
connect({ integration: "slack" })
wait_for_connection({ connectionId })

Tools#

ToolPurpose
slack_send_messagePost to a channel, or reply in a thread with threadTs
slack_update_messageEdit an existing message by channel and ts
slack_delete_messagePermanently delete a message — irreversible
slack_send_dmSend a direct message to a user
slack_list_channelsList channels
slack_join_channelJoin a public channel so its history is readable
slack_get_channel_historyRead a channel's messages
slack_get_thread_repliesRead the replies in a thread
slack_get_permalinkGet a shareable permalink for a message
slack_add_reactionAdd a reaction emoji
slack_remove_reactionRemove a reaction you added
slack_search_allSearch messages and files across the workspace
slack_lookup_userExact-email lookup returning a slim user
slack_find_usersFuzzy find by name or email, falling back to substring match
slack_list_usersPage through workspace members, bots and deactivated included
slack_upload_fileUpload a file to a channel
slack_get_file_infoFile metadata, including the download URL
slack_download_fileDownload a file by its private URL

Notes and gotchas#

`slack_download_file` guards its own credential

Slack file URLs redirect to a presigned CDN host. The tool checks that the URL is https on slack.com, files.slack.com, or a *.slack.com subdomain before it attaches the token. It then follows the redirect chain itself, at most five requests in total, so at most four redirects. It drops the bearer the moment the host stops being Slack, so the token cannot be redirected off-platform. It rejects non-https redirects outright.

It also treats a text/html response as not_authed_or_not_found rather than returning the bytes. Slack answers an unauthorized file request with 200 and a login page, not a 403. A status-code check alone would therefore hand you a login page as if it were your file. If you see that error, the usual cause is a missing files:read grant.

slack_delete_message is marked destructive in its own description and cannot be undone. Keep the ts from slack_send_message if you may later edit or delete what you posted.

Slack's Web API answers errors with HTTP 200 and { "ok": false, "error": "..." }. The server relies on this when it handles the token exchange, and the tools pass the envelope straight through. A tool result that looks successful but carries ok:false is a failure. Read the error field.

Reading a channel the user has not joined commonly returns not_in_channelslack_join_channel fixes it for public channels.

slack_upload_file uses Slack's external upload flow: files.getUploadURLExternal, then files.completeUploadExternal. It does not use files.upload, which was not usable from a newly created app when the integration was built. See Slack's file-upload documentation for the current state of those methods.

Adding a scope after users have connected requires each of them to reconnect. Slack does not upgrade an existing grant.