The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "WF-01 \u00b7 WhatsApp Incoming",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "whatsapp",
"responseMode": "responseNode",
"options": {
"rawBody": true
}
},
"id": "wf01-webhook",
"name": "Webhook \u00b7 WhatsApp",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
-720,
300
],
"notes": "Meta posts here. Point the WhatsApp app's Callback URL at this path."
},
{
"parameters": {
"respondWith": "text",
"responseBody": "EVENT_RECEIVED",
"options": {
"responseCode": 200
}
},
"id": "wf01-ack",
"name": "Ack 200 Immediately",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1,
"position": [
-500,
300
],
"notes": "Meta retries for ~24h on any non-200. Acknowledge first, then process \u2014 never make the platform wait on the AI call."
},
{
"parameters": {
"jsCode": "// Guard clause: only forward payloads that actually contain messages.\n// Status-only callbacks (delivered/read) go down the status branch instead.\nconst items = [];\nfor (const item of $input.all()) {\n const body = item.json.body ?? item.json;\n const entries = body.entry ?? [];\n let hasMessages = false;\n let hasStatuses = false;\n for (const entry of entries) {\n for (const change of entry.changes ?? []) {\n const value = change.value ?? {};\n if ((value.messages ?? []).length) hasMessages = true;\n if ((value.statuses ?? []).length) hasStatuses = true;\n }\n }\n items.push({\n json: {\n body,\n signature: item.json.headers?.['x-hub-signature-256'] ?? null,\n hasMessages,\n hasStatuses,\n },\n });\n}\nreturn items;"
},
"id": "wf01-classify",
"name": "Classify Payload",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-280,
300
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"version": 2
},
"conditions": [
{
"id": "has-messages",
"leftValue": "={{ $json.hasMessages }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "wf01-is-message",
"name": "Contains a Message?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
-60,
300
]
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.OMNI_BACKEND_URL }}/api/internal/webhooks/whatsapp",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $env.OMNI_INTERNAL_TOKEN }}"
},
{
"name": "X-Hub-Signature-256",
"value": "={{ $json.signature }}"
},
{
"name": "X-Workflow-Code",
"value": "WF-01"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.body) }}",
"options": {
"timeout": 15000,
"response": {
"response": {
"fullResponse": false,
"neverError": false
}
}
}
},
"id": "wf01-ingest",
"name": "Normalize & Store (FastAPI)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
180,
200
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 2000,
"onError": "continueErrorOutput",
"notes": "The backend verifies the signature itself, normalizes via WhatsAppAdapter, de-duplicates on (org, platform, external_message_id), and returns the stored message plus its conversation."
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.OMNI_BACKEND_URL }}/api/internal/ai/route",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $env.OMNI_INTERNAL_TOKEN }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ message_id: $json.message_id, conversation_id: $json.conversation_id, organization_id: $json.organization_id }) }}",
"options": {
"timeout": 45000
}
},
"id": "wf01-router",
"name": "AI Router",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
400,
200
],
"retryOnFail": true,
"maxTries": 2,
"waitBetweenTries": 3000,
"onError": "continueErrorOutput",
"notes": "Returns { intent, confidence, action, lead_score, needs_human }."
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"version": 2
},
"conditions": [
{
"leftValue": "={{ $json.needs_human }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Human"
},
{
"conditions": {
"options": {
"version": 2
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "qualify",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Sales"
},
{
"conditions": {
"options": {
"version": 2
},
"conditions": [
{
"leftValue": "={{ $json.action }}",
"rightValue": "answer",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Support"
}
]
},
"options": {
"fallbackOutput": 0
}
},
"id": "wf01-switch",
"name": "Route by Decision",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
620,
200
],
"notes": "Fallback output is Human. When the router is unsure about its own output, a person sees it."
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.OMNI_BACKEND_URL }}/api/internal/approvals",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $env.OMNI_INTERNAL_TOKEN }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ subject_type: 'message', conversation_id: $json.conversation_id, reason: $json.escalation_reason ?? 'Low confidence or sensitive intent', confidence: $json.confidence, organization_id: $json.organization_id }) }}",
"options": {
"timeout": 15000
}
},
"id": "wf01-human",
"name": "Queue for Human",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
860,
60
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.OMNI_BACKEND_URL }}/api/internal/ai/sales",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $env.OMNI_INTERNAL_TOKEN }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ conversation_id: $json.conversation_id, message_id: $json.message_id, organization_id: $json.organization_id }) }}",
"options": {
"timeout": 45000
}
},
"id": "wf01-sales",
"name": "Sales Agent \u00b7 Qualify",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
860,
200
],
"retryOnFail": true,
"maxTries": 2,
"onError": "continueErrorOutput"
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.OMNI_BACKEND_URL }}/api/internal/ai/support",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $env.OMNI_INTERNAL_TOKEN }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ conversation_id: $json.conversation_id, message_id: $json.message_id, organization_id: $json.organization_id }) }}",
"options": {
"timeout": 45000
}
},
"id": "wf01-support",
"name": "Support Agent \u00b7 RAG Answer",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
860,
340
],
"retryOnFail": true,
"maxTries": 2,
"onError": "continueErrorOutput"
},
{
"parameters": {
"conditions": {
"options": {
"version": 2
},
"conditions": [
{
"leftValue": "={{ $json.needs_human }}",
"rightValue": false,
"operator": {
"type": "boolean",
"operation": "false",
"singleValue": true
}
},
{
"leftValue": "={{ $json.answer }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "wf01-safe-to-send",
"name": "Safe to Send?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1080,
340
],
"notes": "Second gate. The agent may downgrade its own confidence after searching the knowledge base, so the check happens again here."
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.OMNI_BACKEND_URL }}/api/internal/messages/send",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $env.OMNI_INTERNAL_TOKEN }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ conversation_id: $json.conversation_id, text: $json.answer, sender_type: 'ai', ai_run_id: $json.ai_run_id, confidence: $json.confidence, organization_id: $json.organization_id }) }}",
"options": {
"timeout": 30000
}
},
"id": "wf01-send",
"name": "Send via WhatsApp API",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1300,
280
],
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"onError": "continueErrorOutput",
"notes": "The backend owns the credential and picks the live or mock adapter. n8n never holds a platform token."
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.OMNI_BACKEND_URL }}/api/internal/webhooks/whatsapp/status",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $env.OMNI_INTERNAL_TOKEN }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.body) }}",
"options": {
"timeout": 15000
}
},
"id": "wf01-status",
"name": "Record Delivery Status",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
180,
420
],
"onError": "continueRegularOutput"
},
{
"parameters": {
"method": "POST",
"url": "={{ $env.OMNI_BACKEND_URL }}/api/internal/workflow-runs",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "=Bearer {{ $env.OMNI_INTERNAL_TOKEN }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ workflow_code: 'WF-01', n8n_execution_id: $execution.id, status: 'failed', platform: 'whatsapp', error: JSON.stringify($json.error ?? $json), input_payload: $json }) }}",
"options": {
"timeout": 15000
}
},
"id": "wf01-deadletter",
"name": "Dead Letter \u00b7 Manual Review",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1300,
540
],
"onError": "continueRegularOutput",
"notes": "Every error output converges here. The run shows up on the Workflows page as a failure needing attention."
}
],
"connections": {
"Webhook \u00b7 WhatsApp": {
"main": [
[
{
"node": "Ack 200 Immediately",
"type": "main",
"index": 0
}
]
]
},
"Ack 200 Immediately": {
"main": [
[
{
"node": "Classify Payload",
"type": "main",
"index": 0
}
]
]
},
"Classify Payload": {
"main": [
[
{
"node": "Contains a Message?",
"type": "main",
"index": 0
}
]
]
},
"Contains a Message?": {
"main": [
[
{
"node": "Normalize & Store (FastAPI)",
"type": "main",
"index": 0
}
],
[
{
"node": "Record Delivery Status",
"type": "main",
"index": 0
}
]
]
},
"Normalize & Store (FastAPI)": {
"main": [
[
{
"node": "AI Router",
"type": "main",
"index": 0
}
],
[
{
"node": "Dead Letter \u00b7 Manual Review",
"type": "main",
"index": 0
}
]
]
},
"AI Router": {
"main": [
[
{
"node": "Route by Decision",
"type": "main",
"index": 0
}
],
[
{
"node": "Dead Letter \u00b7 Manual Review",
"type": "main",
"index": 0
}
]
]
},
"Route by Decision": {
"main": [
[
{
"node": "Queue for Human",
"type": "main",
"index": 0
}
],
[
{
"node": "Sales Agent \u00b7 Qualify",
"type": "main",
"index": 0
}
],
[
{
"node": "Support Agent \u00b7 RAG Answer",
"type": "main",
"index": 0
}
]
]
},
"Sales Agent \u00b7 Qualify": {
"main": [
[
{
"node": "Send via WhatsApp API",
"type": "main",
"index": 0
}
],
[
{
"node": "Dead Letter \u00b7 Manual Review",
"type": "main",
"index": 0
}
]
]
},
"Support Agent \u00b7 RAG Answer": {
"main": [
[
{
"node": "Safe to Send?",
"type": "main",
"index": 0
}
],
[
{
"node": "Dead Letter \u00b7 Manual Review",
"type": "main",
"index": 0
}
]
]
},
"Safe to Send?": {
"main": [
[
{
"node": "Send via WhatsApp API",
"type": "main",
"index": 0
}
],
[
{
"node": "Queue for Human",
"type": "main",
"index": 0
}
]
]
},
"Send via WhatsApp API": {
"main": [
[],
[
{
"node": "Dead Letter \u00b7 Manual Review",
"type": "main",
"index": 0
}
]
]
},
"Queue for Human": {
"main": [
[],
[
{
"node": "Dead Letter \u00b7 Manual Review",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"saveDataErrorExecution": "all",
"saveDataSuccessExecution": "all",
"saveExecutionProgress": true,
"errorWorkflow": "WF-90"
},
"tags": [
{
"name": "incoming"
},
{
"name": "whatsapp"
}
],
"meta": {
"description": "Inbound WhatsApp: acknowledge, normalize through the backend, route with AI, then answer, qualify, or escalate. Every branch has an error output that lands in the dead-letter node."
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
WF-01 · WhatsApp Incoming. Uses httpRequest. Webhook trigger; 14 nodes.
Source: https://github.com/hamza01055/omni-ai-automation/blob/main/n8n/workflows/incoming/WF-01-whatsapp-incoming.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Flujowhatsapp. Uses @aldinokemal2104/n8n-nodes-gowa, postgres, httpRequest. Webhook trigger; 65 nodes.
HR teams, IT Operations, and System Administrators managing employee onboarding at scale. It’s perfect if you use Odoo 18 to trigger account requests and need Redmine + GitLab accounts created instant
This workflow is a complete, production-ready solution for recovering abandoned carts in Shopify stores using a multi-channel, multi-touch approach. It automates personalized follow-ups via Email, SMS
WF_UNIFIED_LEGAL_AUTOMATION. Uses googleSheets, httpRequest, telegram, telegramTrigger. Webhook trigger; 53 nodes.
Backbrief: transcripts (Zoom webhook -> Slack + vault). Uses httpRequest, slack. Webhook trigger; 52 nodes.