WhatsApp AI Assistant
Module 20 · ai · intermediate
starter.json
In n8n, copy this file and paste it onto the canvas, or use Import from File.
{
"name": "20 · WhatsApp AI Assistant (starter)",
"nodes": [
{
"parameters": {
"updates": [
"messages"
],
"options": {}
},
"id": "m20-whatsapp-trigger",
"name": "WhatsApp Trigger",
"type": "n8n-nodes-base.whatsAppTrigger",
"typeVersion": 1,
"position": [
0,
0
],
"webhookId": "course-20-wa-trigger"
},
{
"parameters": {
"httpMethod": "POST",
"path": "wa/inbound",
"responseMode": "responseNode",
"options": {}
},
"id": "m20-inbound-api",
"name": "Inbound API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
180
],
"webhookId": "course-20-inbound"
},
{
"parameters": {
"httpMethod": "GET",
"path": "wa/inbound",
"responseMode": "responseNode",
"options": {}
},
"id": "m20-verify-api",
"name": "Verify API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
400
],
"webhookId": "course-20-verify"
},
{
"parameters": {
"httpMethod": "GET",
"path": "wa/conversation",
"responseMode": "responseNode",
"options": {}
},
"id": "m20-conversation-api",
"name": "Conversation API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
580
],
"webhookId": "course-20-conversation"
},
{
"parameters": {
"httpMethod": "POST",
"path": "wa/handoff/release",
"responseMode": "responseNode",
"options": {}
},
"id": "m20-release-handoff-api",
"name": "Release Handoff API",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2,
"position": [
0,
760
],
"webhookId": "course-20-release"
},
{
"parameters": {
"assignments": {
"assignments": [
{
"id": "s1",
"name": "business_name",
"value": "Cool Breeze Heating & Air",
"type": "string"
},
{
"id": "s2",
"name": "phone_number_id",
"value": "123456789012345",
"type": "string"
},
{
"id": "s3",
"name": "verify_token",
"value": "course-20-verify-me",
"type": "string"
},
{
"id": "s4",
"name": "use_ai",
"value": true,
"type": "boolean"
},
{
"id": "s5",
"name": "timezone",
"value": "America/Chicago",
"type": "string"
},
{
"id": "s6",
"name": "hours",
"value": "={{ { mon: { open: 8, close: 18 }, tue: { open: 8, close: 18 }, wed: { open: 8, close: 18 }, thu: { open: 8, close: 18 }, fri: { open: 8, close: 18 }, sat: { open: 9, close: 13 } } }}",
"type": "object"
},
{
"id": "s7",
"name": "hours_text",
"value": "Mon–Fri 8 AM–6 PM, Sat 9 AM–1 PM (Central)",
"type": "string"
},
{
"id": "s8",
"name": "service_area",
"value": "Austin, Round Rock, Pflugerville and Cedar Park",
"type": "string"
},
{
"id": "s9",
"name": "service_zips",
"value": "={{ ['78701','78702','78703','78704','78705','78721','78722','78723','78727','78728','78729','78731','78745','78748','78750','78751','78752','78753','78757','78758','78759','78660','78664','78665','78681','78613'] }}",
"type": "array"
},
{
"id": "s10",
"name": "services",
"value": "={{ [ { name: 'AC repair', keywords: ['ac ', 'a/c', 'air condition', 'cooling', 'not cooling', 'warm air'] }, { name: 'heating repair', keywords: ['heat', 'furnace', 'heater', 'no heat'] }, { name: 'maintenance tune-up', keywords: ['tune', 'maintenance', 'check-up', 'checkup', 'service plan'] }, { name: 'new system install', keywords: ['new system', 'replace', 'install', 'new unit', 'new ac'] } ] }}",
"type": "array"
},
{
"id": "s11",
"name": "faq",
"value": "={{ [ { keywords: ['hour', 'open', 'close', 'weekend', 'saturday', 'sunday'], answer: 'We are open Mon–Fri 8 AM–6 PM and Sat 9 AM–1 PM (Central). Messages after hours get a reply the next time we open.' }, { keywords: ['price', 'cost', 'how much', 'fee', 'charge', 'diagnostic'], answer: 'Our diagnostic visit is $89, credited toward the repair if you go ahead. Tune-ups are $129. Repairs are quoted on site before any work starts.' }, { keywords: ['area', 'serve', 'location', 'come to', 'zip'], answer: 'We serve Austin, Round Rock, Pflugerville and Cedar Park.' }, { keywords: ['financ', 'payment plan', 'monthly'], answer: 'Financing is available on new systems, with plans from 12 to 60 months (subject to approval).' }, { keywords: ['warranty', 'guarantee'], answer: 'Repairs come with a 1-year parts and labor warranty; new systems with a 10-year parts warranty.' } ] }}",
"type": "array"
},
{
"id": "s12",
"name": "emergency_keywords",
"value": "={{ ['smell gas', 'gas smell', 'gas leak', 'carbon monoxide', 'co alarm', 'smoke', 'sparks', 'fire'] }}",
"type": "array"
},
{
"id": "s13",
"name": "confirm_within",
"value": "1 business hour",
"type": "string"
},
{
"id": "s14",
"name": "handoff_hours",
"value": 12,
"type": "number"
},
{
"id": "s15",
"name": "memory_turns",
"value": 10,
"type": "number"
}
]
},
"includeOtherFields": true,
"options": {}
},
"id": "m20-settings-edit-me",
"name": "Settings (edit me)",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
240,
380
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "r1",
"leftValue": "={{ $('WhatsApp Trigger').isExecuted || $('Inbound API').isExecuted }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "message"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "r2",
"leftValue": "={{ $('Verify API').isExecuted }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "verify"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "r3",
"leftValue": "={{ $('Conversation API').isExecuted }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "conversation"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "r4",
"leftValue": "={{ $('Release Handoff API').isExecuted }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "release"
}
]
},
"options": {}
},
"id": "m20-route",
"name": "Route",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.2,
"position": [
460,
380
]
},
{
"parameters": {
"jsCode": "// Turn a WhatsApp Cloud API webhook into one item per incoming message.\n// Accepts the raw webhook body (Inbound API) or the WhatsApp Trigger's items (one per change.value).\nconst viaApi = $('Inbound API').isExecuted;\nconst q = viaApi ? $('Inbound API').first().json.query ?? {} : {};\nconst values = viaApi\n ? ($('Inbound API').first().json.body?.entry ?? []).flatMap((e) => e.changes ?? []).map((c) => c.value ?? {})\n : $('WhatsApp Trigger').all().map((i) => i.json);\n\nconst textOf = (m) =>\n m.text?.body ??\n m.interactive?.button_reply?.title ??\n m.interactive?.list_reply?.title ??\n m.button?.text ??\n m[m.type]?.caption ??\n '';\n\nconst out = [];\nfor (const v of values) {\n const names = Object.fromEntries((v.contacts ?? []).map((c) => [c.wa_id, c.profile?.name ?? '']));\n for (const m of v.messages ?? []) {\n out.push({\n json: {\n wa_id: String(m.from ?? ''),\n profile_name: names[m.from] ?? '',\n message_id: String(m.id ?? ''),\n type: m.type ?? 'unknown',\n text: String(textOf(m)).trim().slice(0, 2000),\n received_at: m.timestamp ? new Date(Number(m.timestamp) * 1000).toISOString() : new Date().toISOString(),\n phone_number_id: v.metadata?.phone_number_id ?? '',\n dry_run: q.dry_run === 'true',\n now: q.now || new Date().toISOString(), // tests inject the clock with ?now=\n via_api: viaApi,\n },\n });\n }\n}\n// Status updates (sent/delivered/read) have no messages: return one marker item so the API caller gets an answer.\nreturn out.length ? out : [{ json: { ignored: true, reason: 'no messages (status update or other event)', via_api: viaApi } }];\n"
},
"id": "m20-parse-messages",
"name": "Parse Messages",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
700,
60
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ !$json.ignored }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "m20-any-messages",
"name": "Any Messages?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
920,
60
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.via_api }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "m20-ignored-via-api",
"name": "Ignored via API?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
1140,
240
]
},
{
"parameters": {
"respondWith": "json",
"responseBody": "={{ { ignored: true, reason: $json.reason } }}",
"options": {}
},
"id": "m20-respond-ignored",
"name": "Respond Ignored",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
1360,
240
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $('Verify API').first().json.query['hub.mode'] === 'subscribe' && $('Verify API').first().json.query['hub.verify_token'] === $('Settings (edit me)').first().json.verify_token }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "m20-token-matches",
"name": "Token Matches?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
700,
400
]
},
{
"parameters": {
"respondWith": "text",
"responseBody": "={{ $('Verify API').first().json.query['hub.challenge'] }}",
"options": {}
},
"id": "m20-respond-challenge",
"name": "Respond Challenge",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
920,
360
]
},
{
"parameters": {
"respondWith": "text",
"responseBody": "forbidden",
"options": {
"responseCode": 403
}
},
"id": "m20-respond-forbidden",
"name": "Respond Forbidden",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.1,
"position": [
920,
500
]
},
{
"parameters": {
"content": "## Your task\nVerification and message parsing are done. Send a test webhook to `wa/inbound` and look at Parse Messages.\n1. **Conversation State** (Code + static data): load the contact's chat, drop retried message ids, then rules first: STOP/START, safety keywords, active handoff (stay silent), \"talk to a person\", media → else *answer*.\n2. **Answer with AI** (Basic LLM Chain + OpenAI + structured output: reply, intent, handoff, lead), *On Error: Continue*.\n3. **Pick Reply**: AI reply if present, else the rule reply (FAQ, greeting, booking questions one at a time).\n4. **Save Turn**: history, lead fields, handoff timer, opt-out. Dry runs save nothing.\n5. Dry run → respond. Else **WhatsApp → Send** (On Error: Continue) → report (sent only with a message id).\n6. Conversation status + release-handoff endpoints.\n\nTest: `./test/test.sh`",
"height": 260,
"width": 420
},
"id": "starter-task-note",
"name": "Your Task",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
0,
-340
]
}
],
"connections": {
"WhatsApp Trigger": {
"main": [
[
{
"node": "Settings (edit me)",
"type": "main",
"index": 0
}
]
]
},
"Inbound API": {
"main": [
[
{
"node": "Settings (edit me)",
"type": "main",
"index": 0
}
]
]
},
"Verify API": {
"main": [
[
{
"node": "Settings (edit me)",
"type": "main",
"index": 0
}
]
]
},
"Conversation API": {
"main": [
[
{
"node": "Settings (edit me)",
"type": "main",
"index": 0
}
]
]
},
"Release Handoff API": {
"main": [
[
{
"node": "Settings (edit me)",
"type": "main",
"index": 0
}
]
]
},
"Settings (edit me)": {
"main": [
[
{
"node": "Route",
"type": "main",
"index": 0
}
]
]
},
"Route": {
"main": [
[
{
"node": "Parse Messages",
"type": "main",
"index": 0
}
],
[
{
"node": "Token Matches?",
"type": "main",
"index": 0
}
],
[],
[]
]
},
"Parse Messages": {
"main": [
[
{
"node": "Any Messages?",
"type": "main",
"index": 0
}
]
]
},
"Any Messages?": {
"main": [
[],
[
{
"node": "Ignored via API?",
"type": "main",
"index": 0
}
]
]
},
"Ignored via API?": {
"main": [
[
{
"node": "Respond Ignored",
"type": "main",
"index": 0
}
]
]
},
"Token Matches?": {
"main": [
[
{
"node": "Respond Challenge",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond Forbidden",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"pinData": {}
}