18 toolsOAuth bearer
slack_example.py
C123ABC456,
not #deploys — so conversations_list and users_list are how an agent resolves
what a person said into what the API accepts.
Slack is the pack that made envelopes exist.
Authenticating
This pack takes a Slack OAuth bearer token, andconfigure() is optional when $SLACK_BOT_TOKEN is set. Which credential provider you hand it depends on whose account the calls run as.
Refer to Slack’s provider page for the “SLACK” constant the snippets below name, the scopes these 18 tools ask for, and this server’s refresh behaviour.
A token you hold
For a script, or a notebook.EnvTokenProvider re-reads the variable on every call, so a token rotated beside the process is picked up without a restart; StaticTokenProvider takes one you already hold as a string. Neither renews anything, so the calls stop when the token expires.
slack_script.py
One account, refreshed
For an agent or a server acting as you.OAuth2Client turns a client registration and a stored refresh token into an access token, and renews it before it lapses.
slack_agent.py
No refresh token yet? Getting the first grant is the one-time consent flow that hands you one.
Many end users
For a product whose users each connect their own account.SubjectProvider builds one credential per user through a factory you write, and use_subject names the user a call acts for. Your users’ accounts is the consent route inside your app; serving many users is the per-subject cache and its eviction.
slack_server.py
xoxb-…) covers every tool except search_messages, which Slack
only answers for a user token — see the gotcha below.
The client
oauth_tool_factory is the whole client: a thin wrapper over httpx that attaches your token and these endpoint constants to each request. slack-sdk does not enter your dependency tree.credentials is whichever of the three you built in Authenticating. A pack takes it through configure(); a client you build takes the same object as credential_provider, and has no configure() of its own.
Paging through a list
A cursor belongs to the tool that returns it, so it is declared on that tool’s builder call:slack_pagination.py
Pagination is declared on
conversations_list, conversations_history, conversations_replies, users_list and search_messages. The other 13 take no cursor.Tools
Each is aTool, called with
ainvoke as in the snippet above. The name
links to its parameters, its response and what it costs.
Chatchat_post_messagePOSTSend a message to a Slack channel, private group, or DM.chat_updatePOSTUpdate an existing Slack message.chat_deletePOSTDelete a Slack message.chat_post_ephemeralPOSTPost a message only one person in a channel can see, and which vanishes when they reload.chat_schedule_messagePOSTSchedule a message for later, up to 120 days ahead.Conversationsconversations_listGETList channels in the workspace.conversations_historyGETFetch recent messages from a Slack channel.conversations_repliesGETFetch a thread of messages.conversations_openPOSTOpen a direct message with one person, or a group message with up to eight.conversations_createPOSTCreate a channel.conversations_invitePOSTAdd people to a channel.conversations_joinPOSTJoin a public channel.Usersusers_listGETList members of the workspace.users_infoGETGet profile information about a single Slack user.Reactionsreactions_addPOSTAdd an emoji reaction to a Slack message.reactions_removePOSTTake an emoji reaction off a Slack message.reactions_getGETRead the reactions on a message.Searchsearch_messagesGETSearch messages across the workspace.
No tool declares quota_cost. Slack does not charge quota units per call the way
Google does — each method sits in a tier with a requests-per-minute allowance — so
a number here would be fabricated. Each tool records its tier in a comment beside
its declaration in the pack source.
Failure arrives as HTTP 200
Slack answers a rejected call with200 OK and a body:
slack_failure_200.json
SLACK_ENVELOPE on the
factory, and the runtime enforces it on every call — including calls by tools
somebody adds next year:
slack_envelope.py
invalid_auth,
token_revoked and missing_scope raise CredentialError while
channel_not_found raises APIError. A host application can therefore tell
“refresh the token and retry” from “the request was wrong” without parsing
strings. detail_fields carries needed and warning through onto the error,
and needed is the one that says which scope was missing.
APIError.status_code is 200 on these. That really was the status, and it is the
fact that surprises whoever reads the log later.
Response trimming
Slack objects are large: block trees, attachment arrays, edit history, profile image URLs in eight sizes.conversations_list, conversations_history,
conversations_replies and users_list run handlers that keep what an agent can
act on and drop the rest. They are exported:
slack_response_handlers.py
response_metadata.next_cursor, because the pagination
declaration reads it back out of the trimmed payload. search_messages has no
handler, so it returns Slack’s search response as it arrived.
Trimming is context economy only. The ok:false check is not in these handlers,
and a handler here never sees a failed payload — the envelope raised first.
Gotchas
search_messages needs a user token, not a bot token
search_messages needs a user token, not a bot token
search.messages is user-token only, and wants search:read. Called with a
bot token, Slack returns not_allowed_token_type — which is in
credential_errors, so it surfaces as a CredentialError naming the
provider rather than as a generic failure. The scopes this pack declares are
the bot-token set; search:read is not among them.An empty cursor is the last page
An empty cursor is the last page
Slack returns
"" in response_metadata.next_cursor on the final page, not
a missing key. An empty cursor counts as absent, and has_more is declared
besides — so the walk terminates.A bot can only edit and delete its own messages
A bot can only edit and delete its own messages
chat_update and chat_delete work on messages posted by the authenticated
token. A bot token cannot edit a human’s message, and the refusal comes back
as an ok:false body rather than a 403.The bot must be in the channel
The bot must be in the channel
conversations_history on a public channel the bot has not joined returns
not_in_channel. Reading requires membership as well as scope — the
channels:history scope alone is not enough.Slack is snake_case in both directions
Slack is snake_case in both directions
thread_ts, include_all_metadata, reply_broadcast. Both body_case and
query_case are snake on this factory, which for once means the
cascade does nothing — worth saying out loud,
since the Google packs next door are the opposite.Related
- Slack — install flow, scopes, token types
- Envelopes — the declaration in full
- Tool validation and error handling