Skip to main content
18 toolsOAuth bearer
slack_example.py
Post, edit and delete messages; list channels and members; read a channel or a thread; react; search. Every Slack tool takes ids rather than names — 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, and configure() 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
A bot token (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 a Tool, called with ainvoke as in the snippet above. The name links to its parameters, its response and what it costs. 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 with 200 OK and a body:
slack_failure_200.json
A runtime that trusts the status line hands that to the model as though the message had sent. The model reports success, and the mistake is silent and downstream. That is not patched per tool. It is declared once as SLACK_ENVELOPE on the factory, and the runtime enforces it on every call — including calls by tools somebody adds next year:
slack_envelope.py
Ten error codes are listed as credential errors, so 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
The handlers preserve 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 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.
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.
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.
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.
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.