This workflow corresponds to n8n.io template #16482 — we link there as the canonical source.
This workflow follows the Agent → HTTP Request recipe pattern — see all workflows that pair these two integrations.
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 →
{
"id": "JZuXYgGQ5Rs0xhsP",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Book, reschedule, and confirm appointments from WhatsApp messages into Google Sheets",
"tags": [],
"nodes": [
{
"id": "f7c1208e-27b5-4bb0-b3b6-6911def9c945",
"name": "Sticky Note - Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-16,
-224
],
"parameters": {
"width": 800,
"height": 1288,
"content": "## \ud83d\udcc5 WhatsApp Appointment Booking System\n\nThis workflow enables users to book, reschedule, and confirm appointments directly through WhatsApp messages using AI-powered intent detection and Google Sheets as the appointment database.\n\n### Who's it for\n- Service businesses handling appointment bookings (salons, clinics, consultancies, restaurants)\n- Customer support teams wanting to automate repetitive scheduling conversations\n- Small businesses replacing manual WhatsApp booking with an automated system\n- Any operator running Google Sheets as a lightweight appointment database\n\n### How it works\n1. Customer sends a WhatsApp message (e.g. \"Book a haircut tomorrow at 10am\")\n2. Meta webhook delivers the message to n8n in real time\n3. Message context is normalised (phone number, name, timestamp)\n4. AI detects intent: book, reschedule, confirm, or unknown\n5. JS node parses the structured AI output and routes to the correct branch\n6. Each branch validates the request (date, time, business hours, weekday check)\n7. Valid requests are written or updated in Google Sheets\n8. A personalised reply is built and sent back via WhatsApp\n9. Every interaction is logged to an Activity Sheet for auditing\n\n### How to set up\n1. Import this workflow into n8n\n2. Add your Anthropic Claude credentials in n8n credential manager\n3. Create a Google Sheet with two tabs: `Appointments` and `ActivityLog`\n4. Replace `YOUR_SHEET_ID` in all Google Sheets nodes with your actual Sheet ID\n5. Replace `YOUR_PHONE_NUMBER_ID` and store your Meta access token as a Header Auth credential in n8n\n6. Register your n8n webhook URL in the Meta Developer Portal as your WhatsApp callback URL\n7. Activate the workflow and send a test message\n\n### Requirements\n- WhatsApp Business API access (Meta Developer account)\n- Google Sheets with OAuth2 credentials in n8n\n- Anthropic Claude API key\n- n8n instance with a publicly accessible webhook URL (cloud or tunnelled self-hosted)\n\n### How to customize\n- Change business hours by editing the hour range check in `JS - Validate Book Slot` and `JS - Validate Reschedule Slot` (default: 9am\u20136pm weekdays)\n- Add more service types by extending the service list in the AI prompt inside `AI - Detect Intent and Parse`\n- Add a Google Calendar node after the Sheets write to sync confirmed appointments to a calendar\n- Replace the plain text WhatsApp reply with a template message for richer formatting\n- Add a 24-hour reminder by creating a second scheduled workflow that reads upcoming appointments from Sheets and sends reminder messages\n\n### Google Sheets Column Layout\n\n**Appointments tab** \u2014 set up row 1 with these headers:\n`Appointment ID | Customer Name | Phone | Service | Date | Time | Status | Notes | Created At`\n\n**ActivityLog tab** \u2014 set up row 1 with these headers:\n`Timestamp | Phone | Customer Name | Intent | Status | Appointment ID | Date | Time | Service | Message Text`"
},
"typeVersion": 1
},
{
"id": "deda04de-3ebc-4de2-96ab-ab22e2cfa2cf",
"name": "Sticky Note - Trigger",
"type": "n8n-nodes-base.stickyNote",
"position": [
912,
256
],
"parameters": {
"color": 4,
"width": 844,
"height": 520,
"content": "## 1. Trigger and Intake"
},
"typeVersion": 1
},
{
"id": "295a967b-6d86-4793-82a4-b320a91a5cbf",
"name": "Sticky Note - Intent",
"type": "n8n-nodes-base.stickyNote",
"position": [
1792,
160
],
"parameters": {
"color": 3,
"width": 644,
"height": 900,
"content": "## 2. Intent Detection and Parsing"
},
"typeVersion": 1
},
{
"id": "cbd1e9d2-57f6-42a1-8dba-a1d805b8b842",
"name": "Sticky Note - DataOps",
"type": "n8n-nodes-base.stickyNote",
"position": [
2576,
64
],
"parameters": {
"color": 5,
"width": 996,
"height": 1028,
"content": "## 3. Conflict Check and Data Ops"
},
"typeVersion": 1
},
{
"id": "189e61c2-67cf-42c1-aab6-2980751407bb",
"name": "Sticky Note - Reply",
"type": "n8n-nodes-base.stickyNote",
"position": [
3648,
208
],
"parameters": {
"color": 6,
"width": 1236,
"height": 620,
"content": "## 4. Send Reply and Track"
},
"typeVersion": 1
},
{
"id": "f72cea62-79e0-4509-9119-7634f2eaa0ac",
"name": "Webhook - WhatsApp Inbound",
"type": "n8n-nodes-base.webhook",
"position": [
992,
400
],
"parameters": {
"path": "whatsapp-appointment",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 1.1
},
{
"id": "77c2ab57-b209-45b6-bec5-b315af467a8a",
"name": "Webhook - WhatsApp Verify",
"type": "n8n-nodes-base.webhook",
"position": [
1200,
592
],
"parameters": {
"path": "whatsapp-appointment",
"options": {},
"responseMode": "lastNode"
},
"typeVersion": 1.1
},
{
"id": "2ea62efa-57c1-4666-ac5c-a9f72d38006a",
"name": "Prepare Message Context",
"type": "n8n-nodes-base.set",
"position": [
1200,
400
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"name": "phoneNumber",
"type": "string",
"value": "={{ $json.body?.entry?.[0]?.changes?.[0]?.value?.messages?.[0]?.from || $json.entry?.[0]?.changes?.[0]?.value?.messages?.[0]?.from || '' }}"
},
{
"name": "messageText",
"type": "string",
"value": "={{ $json.body?.entry?.[0]?.changes?.[0]?.value?.messages?.[0]?.text?.body || $json.entry?.[0]?.changes?.[0]?.value?.messages?.[0]?.text?.body || '' }}"
},
{
"name": "messageId",
"type": "string",
"value": "={{ $json.body?.entry?.[0]?.changes?.[0]?.value?.messages?.[0]?.id || $json.entry?.[0]?.changes?.[0]?.value?.messages?.[0]?.id || Date.now().toString() }}"
},
{
"name": "displayName",
"type": "string",
"value": "={{ $json.body?.entry?.[0]?.changes?.[0]?.value?.contacts?.[0]?.profile?.name || $json.entry?.[0]?.changes?.[0]?.value?.contacts?.[0]?.profile?.name || 'Guest' }}"
},
{
"name": "timestamp",
"type": "string",
"value": "={{ new Date().toISOString() }}"
},
{
"name": "receivedDate",
"type": "string",
"value": "={{ new Date().toISOString().split('T')[0] }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "421e4b73-5954-42b7-93ca-d6e93250286a",
"name": "Filter Valid Messages",
"type": "n8n-nodes-base.filter",
"position": [
1600,
400
],
"parameters": {
"options": {},
"conditions": {
"conditions": [
{
"operator": {
"type": "string",
"operation": "notEmpty"
},
"leftValue": "={{ $json.messageText }}"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "3516166f-3dee-4c6b-88b9-dacbb28aba85",
"name": "AI - Detect Intent and Parse",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
1824,
400
],
"parameters": {
"text": "=Act as an appointment booking assistant. Analyze the WhatsApp message and extract structured information.\n\nMessage from {{ $json.displayName }} ({{ $json.phoneNumber }}):\n{{ $json.messageText }}\n\nExtract and return ONLY a valid JSON object with these fields (no markdown, no explanation):\n{\"intent\": \"book or reschedule or confirm or cancel or unknown\", \"customerName\": \"extracted name or use displayName\", \"service\": \"haircut or massage or consultation or dental or general\", \"requestedDate\": \"YYYY-MM-DD or null\", \"requestedTime\": \"HH:MM in 24h or null\", \"existingAppointmentRef\": \"appointment ID or date for reschedule/confirm or null\", \"newDate\": \"YYYY-MM-DD for reschedule or null\", \"newTime\": \"HH:MM for reschedule or null\", \"notes\": \"special requests or empty string\", \"confidence\": 0.9}\n\nToday is {{ $json.receivedDate }}. Calculate actual dates for relative references like tomorrow or next Monday.",
"options": {},
"promptType": "define"
},
"typeVersion": 1.6
},
{
"id": "2038c11d-3863-4dfc-9a76-04cca9e769a0",
"name": "Anthropic Claude Model",
"type": "@n8n/n8n-nodes-langchain.lmChatAnthropic",
"position": [
1856,
656
],
"parameters": {
"model": {
"__rl": true,
"mode": "list",
"value": "claude-sonnet-4-20250514"
},
"options": {}
},
"credentials": {
"anthropicApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.3
},
{
"id": "641474b7-a603-4f55-a8e5-382863d243ca",
"name": "JS - Parse AI Output",
"type": "n8n-nodes-base.code",
"position": [
2192,
400
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\nconst rawOutput = item.output || item.text || item.response || '{}';\n\nlet parsed;\ntry {\n const cleaned = rawOutput.replace(/```json|```/g, '').trim();\n parsed = JSON.parse(cleaned);\n} catch (e) {\n parsed = {\n intent: 'unknown',\n customerName: item.displayName || 'Guest',\n service: 'general',\n requestedDate: null,\n requestedTime: null,\n existingAppointmentRef: null,\n newDate: null,\n newTime: null,\n notes: '',\n confidence: 0\n };\n}\n\nreturn {\n json: {\n ...item,\n intent: parsed.intent || 'unknown',\n customerName: parsed.customerName || item.displayName || 'Guest',\n service: parsed.service || 'general',\n requestedDate: parsed.requestedDate || null,\n requestedTime: parsed.requestedTime || null,\n existingAppointmentRef: parsed.existingAppointmentRef || null,\n newDate: parsed.newDate || null,\n newTime: parsed.newTime || null,\n notes: parsed.notes || '',\n confidence: parsed.confidence || 0,\n appointmentId: 'APPT-' + Date.now()\n }\n};"
},
"typeVersion": 2
},
{
"id": "0c0cb49c-61ee-412b-8441-5d4ff714f5ff",
"name": "Route - Book",
"type": "n8n-nodes-base.filter",
"position": [
2736,
272
],
"parameters": {
"options": {},
"conditions": {
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.intent }}",
"rightValue": "book"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "b1c0a72e-1d35-4bdb-9e7c-d81bb138b831",
"name": "Route - Reschedule",
"type": "n8n-nodes-base.filter",
"position": [
2736,
448
],
"parameters": {
"options": {},
"conditions": {
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.intent }}",
"rightValue": "reschedule"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "7b898de6-ef0b-4176-a190-6711b26f7535",
"name": "Route - Confirm",
"type": "n8n-nodes-base.filter",
"position": [
2736,
624
],
"parameters": {
"options": {},
"conditions": {
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.intent }}",
"rightValue": "confirm"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "f5454bd1-aca1-4388-977b-0c8c5511334b",
"name": "Route - Unknown",
"type": "n8n-nodes-base.filter",
"position": [
2736,
800
],
"parameters": {
"options": {},
"conditions": {
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.intent }}",
"rightValue": "book"
},
{
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.intent }}",
"rightValue": "reschedule"
},
{
"operator": {
"type": "string",
"operation": "notEquals"
},
"leftValue": "={{ $json.intent }}",
"rightValue": "confirm"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "d7190b9b-c025-495b-a43f-0dc4b2e55ecb",
"name": "JS - Validate Book Slot",
"type": "n8n-nodes-base.code",
"position": [
3040,
272
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\nconst reqDate = item.requestedDate;\nconst reqTime = item.requestedTime;\n\nif (!reqDate || !reqTime) {\n return { json: { ...item, conflictCheckPassed: false, conflictReason: 'Missing date or time in the booking request.', actionType: 'book' } };\n}\n\nconst parts = reqTime.split(':').map(Number);\nconst hour = parts[0];\nconst dayOfWeek = new Date(reqDate).getDay();\n\nif (dayOfWeek === 0 || dayOfWeek === 6) {\n return { json: { ...item, conflictCheckPassed: false, conflictReason: 'Appointments are not available on weekends. Please choose a weekday.', actionType: 'book' } };\n}\n\nif (hour < 9 || hour >= 18) {\n return { json: { ...item, conflictCheckPassed: false, conflictReason: 'Appointments are available between 9:00 AM and 6:00 PM only.', actionType: 'book' } };\n}\n\nconst apptDate = new Date(reqDate);\nconst today = new Date();\ntoday.setHours(0, 0, 0, 0);\nif (apptDate < today) {\n return { json: { ...item, conflictCheckPassed: false, conflictReason: 'The requested date is in the past. Please choose a future date.', actionType: 'book' } };\n}\n\nreturn { json: { ...item, conflictCheckPassed: true, conflictReason: null, actionType: 'book' } };"
},
"typeVersion": 2
},
{
"id": "8c81c41b-8a0c-4b78-88ed-5faf257e3e4d",
"name": "JS - Validate Reschedule Slot",
"type": "n8n-nodes-base.code",
"position": [
3040,
448
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\nconst newDate = item.newDate;\nconst newTime = item.newTime;\nconst ref = item.existingAppointmentRef;\n\nif (!ref) {\n return { json: { ...item, conflictCheckPassed: false, conflictReason: 'Could not identify the existing appointment to reschedule. Please provide your original appointment date or ID.', actionType: 'reschedule' } };\n}\n\nif (!newDate || !newTime) {\n return { json: { ...item, conflictCheckPassed: false, conflictReason: 'Please provide the new date and time you would like to reschedule to.', actionType: 'reschedule' } };\n}\n\nconst hour = parseInt(newTime.split(':')[0], 10);\nconst dayOfWeek = new Date(newDate).getDay();\n\nif (dayOfWeek === 0 || dayOfWeek === 6) {\n return { json: { ...item, conflictCheckPassed: false, conflictReason: 'The new date falls on a weekend. Please choose a weekday.', actionType: 'reschedule' } };\n}\n\nif (hour < 9 || hour >= 18) {\n return { json: { ...item, conflictCheckPassed: false, conflictReason: 'New time must be between 9:00 AM and 6:00 PM.', actionType: 'reschedule' } };\n}\n\nreturn { json: { ...item, conflictCheckPassed: true, conflictReason: null, actionType: 'reschedule' } };"
},
"typeVersion": 2
},
{
"id": "1c4e13d8-f906-475a-a88e-6e695b244a48",
"name": "JS - Validate Confirm Ref",
"type": "n8n-nodes-base.code",
"position": [
3040,
624
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\nconst ref = item.existingAppointmentRef;\n\nif (!ref) {\n return { json: { ...item, conflictCheckPassed: false, conflictReason: 'Could not find an appointment reference to confirm. Please share your appointment ID or date.', actionType: 'confirm' } };\n}\n\nreturn { json: { ...item, conflictCheckPassed: true, conflictReason: null, actionType: 'confirm' } };"
},
"typeVersion": 2
},
{
"id": "276d406d-14d9-4c85-9c03-2fb6c557a24d",
"name": "JS - Handle Unknown Intent",
"type": "n8n-nodes-base.code",
"position": [
3040,
800
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\nreturn { json: { ...item, conflictCheckPassed: false, conflictReason: null, actionType: 'unknown' } };"
},
"typeVersion": 2
},
{
"id": "a44807fd-5874-46ff-8ca6-3ab4432f8ca0",
"name": "Sheets - Create Appointment",
"type": "n8n-nodes-base.httpRequest",
"position": [
3312,
192
],
"parameters": {
"url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/Appointments!A1:append?valueInputOption=USER_ENTERED",
"method": "POST",
"options": {},
"jsonBody": "={\"values\": [[\"{{ $json.appointmentId }}\", \"{{ $json.customerName }}\", \"{{ $json.phoneNumber }}\", \"{{ $json.service }}\", \"{{ $json.requestedDate }}\", \"{{ $json.requestedTime }}\", \"Booked\", \"{{ $json.notes }}\", \"{{ $json.timestamp }}\"]]}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api"
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "f2cdb2d5-6c83-4a0e-9188-a1a3e3c7f632",
"name": "Sheets - Update Reschedule",
"type": "n8n-nodes-base.httpRequest",
"position": [
3312,
384
],
"parameters": {
"url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/Appointments!G2?valueInputOption=USER_ENTERED",
"method": "PUT",
"options": {},
"jsonBody": "={\"values\": [[\"Rescheduled\", \"{{ $json.newDate }}\", \"{{ $json.newTime }}\"]]}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api"
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "f5adeca4-4157-451e-b52f-39bd5358b3a8",
"name": "Sheets - Confirm Appointment",
"type": "n8n-nodes-base.httpRequest",
"position": [
3312,
592
],
"parameters": {
"url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/Appointments!G2?valueInputOption=USER_ENTERED",
"method": "PUT",
"options": {},
"jsonBody": "={\"values\": [[\"Confirmed\"]]}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api"
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "50e7a57e-a0c3-4fae-9c3f-91079badcce1",
"name": "JS - Build Reply Message",
"type": "n8n-nodes-base.code",
"position": [
3920,
448
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "const item = $input.item.json;\nlet replyMessage = '';\n\nif (item.actionType === 'book') {\n if (item.conflictCheckPassed) {\n replyMessage = 'Appointment Booked! Hi ' + item.customerName + ', your appointment is confirmed.\\nDate: ' + item.requestedDate + '\\nTime: ' + item.requestedTime + '\\nService: ' + item.service + '\\nRef ID: ' + item.appointmentId + '\\n\\nReply CONFIRM ' + item.appointmentId + ' to confirm your slot.';\n } else {\n replyMessage = 'Booking Issue. Hi ' + item.customerName + ', we could not complete your booking.\\nReason: ' + item.conflictReason + '\\n\\nPlease reply with your preferred date (YYYY-MM-DD) and time.';\n }\n} else if (item.actionType === 'reschedule') {\n if (item.conflictCheckPassed) {\n replyMessage = 'Appointment Rescheduled! Hi ' + item.customerName + ', your appointment has been rescheduled.\\nNew Date: ' + item.newDate + '\\nNew Time: ' + item.newTime + '\\nService: ' + item.service + '\\n\\nReply CONFIRM ' + item.existingAppointmentRef + ' to confirm your new slot.';\n } else {\n replyMessage = 'Reschedule Issue. Hi ' + item.customerName + ', we could not reschedule.\\nReason: ' + item.conflictReason + '\\n\\nPlease provide your appointment ID and the new date and time you want.';\n }\n} else if (item.actionType === 'confirm') {\n if (item.conflictCheckPassed) {\n replyMessage = 'Appointment Confirmed! Hi ' + item.customerName + ', your appointment is now confirmed.\\nReference: ' + item.existingAppointmentRef + '\\n\\nWe will send you a reminder 24 hours before. See you soon!';\n } else {\n replyMessage = 'Confirmation Issue. Hi ' + item.customerName + ', we could not confirm.\\nReason: ' + item.conflictReason + '\\n\\nPlease share your appointment ID or booking date.';\n }\n} else {\n replyMessage = 'Welcome to Appointment Booking! Hi ' + (item.customerName || 'there') + ', I can help you:\\n- Book a new appointment\\n- Reschedule an existing one\\n- Confirm your appointment\\n\\nExample: Book a haircut for tomorrow at 10 AM';\n}\n\nreturn {\n json: {\n ...item,\n replyMessage: replyMessage,\n finalStatus: item.conflictCheckPassed ? item.actionType.toUpperCase() + '_SUCCESS' : item.actionType.toUpperCase() + '_FAILED'\n }\n};"
},
"typeVersion": 2
},
{
"id": "a8e6cf45-e137-4603-a4aa-20e3c981a8fa",
"name": "Wait - Rate Limit Buffer",
"type": "n8n-nodes-base.wait",
"position": [
4128,
448
],
"parameters": {},
"typeVersion": 1
},
{
"id": "fb55353d-be41-444d-9096-ecb6c8ae61ea",
"name": "Send WhatsApp Reply",
"type": "n8n-nodes-base.httpRequest",
"position": [
4352,
448
],
"parameters": {
"url": "https://graph.facebook.com/v19.0/YOUR_PHONE_NUMBER_ID/messages",
"method": "POST",
"options": {},
"jsonBody": "={\"messaging_product\": \"whatsapp\", \"recipient_type\": \"individual\", \"to\": \"{{ $json.phoneNumber }}\", \"type\": \"text\", \"text\": {\"preview_url\": false, \"body\": \"{{ $json.replyMessage }}\"}}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "Bearer YOUR_TOKEN_HERE"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "fb35518a-4cf5-41be-9601-06a6e42e0b86",
"name": "Log to Activity Sheet",
"type": "n8n-nodes-base.httpRequest",
"position": [
4352,
624
],
"parameters": {
"url": "https://sheets.googleapis.com/v4/spreadsheets/YOUR_SHEET_ID/values/ActivityLog!A1:append?valueInputOption=USER_ENTERED",
"method": "POST",
"options": {},
"jsonBody": "={\"values\": [[\"{{ $json.timestamp }}\", \"{{ $json.phoneNumber }}\", \"{{ $json.customerName }}\", \"{{ $json.intent }}\", \"{{ $json.finalStatus }}\", \"{{ $json.appointmentId }}\", \"{{ $json.requestedDate }}\", \"{{ $json.requestedTime }}\", \"{{ $json.service }}\", \"{{ $json.messageText }}\"]]}",
"sendBody": true,
"specifyBody": "json",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "googleSheetsOAuth2Api"
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "9298ba4c-8345-4fce-9949-9a53f9ca486f",
"name": "Respond to WhatsApp 200 OK",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
4576,
448
],
"parameters": {
"options": {
"responseCode": 200
},
"respondWith": "json",
"responseBody": "{\"status\": \"ok\"}"
},
"typeVersion": 1.1
},
{
"id": "92a75949-1c67-47e4-83d1-b8ad06e178c0",
"name": "Respond Verify Challenge",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
1488,
592
],
"parameters": {
"options": {
"responseCode": 200
},
"respondWith": "text",
"responseBody": "={{ $json.query['hub.challenge'] }}"
},
"typeVersion": 1.1
},
{
"id": "c8ffbb8f-8a0d-48df-8d6e-bd971cf86bf5",
"name": "Wait For Data",
"type": "n8n-nodes-base.wait",
"position": [
1392,
400
],
"parameters": {},
"typeVersion": 1.1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "775b76f3-236d-4640-9a7e-3c7e9bb8d229",
"connections": {
"Route - Book": {
"main": [
[
{
"node": "JS - Validate Book Slot",
"type": "main",
"index": 0
}
]
]
},
"Wait For Data": {
"main": [
[
{
"node": "Filter Valid Messages",
"type": "main",
"index": 0
}
]
]
},
"Route - Confirm": {
"main": [
[
{
"node": "JS - Validate Confirm Ref",
"type": "main",
"index": 0
}
]
]
},
"Route - Unknown": {
"main": [
[
{
"node": "JS - Handle Unknown Intent",
"type": "main",
"index": 0
}
]
]
},
"Route - Reschedule": {
"main": [
[
{
"node": "JS - Validate Reschedule Slot",
"type": "main",
"index": 0
}
]
]
},
"Send WhatsApp Reply": {
"main": [
[
{
"node": "Respond to WhatsApp 200 OK",
"type": "main",
"index": 0
}
]
]
},
"JS - Parse AI Output": {
"main": [
[
{
"node": "Route - Book",
"type": "main",
"index": 0
},
{
"node": "Route - Reschedule",
"type": "main",
"index": 0
},
{
"node": "Route - Confirm",
"type": "main",
"index": 0
},
{
"node": "Route - Unknown",
"type": "main",
"index": 0
}
]
]
},
"Filter Valid Messages": {
"main": [
[
{
"node": "AI - Detect Intent and Parse",
"type": "main",
"index": 0
}
]
]
},
"Anthropic Claude Model": {
"ai_languageModel": [
[
{
"node": "AI - Detect Intent and Parse",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"JS - Validate Book Slot": {
"main": [
[
{
"node": "Sheets - Create Appointment",
"type": "main",
"index": 0
}
]
]
},
"Prepare Message Context": {
"main": [
[
{
"node": "Wait For Data",
"type": "main",
"index": 0
}
]
]
},
"JS - Build Reply Message": {
"main": [
[
{
"node": "Wait - Rate Limit Buffer",
"type": "main",
"index": 0
}
]
]
},
"Wait - Rate Limit Buffer": {
"main": [
[
{
"node": "Send WhatsApp Reply",
"type": "main",
"index": 0
},
{
"node": "Log to Activity Sheet",
"type": "main",
"index": 0
}
]
]
},
"JS - Validate Confirm Ref": {
"main": [
[
{
"node": "Sheets - Confirm Appointment",
"type": "main",
"index": 0
}
]
]
},
"Webhook - WhatsApp Verify": {
"main": [
[
{
"node": "Respond Verify Challenge",
"type": "main",
"index": 0
}
]
]
},
"JS - Handle Unknown Intent": {
"main": [
[
{
"node": "JS - Build Reply Message",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Update Reschedule": {
"main": [
[
{
"node": "JS - Build Reply Message",
"type": "main",
"index": 0
}
]
]
},
"Webhook - WhatsApp Inbound": {
"main": [
[
{
"node": "Prepare Message Context",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Create Appointment": {
"main": [
[
{
"node": "JS - Build Reply Message",
"type": "main",
"index": 0
}
]
]
},
"AI - Detect Intent and Parse": {
"main": [
[
{
"node": "JS - Parse AI Output",
"type": "main",
"index": 0
}
]
]
},
"Sheets - Confirm Appointment": {
"main": [
[
{
"node": "JS - Build Reply Message",
"type": "main",
"index": 0
}
]
]
},
"JS - Validate Reschedule Slot": {
"main": [
[
{
"node": "Sheets - Update Reschedule",
"type": "main",
"index": 0
}
]
]
}
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
anthropicApigoogleSheetsOAuth2Api
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow receives WhatsApp messages via a Meta webhook, uses Anthropic Claude to detect booking intents and extract appointment details, writes updates to Google Sheets, and sends a confirmation or error reply back to the customer on WhatsApp. Receives incoming WhatsApp…
Source: https://n8n.io/workflows/16482/ — 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.
⏺ 🚀 How it works
WhatsApp AI Agent - Template. Uses redis, httpRequest, postgres, agent. Webhook trigger; 89 nodes.
Lead Pipeline v3.0. Uses httpRequest, agent, lmChatAnthropic, toolThink. Webhook trigger; 77 nodes.
What if AI didn't just write content—but actually thought about how to write it? This n8n workflow revolutionizes content creation by deploying multiple specialized AI agents that handle every aspect
Fully automates your service order pipeline from incoming booking to supplier confirmation — with built-in SLA enforcement and automatic escalation if a supplier goes silent. 📥 Receives orders via web