I spent a week building a Slack bot that turns conversations into a searchable knowledge base. The AI part (detecting Q&A, extracting answers) took about two days. The Slack integration part? Four days. Here's why, and how to avoid the same traps.
The idea
Slack is where most team knowledge lives, but it's terrible at retrieval. You remember someone answered a question six months ago, but finding it means scrolling through thousands of messages. I wanted a bot that would:
- Listen for Q&A patterns in channels
- Extract the question and the best answer
- Store them in a searchable knowledge base
- Let you query it with a slash command:
/slacksays "how do we handle refunds?"
Day 1-2: The Slack OAuth flow (the hard part)
Slack OAuth 2.0 has a specific dance for lack of better words:
- User clicks "Add to Slack" on your website
- They're redirected to Slack's authorization page
- After approving, Slack redirects back to your callback URL with a temporary
code - Your server exchanges the code for a permanent access token
- You now have a token scoped to that workspace
The tricky part is step 4 , the token exchange has to happen server-side, and you need to handle token refresh, workspace-unique tokens, and the fact that each Slack workspace is its own isolated auth.
# Flask OAuth callback
@app.route("/slack/oauth")
def slack_oauth():
code = request.args.get("code")
# Exchange code for token
resp = requests.post("https://slack.com/api/oauth.v2.access", data={
"client_id": SLACK_CLIENT_ID,
"client_secret": SLACK_CLIENT_SECRET,
"code": code,
"redirect_uri": "https://slacksays.com/slack/oauth"
})
token_data = resp.json()
access_token = token_data["access_token"]
team_id = token_data["team"]["id"]
# Store per-workspace
db.save_token(team_id, access_token)
return redirect(f"/dashboard?team={team_id}")
Day 3: Event subscriptions
Slack uses a challenge-response for event URL verification. When you set up event subscriptions, Slack sends a POST with a challenge parameter , your endpoint must return it exactly to prove you control the URL.
@app.route("/slack/events", methods=["POST"])
def slack_events():
data = request.json
# URL verification challenge
if data.get("type") == "url_verification":
return jsonify({"challenge": data["challenge"]})
# Handle real events
if data.get("event", {}).get("type") == "message":
handle_message(data["event"])
return jsonify({"ok": True})
Day 4: Q&A detection logic
This is where the actual product logic lives. I keep it simple , look for messages that end with a question mark, then find the first reply that looks like an answer (longer than 50 chars, from a different user).
def detect_qa_pair(channel_messages: list) -> dict | None:
"""Find the most recent Q&A pair in a channel."""
for i, msg in enumerate(channel_messages):
if msg["text"].strip().endswith("?") and len(msg["text"]) > 20:
# Look for an answer in the next few messages
for j in range(i + 1, min(i + 5, len(channel_messages))):
reply = channel_messages[j]
if reply["user"] != msg["user"] and len(reply["text"]) > 50:
return {
"question": msg["text"],
"answer": reply["text"],
"channel": msg["channel"],
"timestamp": msg["ts"]
}
return None
Day 5: Slash command + web dashboard
The /slacksays slash command triggers a search across stored Q&A pairs:
@app.route("/slack/commands", methods=["POST"])
def slash_command():
query = request.form.get("text", "")
results = search_knowledge_base(query)
if not results:
return jsonify({"text": "No answers found. Try rephrasing your question."})
blocks = []
for r in results[:3]:
blocks.append({
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"*Q:* {r['question']}\n*A:* {r['answer'][:300]}"
}
})
return jsonify({"blocks": blocks})
What I'd do differently
The OAuth flow is unforgiving , one misconfigured redirect URL and nothing works. If I were starting fresh, I'd use a framework that handles the Slack OAuth dance out of the box. Flask is great, but wiring Slack auth manually took two full days I could've spent on product.
The bot itself has been running for a few weeks now at slacksays.com. Teams seem to love it , turns out everyone has the same "where did we discuss X?" problem.
*I packaged the Slack bot boilerplate (Flask, OAuth flow, slash commands, event listeners) into a starter kit: [Slack Bot Starter on Gumroad if anyone wanna take a look.
Top comments (0)