Build a WhatsApp Customer Support Agent Backed by Your Docs with n8n and Qdrant
What We're Building
We're standing up a WhatsApp bot that doesn't hallucinate. When a customer messages your business number, the agent retrieves the most relevant chunks from your actual docs (PDFs, markdown, web pages) and uses an LLM to synthesize a grounded, natural-language answer—all without writing a single line of backend glue code.
Feature list:
- Ingest any text-based docs (markdown, PDF, HTML) into a vector store
- Receive WhatsApp messages via webhook
- Perform semantic search against your docs with Qdrant
- Generate context-aware answers with Groq's Llama 3.3 70B (free tier)
- Send the reply back to the user on WhatsApp
- Log every interaction for later fine-tuning
Architecture & Data Flow
The system is a four-node pipeline orchestrated entirely in n8n. No custom server, no Flask, no Express. Here's how the pieces talk:
Walkthrough: The WhatsApp message hits an n8n webhook. n8n calls Groq's embedding endpoint to vectorize the query, then fires a search against Qdrant. The top 3 document chunks get stuffed into a prompt template and sent to Groq's chat endpoint. The LLM's response is POSTed back to the WhatsApp Business API, which delivers it to the user. Total latency under 2 seconds if you host n8n near your Qdrant cluster.
Prerequisites (All Free Tier)
Before we touch a workflow, grab these accounts. All links point to free-tier signups:
- n8n – Self-host the open-source version via Docker, or use n8n.cloud's free tier (limited workflows). I recommend local Docker for zero rate limits:
docker run -it --rm --name n8n -p 5678:5678 n8nio/n8n - Qdrant Cloud – Free forever cluster with 1GB storage. Sign up at qdrant.tech, create a cluster, grab your API key and endpoint URL.
- GroqCloud – Free inference with generous rate limits. Register at console.groq.com, create an API key. We'll use
llama-3.3-70b-versatilefor chat and a small embedding model. - WhatsApp Business API – Meta's free trial gives you 1,000 conversations/month. Set up via the Meta Developer Portal. You'll need a phone number ID, a permanent access token, and a configured webhook verify token.
- Your documentation – Export your docs as plain text or markdown files. We'll chunk and embed them in Step 1.
Step 1: Spin Up Qdrant and Index Your Docs
We'll use a standalone Python script to chunk and upload your docs. Run this once from your local machine. It uses Qdrant's free tier and Groq's embedding endpoint.
First, install dependencies:
pip install qdrant-client groq tiktoken
Now the indexing script. Replace the placeholder strings with your actual keys and endpoint:
import os
from qdrant_client import QdrantClient
from qdrant_client.models import Distance, VectorParams, PointStruct
from groq import Groq
import tiktoken
# --- CONFIG ---
QDRANT_URL = "https://your-cluster.cloud.qdrant.io:6333"
QDRANT_API_KEY = "your-qdrant-api-key"
GROQ_API_KEY = "your-groq-api-key"
COLLECTION_NAME = "support_docs"
CHUNK_SIZE = 500 # tokens per chunk
# --- CLIENTS ---
qdrant = QdrantClient(url=QDRANT_URL, api_key=QDRANT_API_KEY)
groq_client = Groq(api_key=GROQ_API_KEY)
enc = tiktoken.get_encoding("cl100k_base")
# --- CREATE COLLECTION ---
qdrant.recreate_collection(
collection_name=COLLECTION_NAME,
vectors_config=VectorParams(size=1024, distance=Distance.COSINE)
)
# --- LOAD & CHUNK YOUR DOCS ---
# Put all your .md or .txt files in a folder called 'docs'
points = []
point_id = 0
for filename in os.listdir("docs"):
with open(f"docs/{filename}", "r") as f:
text = f.read()
tokens = enc.encode(text)
for i in range(0, len(tokens), CHUNK_SIZE):
chunk_tokens = tokens[i:i+CHUNK_SIZE]
chunk_text = enc.decode(chunk_tokens)
# Get embedding from Groq
response = groq_client.embeddings.create(
model="all-MiniLM-L6-v2", # free, fast, 384-dim—but we'll pad or use a 1024-dim model
input=chunk_text
)
# Note: all-MiniLM-L6-v2 outputs 384-dim. For production, use a 1024-dim model.
# We'll handle the dimension mismatch in Step 3 by using the same model for queries.
embedding = response.data[0].embedding
points.append(PointStruct(
id=point_id,
vector=embedding,
payload={"text": chunk_text, "source": filename}
))
point_id += 1
# --- UPSERT ---
qdrant.upsert(collection_name=COLLECTION_NAME, points=points)
print(f"Indexed {point_id} chunks across {len(os.listdir('docs'))} files.")
Important: The free Groq embedding model all-MiniLM-L6-v2 outputs 384-dimensional vectors. Adjust the size parameter in VectorParams to 384. In Step 3, we'll use the same model for query embeddings. If you switch to a 1024-dim model later, recreate the collection.
Run the script. You'll see Indexed 147 chunks across 8 files. (or similar). Your knowledge base is live.
Step 2: Configure Groq for LLM Inference
We'll use Groq's chat completions endpoint for the final answer generation. In n8n, we'll call it via the HTTP Request node. First, verify your key works:
curl -X POST https://api.groq.com/openai/v1/chat/completions \
-H "Authorization: Bearer $GROQ_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "llama-3.3-70b-versatile",
"messages": [{"role": "user", "content": "What is your return policy?"}],
"temperature": 0.2
}'
You'll get a fast response. The free tier allows roughly 30 requests per minute—more than enough for a support bot handling a few conversations simultaneously.
Step 3: Build the Core n8n Workflow
Fire up n8n at http://localhost:5678. Create a new workflow. We'll build it node by node.
Node 1: WhatsApp Webhook Trigger
- Add a Webhook node.
- HTTP Method:
POST - Path:
/whatsapp-webhook - Response Mode:
Last Node - This node will receive the inbound message payload from Meta.
Node 2: Extract Message Text
WhatsApp sends a nested JSON payload. We need to parse out the user's message and their phone number. Add a Function node:
// Extract from the WhatsApp webhook payload
const body = $input.item.json.body;
const entry = body.entry[0];
const change = entry.changes[0];
const message = change.value.messages[0];
return {
phoneNumber: message.from,
messageText: message.text.body,
messageId: message.id
};
Node 3: Generate Query Embedding
Add an HTTP Request node. This calls Groq's embedding endpoint to vectorize the user's question.
- Method:
POST - URL:
https://api.groq.com/openai/v1/embeddings - Headers:
Authorization: Bearer {{$credentials.groqApi.apiKey}}Content-Type: application/json
- Body (JSON):
{
"model": "all-MiniLM-L6-v2",
"input": "{{ $json.messageText }}"
}
You'll need to create Groq credentials in n8n (Settings → Credentials → Groq API).
Node 4: Search Qdrant
Add another HTTP Request node. We query Qdrant with the embedding vector from the previous step.
- Method:
POST - URL:
https://your-cluster.cloud.qdrant.io:6333/collections/support_docs/points/search - Headers:
api-key: {{$credentials.qdrantApi.apiKey}}Content-Type: application/json
- Body (JSON):
{
"vector": {{ $json.data[0].embedding }},
"limit": 3,
"with_payload": true
}
Node 5: Assemble Prompt and Call LLM
Add a Function node to build the prompt, then an HTTP Request node to call Groq's chat endpoint.
Function node (Prompt Builder):
const chunks = $input.item.json.result.map(r => r.payload.text);
const context = chunks.join("\n\n---\n\n");
const userQuestion = $('WhatsApp Extract').item.json.messageText;
const systemPrompt = `You are a helpful customer support agent. Answer the user's question using ONLY the provided documentation chunks. If the answer is not in the chunks, say "I couldn't find that in our docs. Let me connect you with a human." Be concise and friendly.`;
return {
system: systemPrompt,
user: `Context:\n${context}\n\nUser question: ${userQuestion}`,
phoneNumber: $('WhatsApp Extract').item.json.phoneNumber
};
HTTP Request node (Groq Chat):
- Method:
POST - URL:
https://api.groq.com/openai/v1/chat/completions - Headers: same auth pattern
- Body (JSON):
{
"model": "llama-3.3-70b-versatile",
"messages": [
{"role": "system", "content": "{{ $json.system }}"},
{"role": "user", "content": "{{ $json.user }}"}
],
"temperature": 0.2,
"max_tokens": 500
}
Node 6: Send Reply via WhatsApp
Add a final HTTP Request node to POST the answer back to the user.
- Method:
POST - URL:
https://graph.facebook.com/v18.0/{{$credentials.whatsappApi.phoneNumberId}}/messages - Headers:
Authorization: Bearer {{$credentials.whatsappApi.accessToken}}Content-Type: application/json
- Body (JSON):
{
"messaging_product": "whatsapp",
"to": "{{ $('Prompt Builder').item.json.phoneNumber }}",
"type": "text",
"text": {
"body": "{{ $json.choices[0].message.content }}"
}
}
Connect all nodes in sequence. Save and activate the workflow.
Step 4: Connect WhatsApp Business API
Back in the Meta Developer Portal, configure your webhook:
- Under WhatsApp → Configuration, set the Callback URL to your n8n webhook URL. If you're running n8n locally, use a tool like ngrok to expose it:
ngrok http 5678. Your URL will behttps://abc123.ngrok.io/webhook-test/whatsapp-webhook. - Set the Verify Token to any string (e.g.,
my_verify_token_123). - In the n8n Webhook node, under Options, set the
Respond to Webhookmethod toUsing Webhook Test URLfor initial verification, then switch toProduction URLonce verified. - Subscribe to the
messageswebhook field.
Test by sending a message to your WhatsApp business number. Check n8n's execution log to trace each step.
Step 5: Running the Full Loop
Send a test message: "What's your refund policy?" The execution path:
- Webhook receives the JSON payload.
- Function node extracts
messageTextandphoneNumber. - Groq embedding node converts the question to a vector.
- Qdrant returns the top 3 most relevant chunks from your docs.
- Prompt builder injects chunks into the system prompt.
- Groq chat node generates a grounded answer.
- WhatsApp API delivers the reply.
Check n8n's execution data to see the exact payloads at each step. This visibility is why n8n beats custom code for prototyping—you can debug the vector search results and LLM prompts without adding logging.
Sensible Extensions
Once the base loop works, harden it:
- Conversation history: Store the last 5 messages per user in Qdrant (separate collection) or a simple SQLite node in n8n. Prepend them to the prompt so the agent remembers context.
- Human handoff: If the LLM's confidence is low (check if the answer contains "I couldn't find"), trigger a notification to a Slack channel or email via n8n's built-in nodes.
- Feedback loop: Add a quick-reply button after each answer ("Was this helpful? 👍/👎"). Log ratings to improve your docs or fine-tune prompts.
- Multi-language: Use Groq's
llama-3.3-70b-versatilewhich handles 10+ languages. Detect the user's language from the message and instruct the LLM to reply in kind.
If you enjoy stitching AI agents into practical tools, our Build a YouTube-to-Blog Repurposing Agent Using Whisper and Gemini Free Tier guide shows another zero-cost automation pattern.
Common Pitfalls
Qdrant dimension mismatch. If you index with a 384-dim model and search with a 1024-dim vector, Qdrant throws an error. Lock the embedding model in both the indexing script and the n8n query node. The free Groq model all-MiniLM-L6-v2 is 384-dimensional.
WhatsApp webhook verification fails. Meta sends a GET request with a hub.challenge parameter. n8n's Webhook node can handle this: in the node settings, set Response Mode to When Last Node Finishes and add a Respond to Webhook node early in the flow that echoes back the challenge. Alternatively, use n8n's built-in WhatsApp trigger node if you're on the cloud version.
ngrok rate limits. Free ngrok tunnels have connection limits. For anything beyond testing, deploy n8n on a $5 VPS (Hetzner, DigitalOcean) with a real domain and SSL. Let's Encrypt via nginx is a 10-minute setup.
Groq rate limits. The free tier is generous but not infinite. If you're handling high volume, implement exponential backoff in the HTTP Request node's retry settings, or cache common queries in Qdrant by storing question-answer pairs.
Stale docs. Your index is a snapshot. Set a cron job in n8n (Cron trigger → Execute Workflow) to re-run the indexing script weekly if your docs change.
FAQ
Q: Can I use this for internal teams on Slack instead of WhatsApp? Absolutely. Swap the WhatsApp nodes for n8n's native Slack node. The retrieval-and-generation core stays identical. This pattern works for Discord, Telegram, or even email via IMAP.
Q: What if my docs are behind a login? Run the indexing script on a machine with access. The resulting vectors in Qdrant don't contain raw credentials, but ensure your Qdrant instance is secured with API keys (the free tier enforces this).
Q: How many documents can the free Qdrant tier handle? 1GB of vector storage. With 384-dim vectors, that's roughly 1 million chunks, or 500 million tokens of text. More than enough for most product documentation sets.
Q: Is Groq's free tier really unlimited? It's rate-limited, not quota-limited. You get around 30 requests/minute for chat and 100+ for embeddings. For a support bot fielding a few concurrent conversations, you'll never hit the cap. If you scale, their paid tier is still cheaper than OpenAI.
Q: Can I fine-tune the LLM on my support transcripts? Groq currently serves inference-only. For fine-tuning, export your n8n execution logs (conversation history), format them as a dataset, and fine-tune an open model via Together AI or locally. Then serve it via Groq if they support custom models in the future.
Q: How do I debug when the agent gives wrong answers?
Open the n8n execution for that message. Inspect the Qdrant Search output—are the top chunks actually relevant? If not, your chunking strategy or embedding model may need tuning. If chunks are correct but the LLM hallucinates, lower the temperature to 0.1 and strengthen the system prompt's instruction to only use provided context.
If you're thinking about building more agentic tools like this, How to Break Into FDE Roles from a Backend or Frontend Background covers the exact skillset that turns these prototypes into production systems.
Want to build like a Forward Deployed Engineer?
FDE Coach is a cohort-based program in frontend, backend, AWS, and AI. Build real products and get referred to 200+ hiring partners.
Explore the program