Bilingual WhatsApp Support Triage: An n8n + WAHA Playbook
Diagnose and recover bilingual WhatsApp support incidents across n8n and WAHA, including errors, hallucinations, sending failures, and handoff issues.
- Time needed
- 45 min
- Steps
- 6
- Level
- Intermediate
Objective
The target end state is a production workflow that:
Inbound handling
Receives inbound WhatsApp messages through WAHA webhooks
Classifies language (English vs. Spanish) and intent deterministically, with optional AI escalation
Generates responses appropriate to the detected language
Handles delivery failures, message reformatting, and handoff to human agents
Logs every decision with enough context to audit triage quality
Why it matters
The business reason for this implementation: bilingual support teams currently divert engineering or operations staff into manual triage of WhatsApp messages. The system does not replace human agents. It removes the repetitive classification and routing work so agents only handle uncertain, high-risk, or complex cases.
Success criteria
An inbound WhatsApp message is received, classified, and answered or escalated without manual intervention in the standard path
English messages receive English responses; Spanish messages receive Spanish responses
Delivery failures are detected and retried within the configured limits; persistent failures surface to an error channel
Ambiguous messages are escalated to a human agent rather than answered incorrectly
Every decision produces a log entry containing message ID, language result, route taken, and timestamp
Prerequisites
Access
Administrative access to an n8n instance (self-hosted or cloud)
Access to a WAHA instance with an authenticated WhatsApp session
Write access to the database or data store used for logging (SQLite, Postgres, or equivalent)
Access to the notification channel (Slack, email, or similar) used for escalations
Data
A sample set of at least 50 historical bilingual support messages, labeled with correct language and intended route, to validate classification accuracy
The WhatsApp business number's display name and language preference settings
Technical conditions
n8n version that supports webhook triggers and the HTTP Request node
WAHA API endpoint reachable from the n8n instance
WhatsApp session active and connected (not in QR-pending state)
Secrets management available for the WAHA API key; do not store credentials in workflow code
Decisions
Language detection method: Deterministic keyword/character-set detection, an AI model via n8n's LangChain or HTTP node, or a hybrid. Deterministic detection is preferred unless message volume or ambiguity justifies AI. See Step 3.
Escalation channel: Which Slack channel, email address, or ticketing system receives handoff notifications.
Response sender: Whether replies come from the same WAHA session or a separate outbound session.
Production release window: When the workflow will begin receiving live traffic, including confirmation that support staff are ready to handle escalations.
Decisions
Confirm the following before starting implementation. These choices affect configuration later in the playbook.
Tools and systems
- n8n: Workflow orchestration. Hosts the webhook trigger, classification logic, routing branches, logging nodes, and retry/error handling.
- WAHA: WhatsApp HTTP API. Provides the webhook for inbound messages, the API for sending replies, and message status events (delivered, failed).
- Database (SQLite/Postgres): Decision log storage. Records message ID, detected language, route, timestamp, and outcome.
- Notification channel: Receives escalation alerts when messages route to human review or when delivery persistently fails.
Step 1 — Prepare the WAHA webhook and message log
What this step does
Actions
- Confirm the WAHA session is connected. In the WAHA dashboard or via the API, verify the session state is not
pendingordisconnected. - Identify the webhook URL that WAHA will use. In n8n, create a Webhook node that listens for POST requests at a path such as
/whatsapp-inbound. Copy the generated webhook URL. - Configure WAHA to send inbound message events to that URL. Use the WAHA webhook settings or dashboard, and set it to deliver
messageandmessage.anyevents at minimum. Includemessage.ackevents if you want to track delivery status in n8n. - Create the message decision log table in the database:
CREATE TABLE message_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wa_message_id TEXT NOT NULL UNIQUE,
phone TEXT NOT NULL,
message_text TEXT,
detected_language TEXT,
route_taken TEXT,
confidence REAL,
decision_source TEXT,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
- In n8n, add a Postgres (or SQLite) node that inserts a row into
message_logwhenever the webhook fires. - Send a test message to the WhatsApp business number.
- Verify the webhook fires in n8n and a row is inserted into
message_log.
Important considerations
- The webhook URL must be reachable from WAHA. If n8n is behind a firewall, expose the webhook endpoint or use an n8n tunnel during testing.
- Do not log the full message body into notifications or error channels. Log it into the database only.
- WAHA may redeliver events. The
UNIQUEconstraint onwa_message_idprevents duplicate log rows; handle insert conflicts gracefully in the workflow.
Step 2 — Set up outbound reply sending
What this step does
Actions
- In n8n, add an HTTP Request node configured to call the WAHA send-message endpoint.
- Set the method to
POSTand the URL to<WAHA_API_BASE_URL>/api/sendText. - Add the WAHA API key as a header:
X-Api-Key: <WAHA_API_KEY>. - Configure the request body:
{
"session": "<WAHA_SESSION_NAME>",
"chatId": "{{ $json.phone }}@c.us",
"text": "{{ $json.replyText }}"
}
- Run the workflow with a test payload containing a real recipient phone number and the text
Test reply from triage system. - Verify the message arrives on the recipient's WhatsApp.
Important considerations
- Store the API key in n8n credentials, not as a static header value in the node.
- WhatsApp requires the
@c.ussuffix for personal chat IDs. Do not construct it twice. - If messages exceed WhatsApp limits, truncate text at the message boundary before sending. WhatsApp will reject or split overlong messages unpredictably.
Step 3 — Build the classification and routing logic
What this step does
Determines the language of the incoming message and decides whether it can be answered automatically or must go to a human. This is the core decision layer of the system.
Choose the language detection model
Use deterministic detection as the default. It is predictable, auditable, and does not require model latency or cost. Deterministic detection works when messages contain enough Spanish-specific character sequences or common words to be unambiguous.
Use AI-based detection when messages are frequently short, mixed-language, or slang-heavy, and the deterministic classifier's error rate is unacceptable in validation.
A hybrid approach may be appropriate: deterministic detection with an AI fallback when confidence is low.
Actions
- Add a Function node after the webhook that normalizes the message text.
- Implement deterministic language detection. A simplified implementation:
// Simplified example — refine thresholds with real data
const text = $json.messageText.toLowerCase();
const spanishMarkers = [
'hola', 'gracias', 'por favor', 'buenos días', 'buenas tardes',
'querría', 'necesito', 'cuándo', 'dónde', 'cuál', 'el', 'la', 'los', 'las',
'ñ', 'á', 'é', 'í', 'ó', 'ú', '¿', '¡'
];
let score = 0;
for (const marker of spanishMarkers) {
if (text.includes(marker)) score += 1;
}
const decision = {
detectedLanguage: score >= 3 ? 'es' : 'en',
confidence: score >= 3 ? Math.min(score / 5, 0.95) : 0.6,
decisionSource: 'deterministic'
};
return decision;
- If using AI fallback, add an HTTP Request node (or LangChain node) that classifies the message when deterministic confidence is below threshold. Require the model to return a JSON object with
languageandconfidencefields. - Add a Switch node with the following branches:
es→ Spanish response pathen→ English response pathambiguous(confidence below threshold, or AI call failed) → escalation path
- For each language path, add a "generate response" step. Start with a template-based response that restates the detected intent and sets expectations. Do not attempt free-form AI answering until the template path is proven in validation.
- Add an escalation branch that constructs a handoff message containing message ID, phone, detected language, confidence, and the original message text, then sends it to the notification channel.
![IMAGE: fig-1 | Decision-flow diagram showing message text entering language detection, branching on confidence into English path, Spanish path, or ambiguous escalation, with each branch feeding either an automated template reply or a human agent handoff notification]]
Important considerations
Step 4 — Implement the response generation and handoff
What this step does
Actions
- Build the English response template. Example:
// Simplified example
const template = `
Hello! We received your message. Our team typically responds within
2 business hours. Your reference number is ${$json.waMessageId}.
`.trim();
- Build the Spanish response template:
// Simplified example
const template = `
¡Hola! Recibimos tu mensaje. Nuestro equipo suele responder dentro
de 2 horas hábiles. Tu número de referencia es ${$json.waMessageId}.
`.trim();
- Connect the template output to the HTTP Request node from Step 2.
- For the escalation path, construct a notification payload containing:
wa_message_idphonemessage_textdetected_languageconfidencereason(e.g., "low confidence", "AI failure")
- Send the notification to the escalation channel.
- Add a final logging node that updates the
message_logrow withroute_takenanddecision_source.
Important considerations
Step 5 — Add delivery confirmation and retry handling
What this step does
Detects and recovers from sending failures so a failed reply does not silently disappear. This is the difference between a demo workflow and a production support tool.
Actions
- Configure WAHA to send
message.ackevents to the n8n webhook (or a second webhook path). - In n8n, create a workflow branch that handles
message.ackevents. Map ack codes:
1/2→ delivered3→ read- Negative or error states → failed
- Build a state store for outbound messages. A simplified table:
CREATE TABLE outbound_state (
id INTEGER PRIMARY KEY AUTOINCREMENT,
wa_message_id TEXT NOT NULL UNIQUE,
phone TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'pending',
attempts INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL DEFAULT (datetime('now'))
);
- When a send is initiated, insert a row with status
pending. - On
message.ackfailure, incrementattempts. If attempts are below the retry limit (start with 3), re-send the same message content. - If attempts exceed the limit, mark the message as
failedand send an alert to the escalation channel containingwa_message_id,phone, and failure information. - Add cleanup: mark messages as
deliveredwhen ack confirms delivery, and schedule a sweep (e.g., n8n cron trigger) that flags messages stuck inpendingfor longer than a threshold (e.g., 10 minutes) as suspicious.
![IMAGE: fig-2 | Sequence diagram showing workflow sending a message, receiving an ack event, retrying on failure up to a limit, then alerting the escalation channel after the final failed attempt]]
Important considerations
Step 6 — Wire the paths together end-to-end
What this step does
Connects the verified components so the system operates as one workflow rather than separate tested fragments.
Actions
- In the main n8n workflow, connect:
- Webhook → message log insert → normalize → classify → route
- Each route branch → response generation → send → outbound state insert
- Ambiguous branch → escalation notification → log update
- Connect the ack-handling workflow or branch to the outbound state table.
- Add the pending-message sweep cron trigger and connect it to the alert path.
- Re-run the full test suite from earlier steps as one continuous run.
- Enable the workflow in n8n (activate it) so the webhook accepts live traffic.
Important considerations
Validation
Run the following end-to-end tests against the live workflow. Use a test phone number, not a real customer.
Functional behavior
Send a Spanish message: Hola, necesito ayuda con mi pedido por favor.
Confirm the customer receives a Spanish template reply with a reference number.
Send an English message: I need help with my order.
Confirm the customer receives an English template reply.
Send an ambiguous message: meh or a single emoji.
Confirm no reply is sent and an escalation appears in the notification channel.
Data correctness
Query message_log and confirm each message has the correct detected_language, route_taken, and decision_source.
Confirm outbound_state contains one row per outbound message with status delivered.
Failure behavior
Temporarily disable the WAHA session.
Send a message that triggers a reply.
Confirm the send fails, retries occur, and after the limit an escalation alert fires.
Restore the WAHA session and confirm the next message succeeds.
Repeatability
Send the same message twice.
Confirm two message_log rows with distinct wa_message_id values, and two distinct reference numbers for the customer.
Rollback & edge cases
Rollback
The system consists of n8n workflows, a WAHA session, and database tables. Roll back in this order:
- Disable the n8n workflow that handles inbound messages. This immediately stops new message processing. Existing webhook traffic that arrives after deactivation is lost — notify the support team before disabling if live traffic is flowing.
- Disable the ack-handling workflow to stop state updates and retries.
- Restore prior n8n workflow versions if you exported them before changes. If not, the workflows are recoverable only from manual reconstruction.
- Keep the database tables. Dropping
message_logoroutbound_stateis destructive and loses audit data. If you must clear test data, delete only rows with test phone numbers, or run a dedicated cleanup that preserves production records.
Do not disable the WAHA session to roll back. This interrupts all WhatsApp traffic, not just automation.
Edge cases
- Empty message body: WAHA may deliver messages with no text (images, voice notes). Route these to escalation with reason
non-text, not to language classification. - Duplicate webhook delivery: WAHA redelivery produces duplicate processing. The
UNIQUEconstraint onwa_message_idprevents duplicate log rows, but ensure the reply-send step also checks whether a reply was already sent for that ID. - Mixed-language message: A message with both English and Spanish may produce a mid-confidence score. Escalate if confidence is below threshold; do not guess.
- Expired WAHA API key: Sends fail with authentication errors. The retry logic will exhaust attempts and alert. Rotate the key and re-test.
- WhatsApp session disconnect: Sends fail immediately. The outbound state will accumulate
pendingrows. Handle by alerting on sustained failure and requiring manual session reconnect. - Ack events not arriving: If WAHA is not configured to send
message.ack, the sweep job will flag all messages as stuck. Confirm ack delivery in one test before enabling the sweep. - Very long messages: WhatsApp truncates. Truncate customer-facing replies at the message boundary and log that truncation occurred.
- Timezone skew: Webhook timestamps use server time. If escalation alerts need local time, apply the conversion at the notification step, not in the log.
Next step
Keep going
More playbooks
Want it running on your stack?
We implement this playbook for clients, tailored to your stack, your data, and your team's workflow, and handed over documented.