Integrating Slack with Your AI Company Brain for Reliable Knowledge Retrieval

If you need a Slack bot that answers from company documents, install an existing one unless the bot has to act inside systems you own. The model call is the easy part. The work that lasts is acknowledging Slack events within three seconds, mapping each Slack user to their document permissions, and deciding who gets to read the reply. We recommend filtering by who can read the answer, not only by who asked, because a correct answer posted in a channel is read by everyone in it. If you do build it, the steps follow.
Build it or install one
Build when the assistant has to call your own internal systems, when documents cannot leave your infrastructure, or when an engineer will own event handling, identity mapping and on-call for it. Install when the job is to answer from your documents with the asker's own access, which replaces the teammate who leaves the thread, finds the page and pastes a link. Whichever you choose, check five things:
- It answers with the asker's access, never through a shared service account.
- Every answer links the page it came from.
- It replies privately when a cited source is restricted.
- It says it cannot find an answer instead of guessing, and never reveals that a restricted page exists.
- If you build it, it acknowledges Slack within three seconds and survives retries.
The last section describes the install option.
How the pieces fit
A working integration is more than a webhook. Slack sends an event to your backend. The backend verifies it, acknowledges it and queues it. A worker maps the Slack user to an internal identity, retrieves only what that identity may read, asks the model to answer from those passages with citations, and posts the reply. Slack signs every request it sends with an X-Slack-Signature header, and the Slack API documentation has the Events API and signing details.
Subscribe to app_mention events and direct messages. The routing layer reads the payload, extracts the channel ID, thread timestamp and text, decides whether the user is asking a question, asking for a thread summary or running a command, and queues that metadata so the backend scales independently of Slack.
Permissions are the critical part. If someone asks for payroll information, retrieve only documents that person may open. A universal service account that indexes and retrieves everything would expose HR, finance and executive documents to anyone who can mention the bot. Pass the Slack user ID through and map it to your identity provider: in Google Cloud's IAM overview, access is granted to principals, which are identities that have authenticated, through roles on resources, and a Slack user ID is not a principal in any of those systems until you map it. Tag every embedding with the access control list of its source document, and filter on the mapped identity before the semantic search runs, not after.
Acknowledge within three seconds, and dedupe retries
Slack expects an HTTP 2xx within three seconds of each event delivery. Without one it retries three times, almost immediately, after one minute and after five minutes. Each retry carries an x-slack-retry-num header, and x-slack-retry-reason says http_timeout when you were too slow. Slack also advises responding as soon as you can and not processing the event in the same process. If more than 95 percent of deliveries in an hour fail, Slack can temporarily disable your event subscriptions, although apps that receive fewer than 1,000 events an hour are exempt. Retrieval and generation rarely fit in three seconds, so the handler only verifies, dedupes, queues and returns:
def handle_event(request):
event_data = request.get_json()
if event_data.get('type') == 'url_verification':
return jsonify({'challenge': event_data['challenge']})
# Slack re-sends a late or failed delivery with the same event_id
first_time = redis_client.set(f"slack-event:{event_data['event_id']}", 1, nx=True, ex=600)
if first_time:
process_slack_message.delay(event_data) # background worker
return ('', 200)Two habits follow. Dedupe on event_id before you queue, with an entry that outlives the five-minute retry window (24 hours if you turn on Slack's delayed events), so a delivery that failed after you queued the work cannot produce a second answer. And count requests that carry x-slack-retry-num. A handler that acknowledges before it works should see almost none, so any retry means the acknowledgement path, which is the signature check, the dedupe write and the queue write, is slow or failing, and you will see it in that count before users complain. You can post a temporary "Searching the knowledge base..." message and replace it later with chat.update.
Build it in four steps
Step 1: Create the Slack app
Create a new app in the Slack API portal and give the bot token these scopes:
app_mentions:read: lets the bot see when it is mentioned.chat:write: lets the bot send messages, including the ephemeral ones used below.channels:history: lets the bot read thread context in public channels it has been added to. Private channels and direct messages usegroups:historyandim:history. The scope covers everything in those channels, so callconversations.repliesonly when the bot is mentioned and read only that thread.
Add links:read only if you also unfurl links. It belongs to the link_shared event, and a question-answering bot does not need it. Enable Socket Mode if you are testing locally, or configure a public Request URL for production. Install the app to your workspace, store the Bot User OAuth Token in a secret manager and inject it at runtime. Never commit it to version control.
Step 2: Verify every request
Reject any request that does not carry a valid signature, or an attacker can pretend to be Slack and pull internal knowledge out of your backend. Slack's guide to verifying requests hashes the raw body, not the parsed JSON, together with the timestamp and your signing secret, and tells you to ignore requests whose timestamp is more than five minutes off, since they may be replays. In Flask, call request.get_data() before anything parses the body.
import hashlib
import hmac
import time
def verify_slack_request(headers, raw_body, signing_secret):
# raw_body: the unparsed request body, as a string
timestamp = headers.get('X-Slack-Request-Timestamp')
if abs(time.time() - float(timestamp)) > 60 * 5:
return False
sig_basestring = f"v0:{timestamp}:{raw_body}"
my_signature = 'v0=' + hmac.new(
signing_secret.encode(),
sig_basestring.encode(),
hashlib.sha256
).hexdigest()
slack_signature = headers.get('X-Slack-Signature')
return hmac.compare_digest(my_signature, slack_signature)Run the handler behind an API gateway or reverse proxy that terminates TLS, and log request latency and payload size.
Step 3: Connect retrieval
Once a request is verified, extract the user ID and the text, map the user to the internal identity, run a vector search filtered by that user's access, and build a prompt from the top chunks. The prompt must tell the model to answer only from the context and to cite sources:
def build_prompt(user_query, retrieved_chunks):
context = "\n\n".join([chunk.text for chunk in retrieved_chunks])
return f"""
Answer the user's question using ONLY the provided context.
If the context does not contain the answer, say "I cannot find the answer in the provided documents."
Cite the source URL for every factual claim.
Context:
{context}
Question:
{user_query}
"""The embedding model turns the query into a vector, and the database ranks document vectors by cosine similarity. Set a similarity threshold so that a question outside the knowledge base retrieves nothing instead of the nearest irrelevant page.
Step 4: Reply with Block Kit
Build the message with Block Kit: the answer in a section block and the source links in a context block. Post it to the thread with chat.postMessage, passing the question's thread_ts. Put the answer itself in the top-level text field as well. Slack uses it in notifications, and screen readers read it and skip the blocks.
{
"channel": "C1234567890",
"thread_ts": "1712345678.000100",
"text": "The standard onboarding laptop is the MacBook Pro 14-inch (M3 Pro). You can request an upgrade through the IT portal.",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "Here is the information you requested based on internal documentation:"
}
},
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "The standard onboarding laptop is the MacBook Pro 14-inch (M3 Pro). You can request an upgrade through the IT portal."
}
},
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "Source: <https://intranet.example.com/it/hardware|IT Hardware Policy>"
}
]
}
]
}Add buttons that let people rate the answer, and send those ratings to whoever owns the page.
Filter by who can read the reply, not only who asked
Filtering retrieval protects the documents. It does not protect the answer once it is posted. A thread reply is visible to every member of the channel, so an answer built from a page that only the asker may open, such as a salary band or a customer contract, is now readable by people who cannot open that page. We recommend a simple rule: if any cited source is restricted, reply privately, and otherwise reply in the thread.
chat.postEphemeral shows a message only to the asker and needs only the chat:write scope, but it does not persist: Slack says delivery is not guaranteed, because the user must be active in Slack and in the channel, and the message disappears with the session. Use an ephemeral reply for a quick restricted lookup and a direct message with the source link when the person needs to keep the answer.
When a user asks for something they cannot open, retrieval returns nothing. Treat that as missing knowledge and reply "I can't find that in the sources available to you". Do not confirm that a restricted page exists, quote its title or give a partial summary. Titles and filenames also leak through autocomplete suggestions and error messages.
When there is nothing to answer
If retrieval finds nothing relevant, the model must not invent an answer, so the prompt tells it to say it cannot find one. The integration should detect that state and offer a next step, such as alternative search terms or a button that sends the question to a human in a support channel. A bot that fails silently gets abandoned.
Test it before everyone has it
Create a Slack user with restricted permissions and ask for confidential documents. The reply should be the generic failure, and the backend logs should show the identity provider rejecting the claim. Then ask a set of known questions and measure how often the right source is retrieved. Chunks that are too large dilute the answer and chunks that are too small leave the model without context, so adjust the chunk size if the right pages keep missing. Once it is live, track how often people mention the bot, the ratio of answers to fallbacks and the latency. A topic that often falls back usually has a missing or poorly written page.
If you cache frequent answers, such as the holiday schedule, in Redis or Memcached, put the permission scope in the cache key so an answer built for one audience is never served to another.
Start small, or skip the build
Start with a private channel, one well-maintained set of documents and the restricted test user, and run the identity test before you add more documents.
An AI agent that works inside your Slack threads
If you would rather not build the integration described above, Kipwise Agent is that layer, already built. It works in the thread, as you, using your own access rather than a shared service account.
- “Do we have a doc about this?”: it searches your knowledge base, reads the page, and answers with the relevant part and a link to the source.
- “Move this deal to Negotiation”: it updates the deal or the ticket in your CRM and logs the note, as you.
- “A customer is asking about X”: it drafts the answer from your own documentation, with customer-visible replies limited to approved team members.
- “How are we doing on search and traffic?”: it reports only figures it actually fetched, never a guess.
Access is the strict boundary this article argues for: it only ever sees the pages you can see, connections are made per tool with your own permissions, and your workspace can turn any lookup off. Everything it writes in Kipwise is attributed to you.



-p-1080.jpg)