Every team has that one Slack channel — the one where the same five questions get asked every single week. “Where’s the onboarding doc?” “What’s the refund policy?” “How do I reset my API key?” Your senior engineers are answering these instead of shipping. Your support lead is copy-pasting the same response for the third time today. With this n8n workflow, you’ll build an AI-powered Slack bot that reads incoming questions, searches your company knowledge base using semantic vector search, and posts precise answers back in the thread — automatically, in about 2 seconds.
Prefer to skip the setup? Grab the ready-made template → and be running in under 10 minutes.
What You’ll Build
- A Slack bot that listens for messages in any channel it’s invited to
- An OpenAI step that converts each incoming question into a 1,536-dimensional vector embedding
- A Pinecone semantic search that retrieves the most relevant chunks from your knowledge base
- A GPT-4o-mini step that reads the retrieved context and writes a clear, grounded answer
- An automatic thread reply in Slack so teammates get instant answers without leaving the channel
How It Works — The Big Picture
This workflow uses the RAG pattern — Retrieval-Augmented Generation. Instead of asking an AI to recall facts from its training data (which goes stale), you store your up-to-date company knowledge in Pinecone as vector embeddings. When a question arrives, the bot finds the semantically closest knowledge chunks and hands them to GPT-4o-mini as context. The result: factual, grounded answers drawn only from your approved content.
┌─────────────────────────────────────────────────────────────────────┐ │ SLACK KNOWLEDGE BASE BOT │ │ │ │ [Slack Trigger] │ │ ↓ │ │ [Filter Bot Messages] ──(bot message)──→ (stop — no loop) │ │ ↓ (user message) │ │ [Generate Question Embedding] (OpenAI text-embedding-3-small) │ │ ↓ 1,536-dim vector │ │ [Query Pinecone] (top-5 semantic matches from knowledge base) │ │ ↓ matched chunks + scores │ │ [Extract Context] (filter score > 0.7, join top 3 chunks) │ │ ↓ formatted context string │ │ [Generate AI Answer] (GPT-4o-mini + RAG prompt) │ │ ↓ natural language answer │ │ [Post Answer to Slack] (reply in the original thread) │ └─────────────────────────────────────────────────────────────────────┘
What You’ll Need
- n8n — self-hosted (v1.0+) or n8n Cloud
- Slack app with Events API enabled and a Bot User Token (starts with
xoxb-) - OpenAI account — API key with access to
text-embedding-3-smallandgpt-4o-mini - Pinecone account — free Starter plan is plenty; create an index with dimension 1536 and metric cosine
- A pre-populated Pinecone namespace called
knowledge-base(the Credentials Guide includes a Python ingestion script)
Estimated build time: 45–60 minutes from scratch, or under 10 minutes with the template.
Building the Bot — Step by Step
1 Slack Trigger
This node opens a webhook endpoint that Slack’s Events API calls every time a message is posted in a channel your bot belongs to. It’s the entry point for everything.
- Add a Slack Trigger node to your canvas.
- Select your Slack Bot Token credential (or create one — see the Credentials Guide).
- Set the Trigger to Message.
- Copy the Webhook URL n8n displays. Paste it into your Slack app’s Event Subscriptions → Request URL field.
- In Slack app settings, enable the
message.channelsandmessage.groupsevent scopes, then reinstall the app to your workspace.
A message event payload looks like this:
{
"type": "message",
"text": "What is our refund policy for annual subscriptions?",
"user": "U04ABCDEF12",
"channel": "C06XYZABC99",
"ts": "1743784201.000100",
"subtype": null
}
Tip: Slack sends events for bot messages too — including the bot’s own replies. Without the next filter node, every answer the bot posts would re-trigger the workflow and create an infinite loop.
2 Filter Bot Messages (IF)
This IF node stops the workflow from processing bot messages. It’s a one-condition check that routes user messages forward and drops everything else.
- Add an IF node connected to the Slack Trigger output.
- Set Value 1 to
={{ $json.subtype }}. - Condition: Is Not Equal To →
bot_message. - Connect the True output to Step 3. Leave False unconnected.
Tip: You can add a second condition here to limit the bot to a specific channel — filter $json.channel equals C06XYZABC99. This is useful if you want the bot active only in #ask-the-bot and not everywhere.
3 Generate Question Embedding (HTTP Request → OpenAI)
This node calls the OpenAI Embeddings API and converts the user’s question into a 1,536-dimensional vector — a list of numbers that captures the semantic meaning of the sentence. Pinecone will use this to find similar content.
- Add an HTTP Request node.
- Method:
POST| URL:https://api.openai.com/v1/embeddings - Authentication: Generic Credential Type → HTTP Header Auth. Create a credential with Name =
Authorizationand Value =Bearer YOUR_OPENAI_API_KEY. - Body Content Type: JSON. Paste this body:
{
"input": "={{ $('Slack Trigger').item.json.text }}",
"model": "text-embedding-3-small"
}
The response contains the embedding inside data[0].embedding:
{
"object": "list",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [0.0023, -0.0189, 0.0341, "...1,533 more values..."]
}
],
"model": "text-embedding-3-small",
"usage": { "prompt_tokens": 11, "total_tokens": 11 }
}
Tip: text-embedding-3-small costs $0.02 per million tokens. A team of 50 people asking 200 questions a day will spend about half a cent on embeddings. If you need higher search accuracy, switch to text-embedding-3-large (3,072 dimensions) — but update your Pinecone index dimension to match before doing so.
4 Query Pinecone (HTTP Request)
This node sends the question vector to Pinecone and gets back the five most semantically similar knowledge chunks, each scored between 0 (irrelevant) and 1 (identical).
- Add another HTTP Request node.
- Method:
POST| URL:https://YOUR_PINECONE_INDEX_HOST/query(replace with your index host from the Pinecone console — it looks likemy-index-abc123.svc.us-east-1.pinecone.io) - Add a header: Name =
Api-Key, Value =YOUR_PINECONE_API_KEY. - Body Content Type: JSON. Use this expression as the body:
{
"vector": "={{ $json.data[0].embedding }}",
"topK": 5,
"includeMetadata": true,
"namespace": "knowledge-base"
}
Pinecone responds with the top matches and their stored metadata:
{
"matches": [
{
"id": "doc-refund-annual-001",
"score": 0.921,
"metadata": {
"text": "Annual subscriptions may be refunded within 30 days of purchase for a full refund. After 30 days, refunds are prorated based on remaining months.",
"source": "help-center/billing",
"last_updated": "2026-03-01"
}
},
{
"id": "doc-refund-annual-002",
"score": 0.874,
"metadata": {
"text": "To request a refund, email billing@acme-corp.com with your order number and reason for cancellation. Refunds are processed within 5 business days.",
"source": "help-center/billing",
"last_updated": "2026-03-01"
}
}
],
"namespace": "knowledge-base"
}
Your Pinecone index must be pre-populated before the bot can answer anything. Each vector record needs a text field in its metadata. The Credentials Guide PDF bundled with the template includes a ready-to-run Python ingestion script that embeds and uploads your documents in minutes.
5 Extract Context (Code)
This JavaScript node processes the Pinecone results: filters low-confidence matches, takes the top 3 chunks, formats them into a numbered context string, and bundles the data for the next node.
const matches = $input.item.json.matches || [];
const slackData = $('Slack Trigger').item.json;
if (matches.length === 0) {
return [{
json: {
context: 'No relevant information found in the knowledge base.',
question: slackData.text,
channel: slackData.channel,
thread_ts: slackData.ts
}
}];
}
const context = matches
.filter(m => m.score > 0.7)
.slice(0, 3)
.map((m, i) => `[${i + 1}] ${m.metadata.text}`)
.join('\n\n');
return [{
json: {
context: context || 'No highly relevant information found.',
question: slackData.text,
channel: slackData.channel,
thread_ts: slackData.ts
}
}];
After this node, the data is clean and ready for the AI:
{
"context": "[1] Annual subscriptions may be refunded within 30 days...\n\n[2] To request a refund, email billing@acme-corp.com...",
"question": "What is our refund policy for annual subscriptions?",
"channel": "C06XYZABC99",
"thread_ts": "1743784201.000100"
}
Tip: The 0.7 score threshold is a good starting point. If the bot returns off-topic answers, raise it to 0.8. If it says “no information found” for questions you know are in the knowledge base, lower it to 0.65 or check that your Pinecone namespace name matches exactly.
6 Generate AI Answer (HTTP Request → OpenAI Chat)
This node sends the retrieved context and the original question to GPT-4o-mini. The system prompt instructs the model to answer strictly from the provided context — no hallucinating facts that aren’t in your knowledge base.
- Method:
POST| URL:https://api.openai.com/v1/chat/completions - Reuse your OpenAI HTTP Header Auth credential.
- Body (JSON):
{
"model": "gpt-4o-mini",
"messages": [
{
"role": "system",
"content": "You are a helpful company knowledge base assistant. Answer questions using ONLY the provided context. If the context does not contain the answer, say so clearly and suggest the user contact the team directly. Keep answers concise and actionable."
},
{
"role": "user",
"content": "Context:\n={{ $json.context }}\n\nQuestion: ={{ $json.question }}"
}
],
"max_tokens": 500,
"temperature": 0.2
}
Tip: temperature: 0.2 keeps answers factual and consistent. For a knowledge base bot you want determinism, not creativity. max_tokens: 500 keeps responses Slack-readable — roughly 3–5 paragraphs maximum.
7 Post Answer to Slack
The final node takes GPT-4o-mini’s answer and posts it as a thread reply — so the answer lives directly under the original question rather than flooding the main channel.
- Add a Slack node. Resource: Message, Operation: Post.
- Channel:
={{ $('Extract Context').item.json.channel }} - Text:
={{ $json.choices[0].message.content }} - Under Other Options, set Thread Timestamp to
={{ $('Extract Context').item.json.thread_ts }} - Use the same Slack credential as the Slack Trigger.
Tip: Want to brand the reply? Change the text field to: 🤖 *Knowledge Base Bot:* {{ $json.choices[0].message.content }}. The asterisks render as bold in Slack, making it clear this is an automated response.
The Data Flow
Here’s how a single question moves through all seven nodes, from Slack message to bot reply:
| Stage | Data Present | Key Field |
|---|---|---|
| After Slack Trigger | Raw Slack event payload | text, channel, ts |
| After Filter | Same payload, confirmed user message | subtype is null |
| After OpenAI Embeddings | 1,536-number float array | data[0].embedding |
| After Pinecone Query | Top 5 knowledge chunks + similarity scores | matches[].score, matches[].metadata.text |
| After Extract Context | Formatted context + original question | context, question, thread_ts |
| After OpenAI Chat | Full ChatGPT response object | choices[0].message.content |
| Posted to Slack | Plain text answer in thread | Visible to all channel members instantly |
Pinecone Knowledge Base Schema
Every vector record you upsert into Pinecone must follow this structure. The text metadata field is required — the Extract Context node reads it directly.
| Field | Type | Example | Description |
|---|---|---|---|
id |
String | doc-refund-001 |
Unique identifier for this knowledge chunk |
values |
Float[1536] | [0.023, -0.019, …] |
Embedding from text-embedding-3-small |
metadata.text |
String | "Annual subscriptions are refunded within 30 days…" |
The raw knowledge chunk — required |
metadata.source |
String | help-center/billing |
Where this content came from (optional) |
metadata.last_updated |
String | 2026-03-01 |
Last update date for freshness tracking (optional) |
Keep each knowledge chunk between 100 and 500 words. Too short and the chunk loses context; too long and the embedding gets diluted. One concept per chunk is a good rule of thumb — for example, one chunk for the refund policy, a separate chunk for the cancellation process.
Full System Flow
User posts question in Slack
│
▼
┌─────────────────────┐
│ Slack Trigger │ Receives message.channels event via webhook
└─────────────────────┘
│
▼
┌─────────────────────┐
│ Filter Bot Messages │──── subtype = "bot_message"? ──→ STOP
└─────────────────────┘
│ user message passes
▼
┌──────────────────────────┐
│ Generate Question │ POST https://api.openai.com/v1/embeddings
│ Embedding (OpenAI) │ model: text-embedding-3-small → 1,536-dim vector
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ Query Pinecone │ POST {index-host}/query
│ (vector search) │ topK=5 · namespace: knowledge-base
└──────────────────────────┘
│ top matches with metadata + scores
▼
┌──────────────────────────┐
│ Extract Context │ Filter: score > 0.7
│ (Code node) │ Join top 3 chunks into context string
└──────────────────────────┘
│
▼
┌──────────────────────────┐
│ Generate AI Answer │ POST https://api.openai.com/v1/chat/completions
│ (GPT-4o-mini) │ RAG prompt · temperature: 0.2 · max_tokens: 500
└──────────────────────────┘
│ grounded answer text
▼
┌──────────────────────────┐
│ Post Answer to Slack │ Thread reply on original message
└──────────────────────────┘
│
▼
Team member sees the answer in 2–3 seconds ✓
Testing Your Workflow
- Make sure the workflow is toggled to Active in n8n.
- Invite your bot to a test channel: type
/invite @YourBotName. - Post a question you know is covered in your knowledge base — for example: “What’s the refund policy for annual plans?”
- Within 2–3 seconds, a thread reply should appear with a grounded answer drawn from your Pinecone content.
- Post a question that’s definitely not in your knowledge base. The bot should say it doesn’t have that information and suggest contacting the team.
- Check the n8n Execution log to confirm all 7 nodes completed with green checkmarks and no errors.
| Problem | Likely Cause | Fix |
|---|---|---|
| Bot doesn’t respond at all | Workflow not Active, or Slack webhook URL mismatch | Toggle workflow to Active; verify webhook URL in Slack app settings matches n8n exactly |
| Bot replies to its own messages | Filter Bot Messages node misconfigured | Check the True output of the IF node connects to Step 3; False output should be unconnected |
| “No relevant information found” for everything | Pinecone index empty or wrong namespace | Run the ingestion script from the Credentials Guide; confirm namespace is exactly knowledge-base |
| Off-topic or wrong answers | Score threshold too low or chunks too large | Raise score threshold to 0.8 in the Extract Context node; re-chunk content into shorter segments |
| OpenAI 401 Unauthorized | API key missing or expired | Regenerate key at platform.openai.com and update the HTTP Header Auth credential in n8n |
| Slack “not_in_channel” error | Bot not invited to the channel | Run /invite @YourBotName in the channel before testing |
Frequently Asked Questions
Do I need to load content into Pinecone before the bot will work?
Yes — without content in Pinecone, every query returns empty results and the bot will say “no information found.” The template package includes a Python ingestion script (in the Credentials Guide PDF) that takes any plain text or Markdown file, splits it into chunks, generates embeddings, and uploads them to Pinecone. You can have a basic knowledge base loaded in 10–15 minutes.
How do I prevent the bot from answering questions it shouldn’t?
The system prompt in Step 6 instructs GPT-4o-mini to answer only from the provided context. If a question doesn’t match anything in Pinecone above the 0.7 threshold, the Extract Context node sends a “no information found” message as the context — and the AI is instructed to honestly say so and redirect to the team. You control what goes into Pinecone, so you control what the bot can answer.
How much does this cost to run per month?
For a team of 50 people asking roughly 200 questions a day: OpenAI embedding calls cost about $0.005/day, and GPT-4o-mini answers cost about $0.10/day. Pinecone’s free Starter plan handles up to 100,000 vectors — more than enough for a thorough company knowledge base. Total cost: roughly $3–4/month in API fees.
Can I restrict the bot to specific Slack channels?
Yes. In the Filter Bot Messages IF node (Step 2), add a second condition: $json.channel equals your target channel ID. The bot will only respond in that specific channel. You can find a channel’s ID by right-clicking it in Slack and selecting “Copy link” — the ID is the string starting with C at the end of the URL.
How do I keep the knowledge base current as our docs change?
Build a second n8n workflow that watches for document updates — a Google Drive trigger that fires when a doc is modified, re-embeds the content, and upserts it to Pinecone by the same id. Since Pinecone’s upsert operation overwrites by ID, you won’t accumulate duplicates. You can also just re-run the ingestion script manually after major documentation updates.
Does this work on n8n Cloud, or only self-hosted?
It works on both. The workflow uses only HTTP Request nodes, a Code node, the Slack Trigger, and the Slack node — all available in n8n Cloud and every self-hosted version from 1.0 onwards. No custom nodes or community packages are required.
🚀 Get the Slack Knowledge Base Bot Template
You now know exactly how this workflow is built. The template gets you there in under 10 minutes: it includes the ready-to-import workflow JSON, a Setup Guide PDF with step-by-step activation instructions, and a Credentials Guide PDF with a working Python ingestion script to load your knowledge base into Pinecone.
Instant download · Works on n8n Cloud and self-hosted
What’s Next?
- Add answer feedback: Let users react 👍 or 👎 on bot replies, log reactions to Airtable, and use quality scores to identify gaps in your knowledge base.
- Auto-ingest from Notion or Confluence: Build a companion n8n workflow that watches for document updates and automatically re-embeds modified pages into Pinecone.
- Add a Slack slash command: Create a
/askcommand so users can query the bot privately in DMs without cluttering a shared channel. - Multi-namespace routing: Create separate Pinecone namespaces for HR, Engineering, and Sales — and route questions to the right namespace based on which Slack channel they came from.