This workflow corresponds to n8n.io template #16601 — we link there as the canonical source.
This workflow follows the Agent → Google Sheets 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": "D4TqDtnEgWfwD9eN",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Log clinic chatbot inquiries from website widget to Google Sheets",
"tags": [
{
"id": "fN86LhlIa0CzEsYF",
"name": "clinic-chatbot",
"createdAt": "2026-06-20T02:40:57.417Z",
"updatedAt": "2026-06-20T02:40:57.417Z"
}
],
"nodes": [
{
"id": "1f0d34ea-df20-435b-8b2c-5e09ca605850",
"name": "OpenAI Chat Model",
"type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
"position": [
304,
704
],
"parameters": {
"model": "gpt-4o-mini",
"options": {}
},
"credentials": {
"openAiApi": {
"name": "<your credential>"
}
},
"typeVersion": 1.1
},
{
"id": "10e80b19-2dcd-4b7c-9d52-1d7dbdcb293f",
"name": "Sticky Note \u2014 Template Overview",
"type": "n8n-nodes-base.stickyNote",
"position": [
-944,
-64
],
"parameters": {
"color": 3,
"width": 640,
"height": 1028,
"content": "## AI Clinic Receptionist Chatbot with Google Sheets Logging\n\n### Who is this for?\nClinics, solo doctors, and healthcare businesses that want a 24/7 virtual receptionist embedded on their website without hiring extra staff.\n\n### What it does\nThis workflow powers an AI chat widget on a clinic website. Patients can send inquiries any time of day and get instant, context-aware replies from an AI receptionist named Medi.\n\nAs the conversation progresses, the AI silently extracts key details such as name, contact, service interest, appointment date and time, doctor preference, and reason for visit, and logs everything to a Google Sheet automatically. One row per visitor, updated in real time. No manual data entry needed.\n\n### How it works\n1. The chat widget on the website sends a POST request to the webhook with the patient's message and a session ID\n2. The AI Agent (Medi) responds using clinic knowledge baked into its system prompt\n3. Conversation memory keeps context across the full session\n4. A hidden data block in every AI reply is stripped before display and parsed for structured info\n5. If any patient data was collected, the Google Sheet row is created or updated\n6. The clean reply is returned to the chat widget\n\n### Requirements\n- OpenAI API key (gpt-4o-mini)\n- Google Sheets OAuth2 connection\n- A clinic website to embed the chat widget on\n\n### How to customize\n- Update the AI system prompt with your real clinic name, services, doctors, and contact info\n- Adjust the sheet column names to match your own Google Sheet\n- Change the session memory window size depending on how long your typical conversations run"
},
"typeVersion": 1
},
{
"id": "0bd3e076-e406-492f-92d5-f5fc101fad7c",
"name": "Sticky Note \u2014 Setup Steps",
"type": "n8n-nodes-base.stickyNote",
"position": [
-944,
1024
],
"parameters": {
"color": 5,
"width": 640,
"height": 696,
"content": "## Setup Instructions\n\n**Step 1 \u2014 Connect OpenAI**\nOpen the OpenAI Chat Model node and connect your OpenAI API key credential.\n\n**Step 2 \u2014 Connect Google Sheets**\nOpen the Update Inquirer Row in Sheets node and connect your Google account via OAuth2.\n\n**Step 3 \u2014 Set your Sheet ID**\nReplace YOUR_GOOGLE_SHEET_ID in the Google Sheets node with the ID from your sheet URL:\nhttps://docs.google.com/spreadsheets/d/YOUR_ID_HERE/edit\n\n**Step 4 \u2014 Prepare your Google Sheet**\nCreate a tab named Inquiries with these exact headers in Row 1:\n\nSession ID | Last Updated | Name | Contact | Intent | Service Interested | Appointment Date | Appointment Time | Doctor Preference | Reason for Visit | Conversation Summary | Status\n\n**Step 5 \u2014 Update the AI system prompt**\nOpen the Medi - AI Receptionist Agent node and update the system prompt with your real clinic name, address, phone, email, services, and doctors.\n\n**Step 6 \u2014 Activate and grab the webhook URL**\nActivate the workflow. Copy the webhook URL from the Receive Chat Message node and paste it into your chatbot.js file as the N8N_WEBHOOK_URL value."
},
"typeVersion": 1
},
{
"id": "e15ef87f-9b51-4184-827f-378c1f3339cb",
"name": "Sticky Note \u2014 Step 1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-256,
240
],
"parameters": {
"width": 340,
"height": 492,
"content": "## Step 1 \u2014 Receive Message\n\nAccepts POST requests from the chat widget. Each request includes:\n- `message` \u2014 the patient's text\n- `sessionId` \u2014 unique browser ID\n- `timestamp` \u2014 ISO datetime\n\nResponse mode is set to Response Node so the reply waits for the full AI output before returning."
},
"typeVersion": 1
},
{
"id": "7fb9d2e5-e4c4-4d77-a01f-5a03590b79b1",
"name": "Sticky Note \u2014 Step 2",
"type": "n8n-nodes-base.stickyNote",
"position": [
240,
240
],
"parameters": {
"width": 528,
"height": 596,
"content": "## Step 2 \u2014 AI Agent (Medi)\n\nRuns the AI receptionist with full clinic knowledge in the system prompt. The Session Memory node uses the visitor's sessionId as the key, storing the last 20 messages so context is never lost mid-conversation.\n\nAt the end of every reply, the AI appends a hidden [INQUIRER_DATA] JSON block with all known patient details. This block is invisible to the patient \u2014 it gets stripped in the next step."
},
"typeVersion": 1
},
{
"id": "6fdc09f4-1dbd-4e1d-8a13-0ba1dc87e7b8",
"name": "Sticky Note \u2014 Step 3",
"type": "n8n-nodes-base.stickyNote",
"position": [
784,
240
],
"parameters": {
"width": 380,
"height": 464,
"content": "## Step 3 \u2014 Parse & Strip\n\nThe Code node does two things:\n\n1. Removes the [INQUIRER_DATA] block from the reply so the patient never sees raw JSON in the chat\n\n2. Parses the block and extracts all structured fields into separate output values\n\nSets `hasData = true` if at least one meaningful field (name, contact, or intent) was collected."
},
"typeVersion": 1
},
{
"id": "2043f64f-ded5-4e38-b325-b6e164a4dba0",
"name": "Sticky Note \u2014 Step 4",
"type": "n8n-nodes-base.stickyNote",
"position": [
1184,
240
],
"parameters": {
"width": 360,
"height": 460,
"content": "## Step 4 \u2014 Conditional Log\n\nSkips writing to Google Sheets on early turns before the patient has shared any details. Only logs when hasData is true.\n\nTRUE path: write to Sheets, then reply\nFALSE path: skip Sheets, reply immediately\n\nThis avoids creating empty rows for every single message exchange."
},
"typeVersion": 1
},
{
"id": "d5718c2d-d165-4419-b70d-b84beb884915",
"name": "Sticky Note \u2014 Step 5",
"type": "n8n-nodes-base.stickyNote",
"position": [
1568,
176
],
"parameters": {
"width": 400,
"height": 516,
"content": "## Step 5 \u2014 Log to Google Sheets\n\nUses appendOrUpdate matched on Session ID. Each visitor gets exactly one row, updated in real time as more details are collected during the conversation.\n\nStatus values:\n- `new` \u2014 just started\n- `in_progress` \u2014 chatting\n- `appointment_set` \u2014 date and time confirmed\n- `completed` \u2014 inquiry resolved\n- `needs_followup` \u2014 requires staff action"
},
"typeVersion": 1
},
{
"id": "4391a0e7-6cdb-425f-b05f-b04c0b731c74",
"name": "Sticky Note \u2014 Step 6",
"type": "n8n-nodes-base.stickyNote",
"position": [
1984,
224
],
"parameters": {
"width": 340,
"height": 472,
"content": "## Step 6 \u2014 Reply to Widget\n\nSends the cleaned reply back to the chat widget as:\n`{ \"reply\": \"...\" }`\n\nIncludes CORS headers so the widget can call this from any domain. The chatbot.js widget expects this exact response shape."
},
"typeVersion": 1
},
{
"id": "c2973430-57db-4171-8161-f1bd3cac644c",
"name": "ReceiveChatMessage",
"type": "n8n-nodes-base.webhook",
"position": [
-128,
560
],
"parameters": {
"path": "clinic-chat",
"options": {
"allowedOrigins": "*"
},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "598267fa-bc90-41ea-993a-14c2fe2ca116",
"name": "ExtractUserMessage",
"type": "n8n-nodes-base.set",
"position": [
112,
560
],
"parameters": {
"options": {}
},
"typeVersion": 3.4
},
{
"id": "8679aa01-1932-41d3-a8d6-058999769527",
"name": "Medi - AI Receptionist",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
352,
560
],
"parameters": {
"text": "={{ $('ReceiveChatMessage').first().json.body.message }}",
"options": {
"systemMessage": "You are Medi, the friendly and professional virtual receptionist for MediCare Clinic located at 123 Health Street, Cebu City, Philippines.\n\nYour job is to assist patients and visitors with their inquiries. Always be warm, helpful, and concise. Use simple language.\n\nToday's date is {{ $now.toFormat('yyyy-MM-dd (EEEE)') }}. Use it to resolve relative dates like \"tomorrow\" or \"next Monday\".\n\n## CLINIC INFORMATION\n- Name: MediCare Clinic\n- Address: 123 Health Street, Cebu City, Philippines\n- Phone: +1234567890\n- Email: user@example.com\n- Hours: Monday\u2013Saturday 8:00 AM \u2013 6:00 PM, Sunday 9:00 AM \u2013 1:00 PM\n\n## SERVICES OFFERED\n- General Consultation\n- Pediatrics\n- Women's Health\n- Laboratory Services\n- Pharmacy\n\n## DOCTORS\n- Dr. Maria Santos \u2014 General Medicine\n- Dr. Jose Reyes \u2014 Pediatrics\n- Dr. Ana Lim \u2014 Women's Health\n\n## HOW TO BOOK AN APPOINTMENT\n1. Call us at +1234567890. Email user@example.com\n3. Walk in during clinic hours\n\n## COLLECTING INFORMATION\nDuring the conversation, naturally and politely collect the following as they become relevant \u2014 do NOT ask all at once:\n- Full name\n- Contact number or email\n- Service they are interested in\n- Preferred appointment date and time (if booking)\n- Doctor preference (if any)\n- Reason for visit / main concern\n\nFor example: if someone wants to book, ask for their name, contact, preferred date/time, and which doctor or service they need.\n\n## RULES\n- NEVER give specific medical diagnoses or treatment advice. Say: \"I'm not a doctor, but I'd recommend booking a consultation so our physician can properly assess you.\"\n- If asked about doctor availability, say: \"Please call us at +1234567890 to confirm availability.\"\n- Always end your reply with a helpful follow-up offer or question.\n- Keep replies to 2\u20134 sentences unless more detail is truly needed.\n- If you don't know something, say so honestly and direct them to call or email.\n\n## FORMATTING\n- Reply in plain, conversational text. NO markdown \u2014 no asterisks (**), no ## headers, no bullet lists.\n- When confirming details, use a short natural sentence, e.g.\n \"Got it \u2014 General Consultation for Airone Villasor (0949...) on June 21 at 9 AM. Shall I finalize it?\"\n- Keep it warm and brief: 2\u20134 sentences.\n\n\n## IMPORTANT \u2014 STRUCTURED DATA OUTPUT\nAt the END of every single reply, always append this hidden block on a new line. The user will NEVER see it \u2014 it gets stripped before display. Use null for anything not yet known. Always reflect the LATEST known values based on the full conversation so far:\n\n[INQUIRER_DATA]\n{\n \"name\": \"<full name or null>\",\n \"contact\": \"<phone number or email or null>\",\n \"intent\": \"<one of: appointment_booking | service_inquiry | general_inquiry | doctor_inquiry | lab_request | pharmacy | other>\",\n \"service_interested\": \"<one or more of: General Consultation, Pediatrics, Women's Health, Laboratory Services, Pharmacy, or null>\",\n \"appointment_date\": \"<preferred date as YYYY-MM-DD or null>\",\n \"appointment_time\": \"<preferred time as HH:MM or null>\",\n \"doctor_preference\": \"<doctor name or null>\",\n \"reason_for_visit\": \"<brief description of concern or null>\",\n \"conversation_summary\": \"<1-2 sentence summary of what the user needs>\",\n \"status\": \"<one of: new | in_progress | appointment_set | completed | needs_followup>\"\n}\n[/INQUIRER_DATA]"
},
"promptType": "define"
},
"typeVersion": 1.7
},
{
"id": "3474dfb6-10a3-4d79-9b29-a6b6a49f77dc",
"name": "Session Memory (per visitor)1",
"type": "@n8n/n8n-nodes-langchain.memoryBufferWindow",
"position": [
512,
704
],
"parameters": {
"sessionKey": "={{ $('ReceiveChatMessage').first().json.body.sessionId || 'anonymous' }}",
"sessionIdType": "customKey",
"contextWindowLength": 20
},
"typeVersion": 1.3
},
{
"id": "d7822ad7-fe9c-4bc0-b34c-641e86eacf6e",
"name": "Strip Hidden Data Block1",
"type": "n8n-nodes-base.code",
"position": [
880,
560
],
"parameters": {
"jsCode": "const raw = $('Medi - AI Receptionist').first().json.output ?? '';\n\nconst MARKER = '[INQUIRER_DATA]';\nconst idx = raw.indexOf(MARKER);\n\n// reply = everything BEFORE the marker (this is the only fix that matters)\nlet reply = idx === -1 ? raw : raw.slice(0, idx);\nreply = reply.trim();\n\n// data = the JSON block AFTER the marker\nlet data = {};\nif (idx !== -1) {\n try {\n data = JSON.parse(raw.slice(idx + MARKER.length).trim());\n } catch (e) {\n data = {}; // malformed block \u2014 don't crash, just log nothing\n }\n}\n\nconst hasData = Boolean(\n data.name || data.contact || data.appointment_date || data.service_interested\n);\n\nreturn [{\n json: {\n reply,\n sessionId: $('ReceiveChatMessage').first().json.body.sessionId ?? 'anonymous',\n timestamp: new Date().toISOString(),\n name: data.name ?? null,\n contact: data.contact ?? null,\n intent: data.intent ?? null,\n service_interested: data.service_interested ?? null,\n appointment_date: data.appointment_date ?? null,\n appointment_time: data.appointment_time ?? null,\n doctor_preference: data.doctor_preference ?? null,\n reason_for_visit: data.reason_for_visit ?? null,\n conversation_summary: data.conversation_summary ?? null,\n status: data.status ?? 'new',\n hasData,\n },\n}];\n"
},
"typeVersion": 2
},
{
"id": "da82d6b5-b73f-4c29-8260-975ec3c474a5",
"name": "Has Collectible Data",
"type": "n8n-nodes-base.if",
"position": [
1232,
560
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "condition-has-data",
"operator": {
"type": "boolean",
"operation": "true"
},
"leftValue": "={{ $json.hasData }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "31bfe108-4442-4f05-9fc8-c84e763ca20e",
"name": "Update Inquirer Row in Sheet",
"type": "n8n-nodes-base.googleSheets",
"position": [
1616,
560
],
"parameters": {
"columns": {
"value": {
"Name": "={{ $json.name }}",
"Intent": "={{ $json.intent }}",
"Status": "={{ $json.status }}",
"Contact": "={{ $json.contact }}",
"Session ID": "={{ $json.sessionId }}",
"Last Updated": "={{ $json.timestamp }}",
"Appointment Date": "={{ $json.appointment_date }}",
"Appointment Time": "={{ $json.appointment_time }}",
"Reason for Visit": "={{ $json.reason_for_visit }}",
"Doctor Preference": "={{ $json.doctor_preference }}",
"Service Interested": "={{ $json.service_interested }}",
"Conversation Summary": "={{ $json.conversation_summary }}"
},
"schema": [
{
"id": "Session ID",
"type": "string",
"display": true,
"required": false,
"displayName": "Session ID",
"defaultMatch": true,
"canBeUsedToMatch": true
},
{
"id": "Last Updated",
"type": "string",
"display": true,
"required": false,
"displayName": "Last Updated",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "Name",
"type": "string",
"display": true,
"required": false,
"displayName": "Name",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "Contact",
"type": "string",
"display": true,
"required": false,
"displayName": "Contact",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "Intent",
"type": "string",
"display": true,
"required": false,
"displayName": "Intent",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "Service Interested",
"type": "string",
"display": true,
"required": false,
"displayName": "Service Interested",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "Appointment Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Appointment Date",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "Appointment Time",
"type": "string",
"display": true,
"required": false,
"displayName": "Appointment Time",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "Doctor Preference",
"type": "string",
"display": true,
"required": false,
"displayName": "Doctor Preference",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "Reason for Visit",
"type": "string",
"display": true,
"required": false,
"displayName": "Reason for Visit",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "Conversation Summary",
"type": "string",
"display": true,
"required": false,
"displayName": "Conversation Summary",
"defaultMatch": false,
"canBeUsedToMatch": false
},
{
"id": "Status",
"type": "string",
"display": true,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": false
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Session ID"
]
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "name",
"value": "Sheet1"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "1_1cq5zhqzzR8_aUbRsfZiLCHfDBVkwhjmeicqV4ObGs"
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.5
},
{
"id": "4cb6949d-456b-4765-b161-3252a8541da2",
"name": "Send Reply To Chat Widget",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
2048,
560
],
"parameters": {
"options": {
"responseHeaders": {
"entries": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "Access-Control-Allow-Origin:",
"value": "*"
}
]
}
},
"respondWith": "json",
"responseBody": "={ \"reply\": {{ JSON.stringify($('Strip Hidden Data Block1').first().json.reply ?? '') }} }"
},
"typeVersion": 1.1
}
],
"active": true,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "597a3cd9-1de3-4db8-9e43-651def287f34",
"nodeGroups": [],
"connections": {
"OpenAI Chat Model": {
"ai_languageModel": [
[
{
"node": "Medi - AI Receptionist",
"type": "ai_languageModel",
"index": 0
}
]
]
},
"ExtractUserMessage": {
"main": [
[
{
"node": "Medi - AI Receptionist",
"type": "main",
"index": 0
}
]
]
},
"ReceiveChatMessage": {
"main": [
[
{
"node": "ExtractUserMessage",
"type": "main",
"index": 0
}
]
]
},
"Has Collectible Data": {
"main": [
[
{
"node": "Update Inquirer Row in Sheet",
"type": "main",
"index": 0
}
],
[
{
"node": "Send Reply To Chat Widget",
"type": "main",
"index": 0
}
]
]
},
"Medi - AI Receptionist": {
"main": [
[
{
"node": "Strip Hidden Data Block1",
"type": "main",
"index": 0
}
]
]
},
"Strip Hidden Data Block1": {
"main": [
[
{
"node": "Has Collectible Data",
"type": "main",
"index": 0
}
]
]
},
"Update Inquirer Row in Sheet": {
"main": [
[
{
"node": "Send Reply To Chat Widget",
"type": "main",
"index": 0
}
]
]
},
"Session Memory (per visitor)1": {
"ai_memory": [
[
{
"node": "Medi - AI Receptionist",
"type": "ai_memory",
"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.
googleSheetsOAuth2ApiopenAiApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow receives website chatbot messages via a webhook, uses OpenAI to generate an AI receptionist reply with session-based context, extracts structured inquiry details, and appends or updates a single visitor row in Google Sheets before returning a clean JSON reply to…
Source: https://n8n.io/workflows/16601/ — 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.
VEP WAPP. Uses openAi, lmChatOpenAi, toolCalculator, agent. Webhook trigger; 100 nodes.
AI Customer Service Automation - Portfolio. Uses googleSheets, httpRequest, agent, memoryBufferWindow. Webhook trigger; 91 nodes.
Enhance your support, onboarding, and internal knowledge workflows with an intelligent RAG-powered chatbot that responds using live data stored in Google Sheets. 🤖📚 Built for teams that rely on struct
🧠 Gwen – The AI Voice Marketing Agent Gwen is your intelligent voice-powered marketing assistant built in n8n. She combines the power of OpenAI, ElevenLabs, and automation workflows to handle content
Agent D'analyse. Uses memoryBufferWindow, googleSheetsTool, agent, lmChatOpenAi. Webhook trigger; 42 nodes.