TL;DR:
- Slack Bolt SDK (Python or Node.js) is the standard starting point — it handles OAuth, event subscriptions, and slash commands with minimal boilerplate
- The main design question isn’t “how do I add AI?” but “where does the agent fit in the workflow?” — agents that interrupt vs. agents that respond on request behave very differently
- The biggest operational pitfall is context management: Slack threads are your memory system, and handling them poorly produces agents that confuse users
Slack has quietly become one of the more interesting deployment targets for AI agents. It’s where people already spend their time, it has a rich event system, and it provides a natural conversational interface without requiring a custom UI. But building a good Slack agent is more than calling an LLM from a slash command.
Here’s what actually matters when you’re building one.
Start with the Bolt SDK
Slack’s Bolt SDK exists in Python and Node.js flavours and is the official, maintained way to build Slack apps. It abstracts over OAuth token handling, request signature verification, event subscriptions, and interactive component callbacks. If you’re not using Bolt, you’re writing a lot of that from scratch.
A minimal Bolt app that responds to a slash command looks like this (Node.js):
const { App } = require('@slack/bolt');
const app = new App({
token: process.env.SLACK_BOT_TOKEN,
signingSecret: process.env.SLACK_SIGNING_SECRET,
});
app.command('/ask', async ({ command, ack, say }) => {
await ack();
const response = await callYourLLM(command.text);
await say(response);
});
(async () => { await app.start(3000); })();
That’s genuinely most of the plumbing. The interesting work is everything around the LLM call.
Event-Driven vs. Request-Driven Agents
The single most important design decision is whether your agent listens to events (messages, reactions, channel activity) or responds only to explicit commands (slash commands, mentions, button clicks).
Request-driven agents are easier to build and more predictable. Users invoke them deliberately — /summarise this thread, @agentname help me draft a response — and the agent responds. The interaction model is clear. Users know when they’re talking to the agent.
Event-driven agents monitor Slack events and act proactively — summarising a channel at the end of the day, flagging urgent messages, posting alerts from connected systems. These are more powerful but require much more care. Agents that post unsolicited into channels are frequently annoying. Agents that act on behalf of users without clear boundaries erode trust.
For most teams building their first Slack agent, start request-driven and only add event-driven behaviour when you have a concrete, well-understood use case for it.
Context Management: Threads as Memory
The biggest practical challenge in Slack agents is context. LLMs need conversation history to give useful responses in a multi-turn interaction, and Slack’s data model doesn’t automatically give you that.
The correct approach is to use threads as your conversation scope. When a user starts a conversation with your agent, keep all follow-up messages in the same thread and retrieve the thread history before each LLM call:
@app.event("message")
async def handle_message(event, client, say):
thread_ts = event.get("thread_ts") or event["ts"]
# Retrieve thread history for context
history = await client.conversations_replies(
channel=event["channel"],
ts=thread_ts
)
messages = format_thread_for_llm(history["messages"])
response = await call_llm_with_history(messages, event["text"])
await say(text=response, thread_ts=thread_ts)
Without this, every message your agent receives is contextless. Users quickly hit the wall of “why doesn’t it remember what I just told it?”
Handling the 3-Second Acknowledgement Requirement
Slack requires your app to acknowledge incoming events within 3 seconds or it will retry (and eventually consider your endpoint unreachable). LLM calls almost never complete in under 3 seconds.
The standard pattern is to acknowledge immediately, then send the actual response asynchronously:
@app.command("/ask")
async def handle_ask(ack, command, client):
await ack() # Acknowledge immediately
# Show a "thinking" indicator
await client.chat_postMessage(
channel=command["channel_id"],
text="Thinking...",
thread_ts=command.get("thread_ts")
)
# Now do the actual work
response = await call_llm(command["text"])
# Update or reply with the result
await client.chat_postMessage(
channel=command["channel_id"],
text=response,
thread_ts=command.get("thread_ts")
)
Alternatively, use Slack’s response_url for delayed responses — it’s designed exactly for this pattern.
Agentic Patterns: Tool Use in Slack
If your agent needs to take actions beyond generating text — looking up data, creating tickets, scheduling things, checking a calendar — you’re into tool-calling territory.
The practical Slack-specific concern here is transparency. When an agent takes an action (creating a Jira ticket, sending an email, scheduling a meeting) from a Slack conversation, users need to know what it did. Silent side effects in a chat interface are deeply confusing.
Build confirmation steps into any action that has external consequences:
User: "Schedule a meeting with the design team for Thursday"
Agent: "I'll schedule a 30-minute meeting with the design team (3 members)
at 2pm Thursday July 17. Their calendars look free at that slot.
Shall I go ahead? [Confirm] [Pick a different time]"
Slack Block Kit’s interactive components (buttons, dropdowns) are the right way to implement this. A text-based “type yes to confirm” pattern is clunky and breaks easily.
Distribution and Permissions
If you’re building for a single workspace, distribute the app via a direct install link from the Slack API dashboard. If you’re building for multiple workspaces (a SaaS product, or company-wide distribution in a large enterprise), you need the full OAuth flow and a database to store per-workspace tokens.
The permissions scope determines what your bot can see and do. Be minimal — request only what you actually use. Users and admins are more likely to approve a bot with channels:history and chat:write than one that requests access to DMs and file uploads without obvious reason.
One Common Mistake to Avoid
Don’t make your agent respond to every mention of its name in a channel. This creates noise quickly. The better pattern is for the agent to respond only in threads where it’s been explicitly invoked, and to stay quiet when it hasn’t. Trust takes time to build in a team Slack environment — an agent that feels like it’s always watching does the opposite.
Start small, prove value in a narrow use case, and expand based on what users actually find useful.