Hi everyone! I built an after-hours WhatsApp assistant for a study-visa consultancy and wrote up how it works. Sharing it here in case it helps anyone building something similar.
What it does
Answers student questions only from the consultancy’s own material (services, documents, intakes, offices)
Never invents fees, band scores or visa chances, and hands those to a counsellor instead
Collects name, service, country and best time to call, one question at a time
Replies in the same language as the student (English, Hindi or Punjabi)
How it’s built
WAHA in Docker next to n8n (demo only; production should use the official Cloud API)
Webhook → Filter → Set → AI Agent → HTTP Request back to WAHA
AI Agent with Gemini 3.5 Flash Lite, Postgres Chat Memory keyed by chat ID, and a Data Table tool (save_lead) that the model calls itself, with every column filled via $fromAI()
Two parallel branches for “seen” and “typing…” so the wait feels natural
Self-hosted on EC2 with Caddy for HTTPS
Things that tripped me up
WhatsApp now sends sender IDs as @lid instead of phone numbers, which broke my allowlist filter
Setting the WAHA webhook in both the env var and the dashboard caused every message to arrive twice
A retired Gemini model and 503 “high demand” errors made the bot go silent. Retry on fail, continue on error and a fallback reply fixed it.
Without “only confirm after save_lead succeeds” in the prompt, the model would tell students their details were saved even when they weren’t
Great project! I’m also working with n8n, Python, FastAPI, and PostgreSQL. I like how you handled duplicate messages and AI errors. Thanks for sharing!
Thanks Shital! This is how I mostly handle the AI errors :
Retry on the AI Agent node: You can retry asking the question again after a short ( usually 500ms - 1000ms ) delay, this handles the “503” errors that we get from free gemini models.
“Continue on error” on the agent: if all retries fail, the workflow keeps going instead of stopping, so the reply step still runs in this case you’d setup a default fallback reply so the end user gets something instead of no reply.
For Handling duplicate messages:
Debouncing the messages :
A Wait node pauses about 8 seconds.
After the wait, check whether a newer message arrived from the same chat. If one did, stop, because the newer run will handle it.
The run for the last message collects all unanswered messages, joins them, and sends them to the AI as one.
2.Rate limit per chat. :
If a chat sends more than about 10 messages in a minute, reply once with “A counsellor will get back to you shortly” and ignore the rest for a while. This protects your token budget from real abuse.
Thanks for explaining! I like your approach to handling AI errors and duplicate messages. The wait-and-combine method is useful for WhatsApp workflows. Rate limiting is also a good way to control costs. Thanks for sharing your experience!
Nice implementation. Was this built around a live consultancy’s requirements or as a prototype? I’m particularly interested in which reliability challenge mattered most in practice—WhatsApp message handling, lead capture, or human escalation.
Thanks! It started as a prototype for a real study-visa consultancy. I built it from their public information (services, document lists, intakes, offices) and pitched it to them as a demo. It ran on real WhatsApp with real messages for a week as a free trial, but it isn’t serving their students yet.
The biggest challenge for me was WhatsApp message handling, during the demo MVP I had no meta business account available at all so I went with WAHA temporarily.
During the week long free trial and “production run” getting the meta business account configured and receiving messages in n8n took 60% of my time, after that , handling the spamming and students sending messages in bursts.
Lead capture and human escalations were relatively easier to handle reliably compared to whatsapp message handling.
The point about only confirming after save_lead succeeds is the most important part for me.
An assistant can produce a convincing response while the actual business action fails, so the conversation can look successful even though no lead was recorded.
One extension I’d test is the handoff itself: when a question is escalated, does the counsellor receive the conversation context, collected details and a clear next action, or only a notification that somebody needs help?
That would make the boundary between “AI answered” and “the business accepted ownership” visible.
Nice build, and the @lid and double-webhook notes are useful to know.
For memory: in the Postgres Chat Memory node, set Session ID to “Define below” and use an expression for the sender’s number instead of the raw chat ID, e.g. {{ $json.from.replace(/@.*/, ‘’) }} (adjust to wherever your WAHA payload puts it). That way each contact gets one persistent thread, even if the @lid format changes later, as long as you normalise it.
Keep in mind the context window length only controls how many recent messages the agent sees, so older details drop off. For long-term memory, I’d keep a small per-contact profile row (name, target country, stage of the enquiry). Update it from the workflow after each meaningful exchange, then inject it into the system prompt on every run. The agent then remembers the essentials even when the chat history has rolled over.
Disclosure: I work on MemorySync, a hosted memory layer for AI apps, which does that per-user long-term memory if you’d rather not maintain it yourself.