The real problem
Why this is harder than it looks
Slack's API is genuinely well-built and the docs are good. The OAuth flow is straightforward to read about. The problems start when you sit down to connect a real agent that acts on behalf of real users across multiple workspaces.
The first thing most developers miss is that Slack's OAuth flow produces two fundamentally different token types, and your agent probably needs both. A bot token (xoxb-) represents your app's independent identity in a workspace — useful for sending notifications or managing channels programmatically. A user token (xoxp-) represents a specific human — needed when your agent should act as that person: sending messages in their name, reading their DMs, or setting their status. These require separate scope declarations in your app config (scope for bot, user_scope for user), separate token storage, and separate handling in your code. Conflating them produces silent failures where your agent either lacks permission or posts from the wrong identity entirely.
Then there's token rotation. By default, Slack tokens don't expire — but once you enable token rotation (which Slack now recommends and requires for distributed apps), every access token expires after 12 hours. Refresh tokens also rotate on use: each time you exchange a refresh token, the old one is revoked and a new one is issued. If two concurrent requests attempt to refresh the same token, you hit Slack's limit of 2 active tokens and the oldest is revoked. Building refresh logic that's safe under concurrency — and that stores the new refresh token atomically after each rotation — is non-trivial. Getting it wrong means users silently lose access and have to re-authorize from scratch.
On top of that: Slack's scope model is granular in ways that catch people off guard. channels:history reads public channel messages, but groups:history is a separate scope for private channels, and mpim:history covers multi-person DMs. Ask for the wrong one and you get a missing_scope error at runtime — not an auth error — which looks identical to a data access problem during debugging. Scalekit handles token type selection, rotation logic, scope configuration, and revocation detection so your code only has to call the tools.
Capabilities
What your agent can do with Slack
Once connected, your agent has 18 pre-built tools covering messaging, channels, users, and reactions:
- Send and manage messages: post to channels or DMs, reply in threads, update or delete sent messages
- Read conversation history: fetch channel messages with time-range filtering and pagination, retrieve full thread replies
- Manage channels: list, create, join, leave, and invite users to public and private channels
- Look up users: get user profiles by ID or email, check presence status, set user status
- React and pin: add emoji reactions to messages, pin important messages to channels
Setup context
What we're building
This guide connects a team communication agent to Slack — helping users send updates, surface messages, manage channels, and act on conversations without leaving your product.
🤖
Example agent
Comms assistant sending messages and reading channel history on behalf of each user
🔐
Auth model
B2B SaaS — each user connects their own workspace. identifier = your user ID
Setup
1 Setup: One SDK, One credential
Install the Scalekit SDK. The only credential your application manages is the Scalekit API key — no Slack secrets, no user tokens, nothing belonging to your customers.
pip install scalekit-sdk-python
import scalekit.client
import os
from dotenv import load_dotenv
load_dotenv()
scalekit = scalekit.client.ScalekitClient(
client_id=os.getenv("SCALEKIT_CLIENT_ID"),
client_secret=os.getenv("SCALEKIT_CLIENT_SECRET"),
env_url=os.getenv("SCALEKIT_ENV_URL"),
)
actions = scalekit.actions
Connected Accounts
2 Per-User Auth: Creating connected accounts
Each user gets their own Connected Account, giving them a dedicated auth context. The identifier is any unique string from your system — a UUID, email, whatever you use internally.
response = actions.get_or_create_connected_account(
connection_name="slack",
identifier="user_slack_123" # your internal user ID
)
connected_account = response.connected_account
print(f"Status: {connected_account.status}")
# Status: PENDING — user hasn't authorized yet
This call is idempotent — safe to call on every session start. Returns the existing account if one already exists.
Authorization Flow
3 The authorization flow
The user authorizes your agent once through Slack's native OAuth consent screen. Scalekit generates the URL with correct scopes, PKCE challenge, and redirect handling pre-configured. After approval, you never see the token.
if connected_account.status != "ACTIVE":
link = actions.get_authorization_link(
connection_name="slack",
identifier="user_slack_123"
)
# Redirect user → Slack consent screen
# Scalekit captures the token on callback
return redirect(link.link)
Token management is automatic
After the user approves, Scalekit stores encrypted tokens and the connected account moves to ACTIVE. If token rotation is enabled on your Slack app, Scalekit handles the rotation cycle — including storing the new refresh token issued on each exchange. If a user revokes access from Slack's app settings, the account moves to REVOKED — no silent failures. Check account.status before critical operations.
Bring Your Own Credentials — required for Slack
Slack requires you to register your own app at api.slack.com/apps. Create the app, paste the Scalekit redirect URI into OAuth & Permissions → Redirect URLs, enable distribution under Manage Distribution, then copy your Client ID and Client Secret into the Scalekit dashboard. Token management stays fully handled.
Already have credentials?
Calling Slack
4 Calling Slack: What your agent writes
With the connected account active, your agent calls Slack tools using execute_tool. Name the tool, pass parameters. Scalekit handles token retrieval and request construction.
Send a message
Post to a channel or DM by channel ID or name. Supports plain text, Block Kit blocks, and thread replies via thread_ts.
result = actions.execute_tool(
identifier="user_slack_123",
tool_name="slack_send_message",
tool_input={
"channel": "#engineering",
"text": "Deployment to production completed. All health checks passing.",
}
)
# Returns the message timestamp (ts) — save it to update or reply later
Fetch channel history
Read recent messages from any channel the user has access to. Use oldest and latest to scope the time range.
result = actions.execute_tool(
identifier="user_slack_123",
tool_name="slack_fetch_conversation_history",
tool_input={
"channel": "C0123456789", # channel ID
"limit": 20,
"oldest": "1700000000" # Unix timestamp
}
)
# { "messages": [ { "type": "message", "text": "...", "ts": "..." }, ... ] }
Look up a user by email
Resolve a user's Slack ID from their email address — useful when your system has the email but needs the Slack user ID to @mention or DM them.
result = actions.execute_tool(
identifier="user_slack_123",
tool_name="slack_lookup_user_by_email",
tool_input={
"email": "jane.smith@acmecorp.com"
}
)
# { "user": { "id": "U0123456", "name": "jane.smith", "profile": {...} } }
Reply in a thread and add a reaction
Reply to an existing message by passing its ts as thread_ts, then acknowledge it with an emoji reaction.
# Reply in thread
reply = actions.execute_tool(
identifier="user_slack_123",
tool_name="slack_send_message",
tool_input={
"channel": "C0123456789",
"text": "On it — will update the ticket and loop in the on-call team.",
"thread_ts": "1700001234.000100" # parent message timestamp
}
)
# Acknowledge original message with a reaction
actions.execute_tool(
identifier="user_slack_123",
tool_name="slack_add_reaction",
tool_input={
"channel": "C0123456789",
"name": "eyes", # emoji name, no colons
"timestamp": "1700001234.000100"
}
)
Framework wiring
5 Wiring into your agent framework
Scalekit integrates directly with LangChain. The agent decides what to send or read; Scalekit handles auth on every invocation. No token plumbing in your agent logic.
from langchain_anthropic import ChatAnthropic
from langchain.agents import AgentExecutor, create_tool_calling_agent
from langchain_core.prompts import ChatPromptTemplate, MessagesPlaceholder
from scalekit.langchain import get_tools
slack_tools = get_tools(
connection_name="slack",
identifier="user_slack_123"
)
prompt = ChatPromptTemplate.from_messages([
("system", "You are a communication assistant. Use the available tools to help manage Slack messages and channels."),
MessagesPlaceholder("chat_history", optional=True),
("human", "{input}"),
MessagesPlaceholder("agent_scratchpad"),
])
agent = create_tool_calling_agent(ChatAnthropic(model="claude-sonnet-4-6"), slack_tools, prompt)
result = AgentExecutor(agent=agent, tools=slack_tools).invoke({
"input": "Find the last 10 messages in #incidents and summarize any open issues"
})
Other frameworks supported
Tool reference
All 18 Slack tools
Grouped by capability. Your agent calls tools by name — no API wrappers to write.
Send a message to a channel or DM. Supports plain text, Block Kit blocks, thread replies, and link unfurling options
Edit a previously sent message by channel ID and message timestamp
Permanently delete a message by channel and timestamp
Add an emoji reaction to any message — specify channel, emoji name (no colons), and message timestamp
Pin a message to a channel so it's easily accessible to all members
slack_fetch_conversation_history
Fetch messages from a channel or DM with time-range filtering and cursor-based pagination
slack_get_conversation_replies
Retrieve the full reply thread for a specific parent message by timestamp
List channels the user has access to. Filterable by type: public, private, MPIM, or DM
slack_get_conversation_info
Retrieve metadata for a channel — name, topic, purpose, member count, and settings
Create a new public or private channel by name
Join a public channel so the authenticated user becomes a member
Leave a channel — the user is removed and stops receiving messages
slack_invite_users_to_channel
Invite one or more users to a channel by passing a comma-separated list of user IDs
List all users in the workspace with profile information and pagination support
Retrieve detailed profile data for a specific user by ID
slack_lookup_user_by_email
Find a user's Slack ID from their email address — requires users:read.email scope
Check whether a user is currently active or away
Set the authenticated user's status text, emoji, and optional expiration timestamp
Connector notes
Slack-specific behavior
Bot token vs. user token — the distinction matters for your agent
Slack issues two separate tokens per installation: a bot token (xoxb-) acting as your app's identity, and a user token (xoxp-) acting as the specific human who authorized. Tools like slack_send_message will post from different identities depending on which token type is active. If your agent should appear to act on behalf of a specific user — not as a bot — ensure your Scalekit connection is configured with user token scopes (chat:write under User Token Scopes, not Bot Token Scopes) in your Slack app settings.
Scope granularity: private channels and DMs need separate scopes
channels:history and channels:read cover public channels only. Private channels require groups:history and groups:read. Multi-person DMs need mpim:history. Missing the right scope returns a missing_scope error at runtime — not an auth failure — which can be confusing to debug. Ensure the scopes configured in your Scalekit dashboard match the full set of channel types your agent needs to access.
Distribution must be enabled for multi-workspace apps
Slack restricts the standard OAuth install flow to apps that have enabled distribution under Manage Distribution in your app settings. Any app installed by users across multiple workspaces — the typical B2B SaaS case — must have distribution enabled before the Scalekit authorization link will work.
Infrastructure decision
Why not build this yourself
The Slack OAuth flow is documented. Token storage isn't technically hard. But here's what you're actually signing up for:
PROBLEM 01
Bot token vs. user token distinction — different identity, different scopes, different storage, and different handling for every API call where it matters
PROBLEM 02
Token rotation with rotating refresh tokens — new refresh token issued on every exchange, concurrent refresh attempts cause revocation, failures require full re-authorization
PROBLEM 03
Granular scope mismatches that produce missing_scope errors — not auth failures — at runtime, where the wrong scope set silently limits what your agent can see
PROBLEM 04
Revocation detection and multi-tenant token isolation — one user's Slack access must never interfere with another's, even across the same workspace
That's one connector. Your agent product will eventually need Salesforce, Gmail, HubSpot, Notion, and whatever else your customers ask for. Each has its own OAuth quirks and failure modes.
Scalekit maintains every connector. You maintain none of them.