Build a Free Discord FAQ Bot with RAG Using n8n, Pinecone & Gemini
Deploy a production-grade Discord FAQ bot that performs Retrieval-Augmented Generation over your docs. Step-by-step guide using free-tier tools: n8n, Pinecone, and Gemini API.
By FDE Coach Editorial11 min read
On this page
What We're Building & Feature List
We're building a Discord bot that acts as a first-line support engineer. A user drops a question in a designated channel, and the bot searches your documentation, summarizes an answer with citations, and posts it back—no humans on call required.
Feature list:
- Listens for messages in a specific Discord channel (ignores its own messages to prevent loops).
- Converts questions into vector embeddings using Google Gemini's free
text-embedding-004model. - Queries a Pinecone vector index for the most relevant documentation chunks.
- Feeds retrieved chunks + user question into Gemini 1.5 Flash for a concise, sourced answer.
- Posts the answer back to Discord with source links.
- Runs entirely on free tiers (n8n self-hosted, Pinecone Starter, Gemini free quota).
High-Level Architecture
The flow is linear but powerful: Discord hits an n8n webhook, we strip noise, embed the query, retrieve context from Pinecone, prompt Gemini with that context, and fire the answer back to Discord. No GPU, no managed services beyond the free tiers.
Prerequisites: Free-Tier Tools Setup
Before writing a single node, get these accounts and keys:
| Tool | What You Need | Free Tier Limit | Link |
|---|---|---|---|
| n8n | Self-hosted instance (Docker or npm) | Unlimited workflows | n8n.io/download (opens in a new tab) |
| Pinecone | API key + index | 1 free pod index (10K vectors) | pinecone.io (opens in a new tab) |
| Google AI Studio | Gemini API key | 1,500 requests/day (free) | aistudio.google.com (opens in a new tab) |
| Discord Developer Portal | Bot token + channel ID | Unlimited | discord.com/developers (opens in a new tab) |
n8n setup (Docker, 30 seconds):
bash
docker run -d --name n8n -p 5678:5678 \
-v n8n_data:/home/node/.n8n \
n8nio/n8n
Open http://localhost:5678, create an owner account, and you're in.
Pinecone index creation:
- Create a new index with dimension
768(matches Geminitext-embedding-004). - Metric:
cosine. Pod type:starter. - Note your index host URL (e.g.,
https://your-index-abc.svc.pinecone.io).
Discord bot setup:
- Create an application at Discord Developer Portal.
- Under Bot, create a bot token and enable
MESSAGE CONTENT INTENT. - Invite the bot to your server with
botandapplications.commandsscopes. Permission integer:2147485696(Send Messages, Read Message History, Mention Everyone). - Enable Developer Mode in Discord, right-click your support channel, and copy its ID.
Step 1: Ingesting Your Docs into Pinecone
Your bot is useless without a knowledge base. We'll build an n8n workflow that scrapes your docs, chunks them, embeds them, and upserts into Pinecone. Run this once (or on a schedule if docs change).
Workflow: "Doc Ingest"
- Manual Trigger (or Cron node for weekly refreshes).
- HTTP Request node – fetch your doc sitemap or a list of URLs. If you have a static site, point it at your sitemap.xml.
- HTML Extract node – grab clean text from each page. Set
Extraction Valuestotextandselectortobody. - Code node (chunking) – split text into overlapping chunks of ~500 tokens. Here's the JavaScript:
javascript
const chunks = [];
const text = $input.first().json.text;
const chunkSize = 500;
const overlap = 50;
const words = text.split(/\s+/);
for (let i = 0; i < words.length; i += chunkSize - overlap) {
const chunk = words.slice(i, i + chunkSize).join(' ');
chunks.push({
text: chunk,
source: $input.first().json.url,
chunkIndex: Math.floor(i / (chunkSize - overlap))
});
}
return chunks;
- Loop Over Items node – iterate each chunk.
- HTTP Request node (embed) – call Gemini embedding API:
- Method: POST
- URL:
https://generativelanguage.googleapis.com/v1beta/models/text-embedding-004:embedContent?key={{$env.GEMINI_API_KEY}} - Body (JSON):
json
{ "model": "models/text-embedding-004", "content": { "parts": [{ "text": "{{$json.text}}" }] } } - Code node (format for Pinecone) – map the response:
javascript
const embedding = $input.first().json.embedding.values;
return {
id: `${$input.first().json.source}#${$input.first().json.chunkIndex}`,
values: embedding,
metadata: {
text: $input.first().json.text,
source: $input.first().json.source
}
};
- Pinecone Vector Upsert node – configure with your API key and index host. Set
Vector Dimensionto 768. Mapid,values, andmetadatafrom the previous step.
Run it. You should see vectors populate in the Pinecone console.
Want a deeper dive on doc-backed agents? Check out our guide on building a WhatsApp Support Agent Backed by Your Docs Using n8n and Supabase—the ingestion pattern is nearly identical.
Step 2: Building the Discord Webhook Listener
Create a new n8n workflow called "Discord FAQ Bot". The entry point is a Webhook node.
- Webhook node – set
HTTP MethodtoPOST, copy the generated URL (e.g.,http://localhost:5678/webhook/discord-faq). - Discord Developer Portal – under your application, create a webhook for the target channel, or we'll use the bot's HTTP interaction endpoint. For simplicity, we'll use a Discord bot library pattern via n8n's HTTP Request nodes.
Alternative: Use n8n's Discord Trigger node (requires a paid n8n cloud tier for webhook triggers, but self-hosted is free). We'll use the raw HTTP approach for maximum control.
Discord Gateway caveat: Discord bots use WebSocket connections, not simple webhooks. To bridge this, we'll run a tiny Node.js shim that forwards Discord messages to n8n's webhook. This keeps n8n stateless and free.
Create a file discord-shim.js:
javascript
const { Client, GatewayIntentBits } = require('discord.js');
const axios = require('axios');
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
]
});
const N8N_WEBHOOK_URL = 'http://localhost:5678/webhook/discord-faq';
const TARGET_CHANNEL_ID = 'YOUR_CHANNEL_ID';
client.on('messageCreate', async (message) => {
if (message.author.bot) return;
if (message.channel.id !== TARGET_CHANNEL_ID) return;
await axios.post(N8N_WEBHOOK_URL, {
content: message.content,
author: message.author.username,
channelId: message.channel.id,
messageId: message.id
});
});
client.login('YOUR_DISCORD_BOT_TOKEN');
Run it: node discord-shim.js. Now every message in your support channel hits n8n.
Step 3: The RAG Pipeline (Embed, Retrieve, Generate)
Back in n8n, after the Webhook node:
Node 2: Filter & Sanitize (Code node)
javascript
const msg = $input.first().json;
// Optional: ignore short messages, commands, etc.
if (!msg.content || msg.content.length < 10) return [];
return [{ query: msg.content.trim(), author: msg.author, channelId: msg.channelId, messageId: msg.messageId }];
Node 3: Generate Query Embedding (HTTP Request)
Same Gemini embedding endpoint as ingestion, but pass msg.query:
json
{
"model": "models/text-embedding-004",
"content": { "parts": [{ "text": "{{$json.query}}" }] }
}
Node 4: Pinecone Vector Query Use the Pinecone node or raw HTTP:
- Method: POST
- URL:
https://your-index-abc.svc.pinecone.io/query - Headers:
Api-Key: {{$env.PINECONE_API_KEY}},Content-Type: application/json - Body:
json
{
"vector": {{ JSON.stringify($input.first().json.embedding.values) }},
"topK": 5,
"includeMetadata": true
}
Node 5: Build RAG Prompt (Code node)
javascript
const matches = $input.first().json.matches || [];
const context = matches
.filter(m => m.score > 0.7)
.map(m => `SOURCE: ${m.metadata.source}\nTEXT: ${m.metadata.text}`)
.join('\n\n');
const query = $input.first().json.query; // carry forward from earlier node
return [{
query,
context: context || "No relevant documentation found.",
sources: matches.map(m => m.metadata.source),
channelId: $input.first().json.channelId,
messageId: $input.first().json.messageId
}];
Node 6: Gemini Chat Completion (HTTP Request)
- URL:
https://generativelanguage.googleapis.com/v1beta/models/gemini-1.5-flash:generateContent?key={{$env.GEMINI_API_KEY}} - Body:
json
{
"contents": [{
"parts": [{
"text": "You are a helpful support assistant. Answer the user's question using ONLY the provided context. Cite sources inline like [1]. If the context doesn't contain the answer, say so politely.\n\nContext:\n{{$json.context}}\n\nUser Question: {{$json.query}}\n\nAnswer:"
}]
}],
"generationConfig": { "temperature": 0.3, "maxOutputTokens": 500 }
}
Node 7: Extract Answer (Code node)
javascript
const answer = $input.first().json.candidates[0].content.parts[0].text;
return [{ answer, sources: $input.first().json.sources, channelId: $input.first().json.channelId, messageId: $input.first().json.messageId }];
Step 4: Wiring the Reply Back to Discord
The shim script that forwards messages to n8n needs a way to get the answer back. We have two clean patterns:
Option A: n8n calls Discord API directly (recommended) Add a final HTTP Request node:
- Method: POST
- URL:
https://discord.com/api/v10/channels/{{$json.channelId}}/messages - Headers:
Authorization: Bot YOUR_DISCORD_BOT_TOKEN,Content-Type: application/json - Body:
json
{
"content": "**Answer:** {{$json.answer}}\n\n📚 Sources: {{$json.sources.join(', ')}}"
}
This eliminates the shim's return path complexity. The shim only does one-way forwarding.
Option B: Shim polls n8n (not recommended for latency) Have the shim store a message ID and poll an n8n webhook for results. Adds latency and state. Skip this.
Reply formatting polish (Code node before Discord HTTP Request):
javascript
const answer = $input.first().json.answer;
const sources = [...new Set($input.first().json.sources)]; // dedupe
const sourceLinks = sources.map((s, i) => `[${i+1}] ${s}`).join('\n');
return [{
content: `🤖 **Here's what the docs say:**\n\n${answer}\n\n📚 **Sources:**\n${sourceLinks}`,
channelId: $input.first().json.channelId
}];
How to Run It Locally
- Start n8n:
docker run -d --name n8n -p 5678:5678 -v n8n_data:/home/node/.n8n n8nio/n8n - Set environment variables in n8n (Settings → Environment Variables):
GEMINI_API_KEYPINECONE_API_KEYDISCORD_BOT_TOKEN
- Import both workflows (Doc Ingest + Discord FAQ Bot) via n8n's import feature.
- Run Doc Ingest once to populate Pinecone.
- Activate Discord FAQ Bot workflow (toggle to Active).
- Run the shim:
node discord-shim.js(keep it alive withpm2orscreen). - Test: Type a question in your Discord support channel.
For production, wrap the shim in a systemd service or Docker container so it survives reboots.
This pattern of wiring external triggers to n8n webhooks is a core FDE skill. If you want to master rapid prototyping like this, check out our breakdown of What a Forward Deployed Engineer Actually Does in a Week.
Sensible Extensions
Once the core loop works, here's where to take it:
- Threaded conversations: Reply in-thread if the user's message is in a thread. The Discord API
message_referencefield handles this—pass it through the shim. - Feedback loop: Add 👍/👎 reactions to answers. Store low-rated answers + correct answers in a dataset for fine-tuning or prompt improvement.
- HyDE (Hypothetical Document Embeddings): Before embedding the user query, ask Gemini to generate a hypothetical perfect answer, then embed that. This often improves retrieval quality for short or vague queries.
- Multi-source RAG: If you have docs + a knowledge base + past resolved tickets, create separate Pinecone namespaces and merge results.
- Rate limiting: Add a simple Redis or in-memory counter in the shim to prevent abuse.
- Slack/Teams adapter: The n8n workflow is channel-agnostic. Swap the Discord shim for a Slack Events API handler and you've got multi-platform support.
If you're thinking about running the embedding model locally to avoid API latency, read our piece on GLM-5.3 Goes Open-Weight for context on what local MoE models can do.
Common Pitfalls & Debugging
| Pitfall | Symptom | Fix |
|---|---|---|
| Embedding dimension mismatch | Pinecone upsert fails with "dimension 768 expected, got X" | Confirm you're using text-embedding-004 (768d). Older Gecko models use 768 but check the model name. |
| Bot ignores messages | No webhook hits in n8n | Verify MESSAGE CONTENT INTENT is enabled in Discord Developer Portal. Without it, bots can't read message content. |
| Pinecone returns empty matches | Answer says "No relevant documentation" | Lower the score threshold in the Code node (try 0.5). Your chunks may be too granular or docs too sparse. |
| Gemini rate limits | 429 errors | Free tier is 1,500 requests/day. Add a Wait node (5s) between embedding and chat calls. Batch queries if traffic spikes. |
| n8n webhook not reachable | Discord shim logs ECONNREFUSED | If using Docker, ensure the shim uses host.docker.internal instead of localhost to reach the host machine's n8n port. |
| Duplicate answers | Bot replies multiple times | The filter node must check message.author.bot on the Discord side. Also ensure n8n's Webhook node has "Respond to Webhook" set to "No Response" to avoid retries. |
| Context window overflow | Gemini returns truncated or garbled answers | Reduce topK to 3 or decrease chunk size during ingestion. Gemini 1.5 Flash has a 1M token context, but keeping prompts tight improves quality. |
FAQ
Q: Why not use n8n's Discord node? The n8n Discord node requires a paid license for the trigger functionality on self-hosted. Our shim approach costs nothing and gives you full control over the message lifecycle.
Q: Can I use OpenAI instead of Gemini?
Yes. Swap the embedding endpoint to text-embedding-3-small (1536d, so update Pinecone index dimension) and the chat endpoint to gpt-3.5-turbo. But you'll burn through free credits fast—Gemini's free tier is far more generous.
Q: How do I handle multiple Discord servers?
Pass the guildId through the shim. In Pinecone, use namespaces per server to keep knowledge bases isolated. The workflow structure stays identical.
Q: What if my docs change frequently?
Set the Doc Ingest workflow's trigger to a Cron node (e.g., nightly). Use Pinecone's upsert with the same IDs—it'll overwrite stale vectors. Consider deleting vectors for removed pages by comparing sitemap snapshots.
Q: Is this production-ready? For internal team use, absolutely. For a public-facing community with thousands of users, add the rate limiter extension, put the shim behind a process manager, and monitor n8n's execution logs. The free Pinecone pod caps at ~10K vectors, so plan your chunking strategy accordingly.
Building RAG pipelines is a bread-and-butter FDE task. If you want to get fast at going from messy customer requirements to shipped prototypes, study our playbook on going from problem to prototype in a week.
- #discord-bot
- #rag
- #customer-support
- #community
The FDE path · free to start
Learn the job, not just the tools.
The FDE path teaches forward deployed engineering in the order a real client project runs. Practise on realistic data, then prove it with graded work.