This workflow corresponds to n8n.io template #11605 — we link there as the canonical source.
This workflow follows the Agent → Datatable 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 →
{
"nodes": [
{
"id": "a1bb9718-a390-4b5f-8534-b8a973ce8695",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-3104,
1984
],
"parameters": {
"rule": {
"interval": [
{
"field": "minutes",
"minutesInterval": 1
}
]
}
},
"typeVersion": 1.2
},
{
"id": "33e9808c-510f-4b1c-883e-f0a569248ab7",
"name": "Append or update slot",
"type": "n8n-nodes-base.googleSheets",
"position": [
-2496,
1696
],
"parameters": {
"columns": {
"value": {
"day": "={{ $json.days }}",
"time": "={{ $json.time }}",
"service": "={{ $json.service }}"
},
"schema": [
{
"id": "service",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "service",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "time",
"type": "string",
"display": true,
"required": false,
"displayName": "time",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "day",
"type": "string",
"display": true,
"required": false,
"displayName": "day",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"service"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1HCl3CvMnzILIrcjnFK-AwLqWuKfMwezM1yXWkJ4KG9Q/edit#gid=0",
"cachedResultName": "slots"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1HCl3CvMnzILIrcjnFK-AwLqWuKfMwezM1yXWkJ4KG9Q",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1HCl3CvMnzILIrcjnFK-AwLqWuKfMwezM1yXWkJ4KG9Q/edit?usp=drivesdk",
"cachedResultName": "CRM"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "fdd67b6a-4301-4fff-bf04-5fd448f768db",
"name": "Get lead",
"type": "n8n-nodes-base.dataTable",
"position": [
-2704,
1888
],
"parameters": {
"operation": "get",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "16bAoJicIDEPak46",
"cachedResultUrl": "/projects/0WLzulivX2ChGmk8/datatables/16bAoJicIDEPak46",
"cachedResultName": "leads"
}
},
"typeVersion": 1
},
{
"id": "a96ea4f0-f1c0-42c3-813b-7fc6ac28ac0d",
"name": "Append or update lead",
"type": "n8n-nodes-base.googleSheets",
"position": [
-2496,
1888
],
"parameters": {
"columns": {
"value": {
"name": "={{ $json.name }}",
"phone": "={{ $json.phone }}",
"short sum": "={{ $json.short_sum }}",
"lead category": "={{ $json.lead_category }}"
},
"schema": [
{
"id": "name",
"type": "string",
"display": true,
"required": false,
"displayName": "name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "lead category",
"type": "string",
"display": true,
"required": false,
"displayName": "lead category",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "phone",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "phone",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "short sum",
"type": "string",
"display": true,
"required": false,
"displayName": "short sum",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"phone"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1596543480,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1HCl3CvMnzILIrcjnFK-AwLqWuKfMwezM1yXWkJ4KG9Q/edit#gid=1596543480",
"cachedResultName": "leads"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1HCl3CvMnzILIrcjnFK-AwLqWuKfMwezM1yXWkJ4KG9Q",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1HCl3CvMnzILIrcjnFK-AwLqWuKfMwezM1yXWkJ4KG9Q/edit?usp=drivesdk",
"cachedResultName": "CRM"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "8bf64824-9d63-4c35-a9ff-0a23fd927bb7",
"name": "Get human call",
"type": "n8n-nodes-base.dataTable",
"position": [
-2704,
2064
],
"parameters": {
"operation": "get",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "ygicufEiNou6tVCb",
"cachedResultUrl": "/projects/0WLzulivX2ChGmk8/datatables/ygicufEiNou6tVCb",
"cachedResultName": "human"
}
},
"typeVersion": 1
},
{
"id": "aa6042af-187c-4326-bfe5-d46e783acdfa",
"name": "Append or update human call",
"type": "n8n-nodes-base.googleSheets",
"position": [
-2496,
2064
],
"parameters": {
"columns": {
"value": {
"name": "={{ $json.name }}",
"phone": "={{ $json.phone }}",
"description": "={{ $json.descreption }}"
},
"schema": [
{
"id": "name",
"type": "string",
"display": true,
"required": false,
"displayName": "name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "phone",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "phone",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "description",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "description",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"phone"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 292004918,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1HCl3CvMnzILIrcjnFK-AwLqWuKfMwezM1yXWkJ4KG9Q/edit#gid=292004918",
"cachedResultName": "human call"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1HCl3CvMnzILIrcjnFK-AwLqWuKfMwezM1yXWkJ4KG9Q",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1HCl3CvMnzILIrcjnFK-AwLqWuKfMwezM1yXWkJ4KG9Q/edit?usp=drivesdk",
"cachedResultName": "CRM"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "9f4c7fec-7444-4b95-aa2c-0cfc70903c1c",
"name": "Get human appointment",
"type": "n8n-nodes-base.dataTable",
"position": [
-2688,
2240
],
"parameters": {
"operation": "get",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "XICUlFpXl76eV2Dm",
"cachedResultUrl": "/projects/0WLzulivX2ChGmk8/datatables/XICUlFpXl76eV2Dm",
"cachedResultName": "appointments"
}
},
"typeVersion": 1
},
{
"id": "9ae86536-5815-4b76-bc4d-e1371866bcda",
"name": "Append or update appointment",
"type": "n8n-nodes-base.googleSheets",
"position": [
-2496,
2240
],
"parameters": {
"columns": {
"value": {
"day": "={{ $json.day }}",
"date": "={{ $json.date }}",
"name": "={{ $json.name }}",
"time": "={{ $json.times }}",
"phone": "={{ $json.phone }}",
"service": "={{ $json.service }}"
},
"schema": [
{
"id": "name",
"type": "string",
"display": true,
"required": false,
"displayName": "name",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "phone",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "phone",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "date",
"type": "string",
"display": true,
"required": false,
"displayName": "date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "time",
"type": "string",
"display": true,
"required": false,
"displayName": "time",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "day",
"type": "string",
"display": true,
"required": false,
"displayName": "day",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "service",
"type": "string",
"display": true,
"required": false,
"displayName": "service",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"phone"
],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": 1977548840,
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1HCl3CvMnzILIrcjnFK-AwLqWuKfMwezM1yXWkJ4KG9Q/edit#gid=1977548840",
"cachedResultName": "appointments"
},
"documentId": {
"__rl": true,
"mode": "list",
"value": "1HCl3CvMnzILIrcjnFK-AwLqWuKfMwezM1yXWkJ4KG9Q",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1HCl3CvMnzILIrcjnFK-AwLqWuKfMwezM1yXWkJ4KG9Q/edit?usp=drivesdk",
"cachedResultName": "CRM"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "7685907b-6ffe-43f2-8963-eac68e4ff6f4",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"disabled": true,
"position": [
-3248,
1456
],
"parameters": {
"color": 7,
"width": 1296,
"height": 1168,
"content": "## SENDING new rows to google sheet"
},
"typeVersion": 1
},
{
"id": "5577ae1c-5d66-4905-8fa3-71d4d7d2e283",
"name": "Booked Appointment",
"type": "n8n-nodes-base.dataTableTool",
"position": [
-864,
880
],
"parameters": {
"operation": "get",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "XICUlFpXl76eV2Dm",
"cachedResultUrl": "/projects/0WLzulivX2ChGmk8/datatables/XICUlFpXl76eV2Dm",
"cachedResultName": "appointments"
}
},
"typeVersion": 1
},
{
"id": "1e612891-e43a-4153-8098-22f0326a3883",
"name": "AI Summarizer1",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
128,
1264
],
"parameters": {
"text": "={{ $('Format Context').item.json.formatted_output }}",
"options": {
"systemMessage": "=# Customer Message Analyzer\n\nAnalyze customer messages and return only JSON:\n```json\n{\"summary\": \"brief description\", \"category\": \"category_name\"}\n```\n\n## Categories\n- **appointment**: booking request\n- **pricing**: price inquiry\n- **discount**: discount request\n- **teeth_whitening**: whitening inquiry\n- **skincare**: skincare inquiry\n- **staff_request**: staff contact request\n- **out_of_scope**: unrelated topic\n\n## Instructions\n- Summary: one clear sentence describing the customer's request\n- Output: JSON only, no additional text\n- answer with the user's language "
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 2.2
},
{
"id": "c0e92bbc-42cb-4a40-b403-27150e290bb2",
"name": "Check Appointment1",
"type": "n8n-nodes-base.if",
"position": [
304,
352
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "1",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.has_appointment }}",
"rightValue": "YES"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "98e8a42a-9e2c-4468-87f4-8f7b441a94d3",
"name": "AI Data Extractor",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-208,
352
],
"parameters": {
"text": "={{ $json.output }}",
"options": {
"systemMessage": "=// System Prompt for AI Data Extractor Node\n-- today is {{ $now }}\nYou are a precise data extractor. Extract information in JSON format ONLY.\n-- **if there was no date** return the correct date of the nearest day that was chosen. \n\u26a0\ufe0f Return ONLY valid JSON, no markdown or extra text.\n\n\ud83d\udccb Required Format:\n{\n \"has_appointment\": true or false,\n \"is_reschedule\": true or false,\n \"needs_human\": true or false,\n \"appointment_date\": \"YYYY-MM-DD\" or null,\n \"appointment_day\": \"\u0627\u0644\u0623\u062d\u062f/\u0627\u0644\u0627\u062b\u0646\u064a\u0646/etc\" or null,\n \"appointment_time\": \"HH:MM\" or null,\n \"service\": \"service name in Arabic\" or null,\n \"lead_category\": \"only one of those booking/reschedule/pricing/discount/teeth_whitening/skin_care/ask_agent/out_of_scope/other\",\n \"description\": \"brief description in Arabic\",\n \"notes\": \"additional notes\" or null\n}\n\n\ud83d\udcdd Rules:\n**has_appointment:**\n- true if booking confirmed: \"\u062a\u0645 \u062d\u062c\u0632 \u0645\u0648\u0639\u062f\u0643\" OR \"\u062a\u0645 \u062a\u0639\u062f\u064a\u0644 \u0645\u0648\u0639\u062f\u0643\"\n- false otherwise\n\n**is_reschedule:**\n- true if customer changed existing appointment\n- false for new bookings\n\n**needs_human:**\n\u26a0\ufe0f BE VERY STRICT - only true if:\n- EXPLICIT request: \"\u0628\u062f\u064a \u0645\u0648\u0638\u0641\" / \"\u0648\u062f\u064a\u0646\u064a \u0639\u0644\u0649 \u062d\u062f\u0627\" / \"\u0628\u062f\u064a \u0623\u062d\u0643\u064a \u0645\u0639 \u0634\u062e\u0635\" / \"\u0627\u062a\u0635\u0644 \u0641\u064a\u0646\u064a\"\n- Clear anger/frustration: cursing, multiple complaints, \"\u0645\u0634 \u0631\u0627\u0636\u064a\" / \"\u0632\u0647\u0642\u062a\"\n- Medical emergency: \"\u0648\u062c\u0639 \u0634\u062f\u064a\u062f\" / \"\u062d\u0627\u0644\u0629 \u0637\u0627\u0631\u0626\u0629\" / \"\u0645\u0631\u064a\u0636 \u0643\u062b\u064a\u0631\"\n- Bot explicitly failed multiple times\n\n\u26d4 DO NOT set true for:\n- Simple questions about services\n- Asking about prices or discounts\n- General inquiries (\"\u0634\u0648 \u0639\u0646\u062f\u0643\u0645\u061f\" / \"\u0643\u064a\u0641\u061f\")\n- Booking/rescheduling requests\n- Normal conversation\n\n**appointment_date/day/time/service:**\n- Extract as before\n- null if not mentioned\n\n**lead_category:**\n- \"reschedule\": if modifying existing appointment\n- \"booking\": new appointment booking\n- \"pricing\": price inquiry\n- \"discount\": discount request\n- \"teeth_whitening\": teeth whitening inquiry\n- \"skin_care\": skin care inquiry\n- \"ask_agent\": wants to speak with staff (ONLY if explicit request)\n- \"out_of_scope\": outside clinic specialty\n- \"other\": anything else\n\n**description:**\n- One sentence in Arabic\n- Examples:\n - \"\u0627\u0644\u0639\u0645\u064a\u0644 \u0642\u0627\u0645 \u0628\u062a\u0639\u062f\u064a\u0644 \u0645\u0648\u0639\u062f\u0647 \u0644\u062a\u0628\u064a\u064a\u0636 \u0627\u0644\u0623\u0633\u0646\u0627\u0646\"\n - \"\u0637\u0644\u0628 \u062a\u063a\u064a\u064a\u0631 \u0627\u0644\u0645\u0648\u0639\u062f \u0645\u0646 \u0627\u0644\u0623\u062d\u062f \u0625\u0644\u0649 \u0627\u0644\u062b\u0644\u0627\u062b\u0627\u0621\"\n - \"\u0637\u0644\u0628 \u0627\u0644\u062a\u062d\u062f\u062b \u0645\u0639 \u0645\u0648\u0638\u0641 \u0628\u0634\u0643\u0644 \u0635\u0631\u064a\u062d\"\n\n\ud83c\udfaf Example - Normal Question (needs_human = FALSE):\nUser: \"\u0634\u0648 \u0639\u0646\u062f\u0643\u0645 \u062e\u062f\u0645\u0627\u062a\u061f\"\n{\n \"has_appointment\": false,\n \"is_reschedule\": false,\n \"needs_human\": false,\n \"appointment_date\": null,\n \"appointment_day\": null,\n \"appointment_time\": null,\n \"service\": null,\n \"lead_category\": \"other\",\n \"description\": \"\u0627\u0633\u062a\u0641\u0633\u0627\u0631 \u0639\u0627\u0645 \u0639\u0646 \u0627\u0644\u062e\u062f\u0645\u0627\u062a\",\n \"notes\": null\n}\n\n\ud83c\udfaf Example - Explicit Human Request (needs_human = TRUE):\nUser: \"\u0628\u062f\u064a \u0623\u062d\u0643\u064a \u0645\u0639 \u0645\u0648\u0638\u0641 \u0645\u0634 \u0645\u0639\u0643\"\n{\n \"has_appointment\": false,\n \"is_reschedule\": false,\n \"needs_human\": true,\n \"appointment_date\": null,\n \"appointment_day\": null,\n \"appointment_time\": null,\n \"service\": null,\n \"lead_category\": \"ask_agent\",\n \"description\": \"\u0637\u0644\u0628 \u0635\u0631\u064a\u062d \u0644\u0644\u062a\u062d\u062f\u062b \u0645\u0639 \u0645\u0648\u0638\u0641\",\n \"notes\": \"\u064a\u062d\u062a\u0627\u062c \u062a\u062f\u062e\u0644 \u0628\u0634\u0631\u064a \u0641\u0648\u0631\u064a\"\n}\n\n\ud83c\udfaf Example - Frustration (needs_human = TRUE):\nUser: \"\u064a\u0627 \u0632\u0644\u0645\u0629 \u0645\u0634 \u0641\u0627\u0647\u0645 \u0639\u0644\u064a\u0643\u060c \u0648\u062f\u064a\u0646\u064a \u0639\u0644\u0649 \u062d\u062f\u0627 \u0628\u0641\u0647\u0645\"\n{\n \"has_appointment\": false,\n \"is_reschedule\": false,\n \"needs_human\": true,\n \"appointment_date\": null,\n \"appointment_day\": null,\n \"appointment_time\": null,\n \"service\": null,\n \"lead_category\": \"ask_agent\",\n \"description\": \"\u0625\u062d\u0628\u0627\u0637 \u0648\u0637\u0644\u0628 \u0627\u0644\u062a\u062d\u062f\u062b \u0645\u0639 \u0634\u062e\u0635\",\n \"notes\": \"\u0639\u062f\u0645 \u0631\u0636\u0627 \u0648\u0627\u0636\u062d\"\n}"
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 2.2
},
{
"id": "56670131-4402-4ae7-ad14-22f67064e4b6",
"name": "Send via WhatsApp",
"type": "n8n-nodes-base.httpRequest",
"position": [
-80,
48
],
"parameters": {
"url": "=https://graph.facebook.com/v22.0/{{ $('Format Context').item.json.wa_id }}/messages",
"method": "POST",
"options": {},
"jsonBody": "={\n \"messaging_product\": \"whatsapp\",\n \"to\": \"{{ $('Format Context').item.json.sender }}\",\n \"type\": \"text\",\n \"text\":{ \"body\":{{ JSON.stringify($json.output) }}}\n}",
"sendBody": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4
},
{
"id": "44b6b15f-d026-4710-8e2a-0878df5a6aa6",
"name": "Pinecone Vector Store",
"type": "@n8n/n8n-nodes-langchain.vectorStorePinecone",
"position": [
-1328,
2016
],
"parameters": {
"mode": "insert",
"options": {
"pineconeNamespace": "1"
},
"pineconeIndex": {
"__rl": true,
"mode": "list",
"value": "n8n",
"cachedResultName": "n8n"
}
},
"typeVersion": 1
},
{
"id": "4bdb5738-0c36-40cb-ae81-315d54320894",
"name": "Default Data Loader",
"type": "@n8n/n8n-nodes-langchain.documentDefaultDataLoader",
"position": [
-1168,
2240
],
"parameters": {
"options": {}
},
"typeVersion": 1
},
{
"id": "d31374e1-7546-4dbb-8df8-50e88ae64338",
"name": "Recursive Character Text Splitter",
"type": "@n8n/n8n-nodes-langchain.textSplitterRecursiveCharacterTextSplitter",
"position": [
-1184,
2448
],
"parameters": {
"options": {},
"chunkOverlap": 100
},
"typeVersion": 1
},
{
"id": "1d149211-15e0-4925-aeb7-5a946fbe24ce",
"name": "Get a document",
"type": "n8n-nodes-base.googleDocs",
"position": [
-1552,
2016
],
"parameters": {
"operation": "get",
"documentURL": "=https://docs.google.com/document/d/1M2VXsD_gLRDbRv8VU2S2IfnCkPl5NaPqX_QulHV-050/edit?tab=t.0"
},
"typeVersion": 2
},
{
"id": "07700abc-c2bd-44a1-8188-3d51a4022d9e",
"name": "When clicking \u2018Execute workflow\u2019",
"type": "n8n-nodes-base.manualTrigger",
"position": [
-1744,
2016
],
"parameters": {},
"typeVersion": 1
},
{
"id": "80e81e5e-e2f0-4bbc-80dc-02dbf0b2bca4",
"name": "Embeddings OpenAI",
"type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
"position": [
-1504,
2256
],
"parameters": {
"options": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "ffa2a0af-0af3-4466-a009-2ab4522bb72e",
"name": "Vector Store Tool",
"type": "@n8n/n8n-nodes-langchain.toolVectorStore",
"position": [
-1248,
976
],
"parameters": {
"name": "company_documents_tool",
"description": "Retrieve information from any company documents"
},
"typeVersion": 1
},
{
"id": "80bd0f37-5df9-4430-adb8-253585287dbb",
"name": "Pinecone Vector Store (Retrieval)",
"type": "@n8n/n8n-nodes-langchain.vectorStorePinecone",
"position": [
-1344,
1152
],
"parameters": {
"options": {},
"pineconeIndex": {
"__rl": true,
"mode": "list",
"value": "clinic",
"cachedResultName": "clinic"
}
},
"typeVersion": 1
},
{
"id": "e60712a2-3949-4719-8ac1-a4b218f1f192",
"name": "Embeddings OpenAI1",
"type": "@n8n/n8n-nodes-langchain.embeddingsOpenAi",
"position": [
-1280,
1376
],
"parameters": {
"options": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.2
},
{
"id": "6b947bca-a0ec-486e-b483-f9e239987032",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1888,
1952
],
"parameters": {
"color": 7,
"width": 1024,
"height": 656,
"content": "## RAG System\n"
},
"typeVersion": 1
},
{
"id": "e78bf1f5-33f0-409a-901d-d7f8257e32ae",
"name": "Verify \u2022 WA (hub.challenge)",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
-3824,
272
],
"parameters": {
"options": {
"responseCode": 200,
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "text/plain"
}
]
}
},
"respondWith": "text",
"responseBody": "={{ $json.query['hub.challenge'] || 'OK' }}"
},
"typeVersion": 1.4
},
{
"id": "e13b4e56-2078-4a94-8b6a-c3e4cd1080f5",
"name": "filter text messages",
"type": "n8n-nodes-base.if",
"position": [
-3136,
256
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "1",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.body.entry[0].changes[0].value.messages[0].type }}",
"rightValue": "text"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "f43e83f8-161b-4231-9dbc-79962676b2d5",
"name": "If Audio",
"type": "n8n-nodes-base.if",
"position": [
-3552,
464
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "1",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.body.entry[0].changes[0].value.messages[0].type }}",
"rightValue": "audio"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "13e1d14a-d5ed-4d42-b437-ff3de78ed653",
"name": "Get Audio",
"type": "n8n-nodes-base.httpRequest",
"position": [
-3328,
448
],
"parameters": {
"url": "=https://graph.facebook.com/v22.0/{{ $('WhatsApp Webhook').item.json.body.entry[0].changes[0].value.messages[0].audio.id }}",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "8e96d4bb-2210-44c6-a28d-6bcb780bf1f9",
"name": "Download Audio",
"type": "n8n-nodes-base.httpRequest",
"position": [
-3136,
448
],
"parameters": {
"url": "={{$json.url}}",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "1da24395-e5e2-4b21-8629-11a63baab66f",
"name": "Transcribe Audio",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
-2928,
448
],
"parameters": {
"options": {
"language": "ar",
"temperature": 0.6
},
"resource": "audio",
"operation": "transcribe"
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.8
},
{
"id": "1ebf7b18-abb1-44cf-8638-36dca9cf28c5",
"name": "If Image",
"type": "n8n-nodes-base.if",
"position": [
-3536,
704
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "1",
"operator": {
"type": "string",
"operation": "contains"
},
"leftValue": "={{ $json.body.entry[0].changes[0].value.messages[0].image.mime_type }}",
"rightValue": "image"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "2105b13a-d511-4626-bc32-019d94e63749",
"name": "If Video/PDF",
"type": "n8n-nodes-base.if",
"position": [
-3520,
880
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "or",
"conditions": [
{
"id": "1",
"operator": {
"type": "string",
"operation": "contains"
},
"leftValue": "={{ $json.body.entry[0].changes[0].value.messages[0].document.mime_type }}",
"rightValue": "application"
},
{
"id": "a584f702-7224-400b-85ff-eeff8eca35b3",
"operator": {
"type": "string",
"operation": "contains"
},
"leftValue": "={{ $json.body.entry[0].changes[0].value.messages[0].document.mime_type }}",
"rightValue": "video"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "0d717c38-68da-47a7-a79f-7caac828e8aa",
"name": "Get Image",
"type": "n8n-nodes-base.httpRequest",
"position": [
-3296,
688
],
"parameters": {
"url": "=https://graph.facebook.com/v22.0/{{ $('WhatsApp Webhook').item.json.body.entry[0].changes[0].value.messages[0].image.id }}",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "1f019d11-f137-4fce-983e-8940de919d2b",
"name": "Download Image",
"type": "n8n-nodes-base.httpRequest",
"position": [
-3072,
688
],
"parameters": {
"url": "={{$json.url}}",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "4c2cf8bd-065a-4320-8e79-770d7cc9e4c2",
"name": "Get file",
"type": "n8n-nodes-base.httpRequest",
"position": [
-3296,
864
],
"parameters": {
"url": "=https://graph.facebook.com/v22.0/{{ $('WhatsApp Webhook').item.json.body.entry[0].changes[0].value.messages[0].document.id }}",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpBearerAuth": {
"name": "<your credential>"
}
},
"typeVersion": 4.2
},
{
"id": "b6ebf3af-7914-4962-84d5-a883507b3111",
"name": "Download file",
"type": "n8n-nodes-base.httpRequest",
"position": [
-3088,
864
],
"parameters": {
"url": "={{$json.url}}",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "=httpBearerAuth"
},
"typeVersion": 4.2
},
{
"id": "2d8c8b15-7149-4904-8368-b2c04423001b",
"name": "Prepare Email With Attachment",
"type": "n8n-nodes-base.code",
"position": [
-2864,
864
],
"parameters": {
"jsCode": "// \u0627\u0642\u0631\u0623 \u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0648\u0650\u0628\u0647\u0648\u0643 \u0648\u0627\u0644\u0640 input\nconst webhookData = $('WhatsApp Webhook').first().json;\nconst firstItem = $input.first();\n\nconst name = webhookData.body?.entry?.[0]?.changes?.[0]?.value?.contacts?.[0]?.profile?.name || \"\";\nconst phone = webhookData.body?.entry?.[0]?.changes?.[0]?.value?.contacts?.[0]?.wa_id || firstItem.json.phone || \"\";\n\n// \u0644\u0648 \u0641\u064a\u0647 binary \u062c\u0627\u0647\u0632 \u0645\u0646 \u0639\u0642\u062f\u0629 \u0633\u0627\u0628\u0642\u0629\u060c \u062e\u0630\u0647\nconst bin = firstItem.binary || {};\nconst binaryKey = Object.keys(bin)[0] || \"\"; // \u0645\u062b\u0627\u0644: \"file\" \u0623\u0648 \"data\"\nif (!name || !phone || !binaryKey) {\n throw new Error(\"\u0645\u0637\u0644\u0648\u0628 name \u0648 phone \u0648 \u0645\u0644\u0641 binary \u0645\u0646 \u0627\u0644\u0639\u0642\u062f\u0629 \u0627\u0644\u0633\u0627\u0628\u0642\u0629.\");\n}\n\nconst subject = \"\u0625\u0634\u0639\u0627\u0631 \u062a\u0648\u0627\u0635\u0644 \u062c\u062f\u064a\u062f\";\nconst html = `\n<!doctype html>\n<html lang=\"ar\" dir=\"rtl\">\n<head>\n <meta charset=\"utf-8\"><title>${subject}</title>\n <style>\n body { background:#e8f3ff; direction:rtl; text-align:right; font-family:'Segoe UI',Roboto,Helvetica,Arial,sans-serif; margin:0; padding:0; }\n .container { max-width:600px; margin:24px auto; background:#fff; border-radius:12px; overflow:hidden; box-shadow:0 4px 16px rgba(13,71,161,0.15); }\n .header { background:linear-gradient(135deg,#0D47A1,#00B894); padding:20px; color:#fff; }\n .content { padding:24px; }\n .row { margin:0 0 12px 0; }\n .label { color:#0D47A1; font-weight:600; }\n .footer { background:#f7fafc; padding:16px; font-size:12px; color:#555; border-top:1px solid #e9eef8; }\n .brand { color:#0D47A1; font-weight:600; }\n </style>\n</head>\n<body>\n <div class=\"container\">\n <div class=\"header\">\n <h2 style=\"margin:0;\">${subject}</h2>\n <p style=\"margin:6px 0 0 0;\">\u0644\u0642\u062f \u062a\u0645 \u0627\u0633\u062a\u0644\u0627\u0645 \u0637\u0644\u0628 \u062c\u062f\u064a\u062f \u0645\u0646 \u0645\u0648\u0642\u0639\u0643</p>\n </div>\n <div class=\"content\">\n <p class=\"row\"><span class=\"label\">\u0627\u0644\u0627\u0633\u0645:</span> ${name}</p>\n <p class=\"row\"><span class=\"label\">\u0631\u0642\u0645 \u0627\u0644\u0647\u0627\u062a\u0641:</span> ${phone}</p>\n <p class=\"row\">\u0627\u0644\u0645\u0631\u0641\u0642 \u062a\u0645 \u0625\u0631\u0641\u0627\u0642\u0647 \u0645\u0639 \u0647\u0630\u0627 \u0627\u0644\u0628\u0631\u064a\u062f.</p>\n </div>\n <div class=\"footer\">\n \u062a\u0645 \u0627\u0644\u0625\u0631\u0633\u0627\u0644 \u0628\u0648\u0627\u0633\u0637\u0629 <span class=\"brand\">\u062a\u0648\u0627\u0635\u0644 \u0627\u0644\u0639\u064a\u0627\u062f\u0627\u062a</span>\n </div>\n </div>\n</body>\n</html>\n`;\n\n// \u0631\u062c\u0651\u0639 \u0646\u0641\u0633 \u0627\u0644\u0640 binary \u062d\u062a\u0649 \u064a\u0644\u062a\u0642\u0637\u0647 Email node \u0643\u0645\u0631\u0641\u0642\nreturn [\n {\n json: { subject, html },\n binary: { [binaryKey]: bin[binaryKey] },\n },\n];\n"
},
"typeVersion": 2
},
{
"id": "f197f696-3c15-4bdf-aa64-15a87523b055",
"name": "Send Email with attachment",
"type": "n8n-nodes-base.gmail",
"position": [
-2640,
864
],
"parameters": {
"sendTo": "user@example.com",
"message": "={{ $json.html }}",
"options": {
"attachmentsUi": {
"attachmentsBinary": [
{}
]
}
},
"subject": "\u0647\u0646\u0627\u0643 \u0639\u0645\u064a\u0644 \u0623\u0631\u0633\u0644 \u0645\u0644\u0641 "
},
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
},
"typeVersion": 2.1
},
{
"id": "49ed334f-d0dd-43ac-90aa-0ad08d9f6ead",
"name": "Edit Fields",
"type": "n8n-nodes-base.set",
"position": [
-2640,
688
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "05294dc6-587a-4631-b03a-395ffad76a89",
"name": "text",
"type": "string",
"value": "=image description: {{ $json.content }}\nuser's message: {{ $('WhatsApp Webhook').item.json.body.entry[0].changes[0].value.messages[0].image.caption }}"
}
]
}
},
"typeVersion": 3.4
},
{
"id": "4055776e-6283-472a-bfbc-374ad20cc555",
"name": "Analyze image",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
-2848,
688
],
"parameters": {
"text": "Here is an image sent by the user. Describe the image and transcribe any text visible in the image.",
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-4o-mini",
"cachedResultName": "GPT-4O-MINI"
},
"options": {
"maxTokens": 50
},
"resource": "image",
"inputType": "=base64",
"operation": "analyze"
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.8
},
{
"id": "98fa0d1d-cea9-4f2f-a8dc-4cd997ebb1ce",
"name": "Prepare Text Data",
"type": "n8n-nodes-base.code",
"position": [
-2112,
576
],
"parameters": {
"jsCode": "const items = $input.all();\n\n// Get phone number from webhook\nconst phoneNumber = $('WhatsApp Webhook').first().json.body.entry[0].changes[0].value.messages[0].from || '';\n\n// Try to get text from different sources\nlet textFromWhisper;\nlet textFromWebhook;\nlet textFromImage;\n\n// 1. Try to get transcribed voice from Whisper\ntry {\n textFromWhisper = $('Transcribe Audio').first().json.text;\n console.log('Voice text found:', textFromWhisper);\n} catch (e) {\n textFromWhisper = undefined;\n}\n\n// 2. Try to get regular text message from webhook\ntry {\n textFromWebhook = $('WhatsApp Webhook').first().json.body.entry[0].changes[0].value.messages[0].text.body;\n console.log('Text message found:', textFromWebhook);\n} catch (e) {\n textFromWebhook = undefined;\n}\n\n// 3. Try to get image analysis from Edit Fields node\ntry {\n textFromImage = $('Edit Fields').first().json.text;\n console.log('Image analysis found:', textFromImage);\n} catch (e) {\n textFromImage = undefined;\n}\n\n// Combine all available text sources\n// Priority: voice > image > text message\nlet finalText = textFromWhisper || textFromImage || textFromWebhook || '';\n\n// Add source indicator for debugging\nlet source = '';\nif (textFromWhisper) {\n source = 'voice';\n} else if (textFromImage) {\n source = 'image';\n} else if (textFromWebhook) {\n source = 'text';\n}\n\nconsole.log('Final text source:', source);\nconsole.log('Final text:', finalText);\n\nreturn items.map(item => {\n return {\n json: {\n phone: phoneNumber,\n text: finalText,\n source: source, // \u0644\u0644\u0645\u0639\u0631\u0641\u0629 \u0645\u0646 \u0648\u064a\u0646 \u062c\u0627\u064a \u0627\u0644\u0646\u0635\n timestamp: new Date().toISOString()\n }\n };\n});"
},
"typeVersion": 2
},
{
"id": "4f37fbd4-21f2-4f31-ae58-8774cf8c21bc",
"name": "Insert Row Memory",
"type": "n8n-nodes-base.dataTable",
"position": [
-1920,
576
],
"parameters": {
"columns": {
"value": {
"phone": "={{ $('WhatsApp Webhook').item.json.body.entry[0].changes[0].value.contacts[0].wa_id }}",
"sender": "={{ $json.text }}"
},
"schema": [
{
"id": "sender",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "sender",
"defaultMatch": false
},
{
"id": "reciever",
"type": "string",
"display": true,
"removed": true,
"readOnly": false,
"required": false,
"displayName": "reciever",
"defaultMatch": false
},
{
"id": "phone",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "phone",
"defaultMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "l6biIqkSE1qLJvaZ",
"cachedResultUrl": "/projects/0WLzulivX2ChGmk8/datatables/l6biIqkSE1qLJvaZ",
"cachedResultName": "test table"
}
},
"executeOnce": true,
"typeVersion": 1,
"alwaysOutputData": true
},
{
"id": "3ed7200b-b2d4-490c-b26d-92fbebc6c607",
"name": "Get History",
"type": "n8n-nodes-base.dataTable",
"position": [
-1728,
576
],
"parameters": {
"filters": {
"conditions": [
{
"keyName": "phone",
"keyValue": "={{ $('Prepare Text Data').item.json.phone }}"
}
]
},
"operation": "get",
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "l6biIqkSE1qLJvaZ",
"cachedResultUrl": "/projects/0WLzulivX2ChGmk8/datatables/l6biIqkSE1qLJvaZ",
"cachedResultName": "test table"
}
},
"executeOnce": true,
"typeVersion": 1,
"alwaysOutputData": true
},
{
"id": "5f0569e7-dde9-41ed-9b16-544a7b7013b8",
"name": "Format Context",
"type": "n8n-nodes-base.code",
"position": [
-1360,
576
],
"parameters": {
"jsCode": "// \u0627\u0633\u062a\u062e\u0631\u0627\u062c \u0627\u0644\u0628\u064a\u0627\u0646\u0627\u062a \u0627\u0644\u0623\u0633\u0627\u0633\u064a\u0629\nconst jsText = $('Prepare Text Data').first().json.text || '';\nconst phoneNumber = $('Prepare Text Data').first().json.phone || '';\nconst wa_id = $('WhatsApp Webhook').item.json.body.entry[0].changes[0].value.metadata.phone_number_id;\nconst name = $('WhatsApp Webhook').first().json.body.entry[0].changes[0].value.contacts[0].profile.name;\n\n// Get available slots\nlet availableSlots = [];\ntry {\n const slotsData = $('Get Slots1').all();\n console.log('Get Slots1 data:', JSON.stringify(slotsData.map(s => s.json)));\n \n availableSlots = slotsData.map(slot => {\n console.log('Slot structure:', Object.keys(slot.json));\n \n const date = slot.json['\u0627\u0644\u062a\u0627\u0631\u064a\u062e (A)'] || slot.json.date || slot.json.\u0627\u0644\u062a\u0627\u0631\u064a\u062e || '';\n const time = slot.json['\u0627\u0644\u0648\u0642\u062a (B)'] || slot.json.time || slot.json.\u0627\u0644\u0648\u0642\u062a || '';\n const service = slot.json['\u0627\u0644\u062e\u062f\u0645\u0629 (C)'] || slot.json.service || slot.json.\u0627\u0644\u062e\u062f\u0645\u0629 || '';\n \n return `${date} - ${time} (${service})`;\n });\n \n console.log('Available slots formatted:', availableSlots);\n} catch (e) {\n console.error('Error fetching slots:', e.message);\n availableSlots = [];\n}\n\n// Get existing appointments for this customer\nlet existingAppointments = [];\ntry {\n const appointmentsData = $('Get appointment1').all();\n console.log('Get History1 appointments:', appointmentsData.length);\n \n existingAppointments = appointmentsData.map(apt => {\n console.log('Appointment structure:', Object.keys(apt.json));\n \n const date = apt.json.date || '';\n const time = apt.json.times || apt.json.time || '';\n const service = apt.json.service || '';\n const name = apt.json.name || '';\n \n return `${date} at ${time} - ${service} (${name})`;\n });\n \n console.log('Existing appointments formatted:', existingAppointments);\n} catch (e) {\n console.error('Error fetching existing appointments:', e.message);\n existingAppointments = [];\n}\n\n// Format conversation history from Get History1\nlet formattedHistory = '';\nlet historyCount = 0;\ntry {\n const historyData = $('Get History').all().slice(0, 10);\n historyCount = historyData.length;\n console.log('Get History1 data:', historyCount);\n \n formattedHistory = historyData.map(entry => {\n const createdAt = entry.json.createdAt || '';\n const aiResponse = entry.json.reciever || entry.json.ai || '';\n const userMessage = entry.json.sender || entry.json.user || '';\n \n // Format timestamp as yyyy-mm-dd:HH:MM\n let formattedTime = '';\n if (createdAt) {\n const date = new Date(createdAt);\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, '0');\n const day = String(date.getDate()).padStart(2, '0');\n const hours = String(date.getHours()).padStart(2, '0');\n const minutes = String(date.getMinutes()).padStart(2, '0');\n formattedTime = `${year}-${month}-${day}:${hours}:${minutes}`;\n }\n \n return `( ${formattedTime}, ai: ${aiResponse}, user: ${userMessage})`;\n }).join('\\n');\n \n console.log('Formatted history:', formattedHistory);\n} catch (e) {\n console.error('Error formatting history:', e.message);\n formattedHistory = '';\n historyCount = 0;\n}\n\n// \u26a0\ufe0f FIX: Get ALL user appointments and format them\nlet userAppointments = '';\nlet userAppointmentsCount = 0;\ntry {\n const userAppts = $input.all(); // Get ALL appointments\n userAppointmentsCount = userAppts.length;\n \n if (userAppointmentsCount > 0) {\n userAppointments = userAppts.map((apt, index) => {\n const aptName = apt.json.name || '';\n const aptService = apt.json.service || '';\n const aptDate = apt.json.date ? apt.json.date.split('T')[0] : '';\n const aptDay = apt.json.day || '';\n const aptTime = apt.json.times || '';\n \n return `\n\u0645\u0648\u0639\u062f ${index + 1}:\n\u0627\u0644\u0627\u0633\u0645: ${aptName}\n\u0627\u0644\u062e\u062f\u0645\u0629: ${aptService}\n\ud83d\udcc5 \u0627\u0644\u062a\u0627\u0631\u064a\u062e: ${aptDate} | ${aptDay}\n\ud83d\udd50 \u0627\u0644\u0648\u0642\u062a: ${aptTime}`;\n }).join('\\n---\\n');\n } else {\n userAppointments = '\u0644\u0627 \u064a\u0648\u062c\u062f \u0645\u0648\u0627\u0639\u064a\u062f \u0645\u062d\u062c\u0648\u0632\u0629 \u062d\u0627\u0644\u064a\u0627\u064b';\n }\n \n console.log('User appointments formatted:', userAppointments);\n} catch (e) {\n console.error('Error fetching user appointments:', e.message);\n userAppointments = '\u0644\u0627 \u064a\u0648\u062c\u062f \u0645\u0648\u0627\u0639\u064a\u062f \u0645\u062d\u062c\u0648\u0632\u0629 \u062d\u0627\u0644\u064a\u0627\u064b';\n userAppointmentsCount = 0;\n}\nlet type = $('Prepare Text Data').first().json.source;\nlet users_id = $('Insert Row Memory').first().json.id;\n\nconst result = {\n sender: phoneNumber,\n text: jsText,\n available_slots: availableSlots.join('\\n'), \n available_slots_count: availableSlots.length,\n existing_appointments: existingAppointments.join('\\n'),\n existing_appointments_count: existingAppointments.length,\n formatted_output: formattedHistory,\n history_count: historyCount,\n name,\n wa_id,\n users_appointments: userAppointments, // \u2705 All appointments formatted\n users_appointments_count: userAppointmentsCount, // \u2705 Count of appointments\n users_id,\n type\n};\n\nconsole.log('Final output:', JSON.stringify(result, null, 2));\n\nreturn [\n {\n json: result\n }\n];\n"
},
"executeOnce": true,
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "ac272c74-a855-4a88-ae47-71b12ef39ab2",
"name": "Main AI Agent",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
-1120,
576
],
"parameters": {
"text": "={{ $json.text }}",
"options": {
"systemMessage": "=## Medical Clinic Bot\nToday: {{ $now }}\nUser: {{ $json.name }} ({{ $json.sender }})\n\n## Knowledge source:\n\n- Use the **Pinecone vector store** to answer anything about the clinic (services, policies, offers, location, doctors, instructions, etc.).\n-Always use this Memory/last conversations:\n{{ $json.formatted_output }}\n----\nSlots:\n{{ $json.available_slots }}\n-----\nAppointments:\n{{ $json.users_appointments }}\n-----\n\nRules: Arabic only, no markdown, verify data, 2-digit time (12=twelve), different services for multiple bookings\nImportant Rule:\n--- you do not invent dates, think twice before makeing up a date make sure it is correct \n\n-\u062a\u0642\u062f\u0645 \u062b\u0644\u0627\u062b \u062e\u062f\u0645\u0627\u062a, feller, \u0639\u0646\u0627\u064a\u0629 \u0628\u0627\u0644\u0628\u0634\u0631\u0629, \u062a\u0628\u064a\u064a\u0636 \u0623\u0633\u0646\u0627\u0646\n\nBooking: \u062a\u0645 \u062d\u062c\u0632 \u0645\u0648\u0639\u062f\u0643! \u2705 \ud83d\udcc5 [date]|[day] \ud83d\udd50 [time] \ud83d\udc89 [service]\nReschedule: \u062a\u0645 \u062a\u0639\u062f\u064a\u0644 \u0645\u0648\u0639\u062f\u0643 \ud83d\udcc5 [date]|[day] \ud83d\udd50 [time] \ud83d\udc89 [service]\n\n## IMPORTANT: if image is equal to {{ $json.type }} then answer with a short answer of the image and user message if existed"
},
"promptType": "=define",
"hasOutputParser": true
},
"executeOnce": true,
"typeVersion": 2.2
},
{
"id": "7ff93428-5292-47ec-8084-c56bd2ccd6ec",
"name": "Simple Memory",
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"position": [
-1264,
784
],
"parameters": {
"sessionKey": "={{ $json.sender }}",
"sessionIdType": "customKey",
"contextWindowLength": 1
},
"typeVersion": 1.3
},
{
"id": "bc33c7ae-22a5-417f-bc02-e4e59cc96eb6",
"name": "save appointment - different service",
"type": "n8n-nodes-base.dataTable",
"position": [
1024,
320
],
"parameters": {
"columns": {
"value": {
"day": "={{ $json.appointment_day }}",
"date": "={{ $json.appointment_date }}",
"name": "={{ $('Output Format Text').item.json.customer_name }}",
"phone": "={{ $json.phone }}",
"times": "={{ $json.appointment_time }}",
"service": "={{ $json.service }}"
},
"schema": [
{
"id": "name",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "name",
"defaultMatch": false
},
{
"id": "phone",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "phone",
"defaultMatch": false
},
{
"id": "date",
"type": "dateTime",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "date",
"defaultMatch": false
},
{
"id": "day",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "day",
"defaultMatch": false
},
{
"id": "service",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "service",
"defaultMatch": false
},
{
"id": "times",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "times",
"defaultMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {},
"dataTableId": {
"__rl": true,
"mode": "list",
"value": "XICUlFpXl76eV2Dm",
"cachedResultUrl": "/projects/0WLzulivX2ChGmk8/datatables/XICUlFpXl76eV2Dm",
"cachedResultName": "appointments"
}
},
"typeVersion": 1
},
{
"id": "96690897-ea79-4060-b288-f532b516a0af",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-4688,
96
],
"parameters": {
"width": 480,
"height": 704,
"content": "## \ud83c\udfe5\ud83e\udd16 Medical Clinic WhatsApp Agent + CRM Sync\n\nThis workflow receives WhatsApp Cloud API messages via a Webhook and processes them based on message type (\ud83d\udcdd Text / \ud83c\udf99\ufe0f Audio / \ud83d\uddbc\ufe0f Image / \ud83d\udcce Document). \nFor audio messages \ud83c\udf99\ufe0f, it downloads the media and transcribes it into Arabic text (Whisper). \nFor images \ud83d\uddbc\ufe0f, it downloads the image and analyzes it to describe the content and extract any visible text (Vision). \nFor documents/PDFs \ud83d\udcce, it downloads the file and sends a Gmail notification \u2709\ufe0f to staff with the attachment.\n\nAfter normalizing the final customer text (\ud83c\udf99\ufe0f voice > \ud83d\uddbc\ufe0f image > \ud83d\udcdd text), the workflow builds a structured context \ud83e\udde0 that includes customer data \ud83d\udc64, recent conversation history \ud83d\udcac (memory), the customer\u2019s upcoming appointments \ud83d\udcc5, and available booking slots \ud83d\udd50. The message is then sent to the Main AI Agent \ud83e\udd16 (Arabic only \ud83c\uddf8\ud83c\udde6/\ud83c\uddef\ud83c\uddf4), with RAG enabled \ud83d\udcda using Pinecone \ud83c\udf32 to answer clinic-related questions from uploaded clinic documents (Google Docs).\n\nThe AI response is sent back to the customer via WhatsApp \u2705. In parallel, a strict JSON extractor \ud83e\uddfe parses key fields (booking/reschedule details, service \ud83d\udc89, date/time \u23f0, lead category \ud83e\uddf2, and human escalation \ud83e\uddd1\u200d\u2695\ufe0f). If `needs_human` is true \ud83d\udea8, the workflow emails staff \u2709\ufe0f and stores the case in a \u201chuman\u201d queue table \ud83d\udce5. \nA scheduled sync \u23f2\ufe0f (every 1 minute) updates a Google Sheets CRM \ud83d\udcca from n8n Data Tables (slots/leads/human/appointments).\n\n**Requirements:** WhatsApp Cloud API token \ud83d\udd11 + phone_number_id \ud83d\udcf1, Gmail \u2709\ufe0f + Google Sheets \ud83d\udcca credentials, Pinecone \ud83c\udf32 index/namespace, and the referenced n8n Data Tables \ud83d\uddc2\ufe0f.\n"
},
"typeVersion": 1
},
{
"id": "e5d403fa-47d3-4d8f-a4f5-96c7feb21782",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
-4016,
48
],
"parameters": {
"color": 7,
"width": 400,
"height": 208,
"content": "## Section 1 \u2014 Webhook & Verification\nReceives WhatsApp webhooks, responds to `hub.challenge` verification, and routes incoming messages by type.\n"
},
"typeVersion": 1
},
{
"id": "ad430754-6c6a-4838-ad0e-52b3d367977d",
"name": "Sticky Note7",
"type": "n8n-nodes-base.stickyNote",
"position": [
-368,
-256
],
"parameters": {
"color": 7,
"width": 1552,
"height": 752,
"content": "## Section 5 \u2014 Extraction + Appointment Save\n\n- **AI Data Extractor**: reads the agent output and extracts strict JSON fields (date/day/time/service, booking vs reschedule, needs_human, lead_category).\n- **Code in JavaScript1**: parses the JSON safely, enriches it with customer metadata (phone/name/timestamps), and normalizes YES/NO flags.\n- **Check Appointment1**: routes only when `has_appointment = true`.\n- **save appointment - same service (upsert)**: updates/creates an appointment row matched by `(phone + service)` so each service has its own record.\n- **If Appointment is ready**: compares the new service vs previous/checked service; if different, allows creating another booking record.\n- **save appointment - different service (insert)**: inserts a new appointment row for an additional service (separate from existing one).\n\n"
},
"typeVersion": 1
},
{
"id": "82315f3e-7edb-4ece-a709-2dc2b12782ac",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
-384,
736
],
"parameters": {
"color": 7,
"wid
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.
gmailOAuth2googleSheetsOAuth2ApihttpBearerAuthopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow automates patient communication for medical clinics using the WhatsApp Business API. It supports appointment booking, rescheduling, service inquiries, follow-ups, and document submissions. The workflow includes AI capabilities, appointment management, human…
Source: https://n8n.io/workflows/11605/ — 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.
Supercharge your trading decisions with this end-to-end AI automation that connects market intelligence, technical analysis, and automated trade execution — all without manual intervention.
WooriFisa. Uses agent, httpRequest, documentDefaultDataLoader, vectorStorePinecone. Scheduled trigger; 86 nodes.
WooriFisa 최종. Uses memoryMongoDbChat, agent, httpRequest, documentDefaultDataLoader. Scheduled trigger; 68 nodes.
Tech Radar. Uses googleDrive, documentDefaultDataLoader, stickyNote, mySql. Scheduled trigger; 53 nodes.
This project is built on top of the famous open source ThoughtWorks Tech Radar.