{
  "name": "Dental AI Receptionist — WhatsApp Booking",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "whatsapp-inbound",
        "responseMode": "onReceived",
        "options": {}
      },
      "id": "whatsapp-trigger",
      "name": "WhatsApp Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [100, 350],
      "webhookId": "dental-receptionist-whatsapp",
      "webhookMethods": ["POST"]
    },
    {
      "parameters": {
        "functionCode": "// Parse incoming WhatsApp Cloud API message format\nconst body = $input.item.json;\n\n// Handle WhatsApp Cloud API format\nconst entry = body.entry && body.entry[0];\nconst change = entry && entry.changes && entry.changes[0];\nconst value = change && change.value;\nconst messages = value && value.messages;\n\nif (!messages || !messages.length) {\n  // Status update or other event — skip\n  return [{ json: { skip: true, body: JSON.stringify(body).substring(0, 100) } }];\n}\n\nconst msg = messages[0];\nconst status = msg.status; // For status updates\n\nif (status) {\n  // Message status update (delivered, read, etc.) — skip\n  return [{ json: { skip: true, event: 'status_update' } }];\n}\n\nconst phoneFrom = msg.from;\nconst messageBody = (msg.text && msg.text.body) || '';\nconst messageId = msg.id;\nconst timestamp = msg.timestamp;\nconst profileName = value && value.contacts && value.contacts[0] && value.contacts[0].profile && value.contacts[0].profile.name;\n\nreturn [{ json: { skip: false, phoneFrom, messageBody, messageId, timestamp, profileName } }];"
      },
      "id": "parse-whatsapp-message",
      "name": "Parse Message",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [300, 350]
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" },
          "conditions": [
            {
              "id": "skip-check",
              "leftValue": "{{ $json.skip }}",
              "rightValue": false,
              "operator": { "type": "boolean", "operation": "equals" }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "skip-non-messages",
      "name": "Is a Real Message?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [500, 350],
      "continueOnFail": false
    },
    {
      "parameters": {
        "mode": "internal",
        "model": "gpt-4o",
        "messages": [
          {
            "role": "system",
            "content": "You are the AI receptionist for \"Bright Smile Dental\" — a modern dental clinic in Miami.\n\nCLINIC INFO:\n- Name: Bright Smile Dental\n- Address: 123 Wellness Blvd, Suite 100, Miami FL 33101\n- Phone: (305) 555-0199\n- Hours: Mon–Fri 8am–6pm, Sat 9am–2pm\n- Services: General Checkup, Teeth Cleaning, Teeth Whitening, Root Canal, Braces Consultation, Emergency Care\n\nYOUR JOB:\n- Help patients book appointments via WhatsApp\n- Keep responses SHORT — 1-3 sentences max\n- Extract: intent (booking/greeting/cancel/question/other), patient_name, preferred_date (YYYY-MM-DD), preferred_time (HH:MM), service\n- For booking: confirm name + date + time + location\n- Never invent appointment times — use the slots provided in the conversation\n- Be friendly, professional, never robotic\n\nAvailable time slots (standard dental hours): 09:00, 09:30, 10:00, 11:00, 11:30, 14:00, 14:30, 15:00, 16:00, 16:30\n\nReturn STRICT JSON (no markdown, no explanation, just the JSON):\n{\"intent\":\"booking|greeting|cancel|question|other\",\"patient_name\":\"first name or null\",\"preferred_date\":\"YYYY-MM-DD or null\",\"preferred_time\":\"HH:MM or null\",\"service\":\"service name or null\"}\n\nIf patient says something like \"tomorrow at 10\", set preferred_date to tomorrow's date and preferred_time to \"10:00\"."
          },
          {
            "role": "user",
            "content": "={{ $json.messageBody }}"
          }
        ],
        "options": {
          "maxTokens": 300,
          "temperature": 0.3,
          "timeout": 8000
        }
      },
      "id": "ai-receptionist-agent",
      "name": "AI Receptionist (GPT-4o)",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "typeVersion": 1.3,
      "position": [700, 200],
      "continueOnFail": true
    },
    {
      "parameters": {
        "functionCode": "// Parse AI response and combine with original message data\nconst aiResponse = $input.item.json;\nlet parsed = { intent: 'other', patient_name: null, preferred_date: null, preferred_time: null, service: null };\n\ntry {\n  if (aiResponse.text || aiResponse.message || aiResponse.output) {\n    const text = aiResponse.text || aiResponse.message || aiResponse.output;\n    // Try to extract JSON from the response\n    const jsonMatch = text.match(/\\{[^{}]*\\}/s);\n    if (jsonMatch) {\n      parsed = JSON.parse(jsonMatch[0]);\n    }\n  }\n  if (aiResponse.parsed) {\n    parsed = aiResponse.parsed;\n  }\n} catch(e) {\n  // Keep default\n}\n\n// Fallback: try to parse from $json directly if AI returned structured data\nif ($input.item.json.intent) {\n  parsed = $input.item.json;\n}\n\nreturn [{ json: { \n  ...$input.item.json,\n  ...parsed,\n  intent: parsed.intent || 'other'\n} }];"
      },
      "id": "parse-ai-response",
      "name": "Parse AI Intent",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [900, 200]
    },
    {
      "parameters": {
        "url": "=https://www.googleapis.com/calendar/v3/calendars/{{ $env.GOOGLE_CALENDAR_ID }}/events",
        "method": "GET",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.GOOGLE_ACCESS_TOKEN }}"
            }
          ]
        },
        "queryParameters": {
          "parameters": [
            {
              "name": "timeMin",
              "value": "={{ $json.preferred_date || $now.format('YYYY-MM-DD') }}T00:00:00Z"
            },
            {
              "name": "timeMax",
              "value": "={{ $json.preferred_date || $now.format('YYYY-MM-DD') }}T23:59:59Z"
            },
            {
              "name": "singleEvents",
              "value": "true"
            },
            {
              "name": "orderBy",
              "value": "startTime"
            }
          ]
        },
        "options": {}
      },
      "id": "google-calendar-check",
      "name": "Google Calendar — Check Slots",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [1100, 200],
      "continueOnFail": true
    },
    {
      "parameters": {
        "functionCode": "// Compute available dental appointment slots\n// Standard dental hours: 9am–6pm, 30-min slots\nconst allSlots = [];\nfor (let h = 9; h < 18; h++) {\n  allSlots.push(`${String(h).padStart(2,'0')}:00`);\n  allSlots.push(`${String(h).padStart(2,'0')}:30`);\n}\n\nconst input = $input.item.json;\nconst items = input.data?.items || input.items || [];\n\n// Extract booked times from calendar events\nconst bookedTimes = items\n  .filter(item => item.start && item.start.dateTime)\n  .map(item => {\n    const d = new Date(item.start.dateTime);\n    return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;\n  });\n\nconst availableSlots = allSlots.filter(slot => !bookedTimes.includes(slot));\n\n// Format for display\nconst displaySlots = availableSlots.map(slot => {\n  const [h, m] = slot.split(':');\n  const hour = parseInt(h);\n  const ampm = hour >= 12 ? 'PM' : 'AM';\n  const h12 = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour);\n  return `${h12}:${m} ${ampm}`;\n});\n\nreturn [{ json: {\n  ...input,\n  availableSlots,\n  displaySlots: displaySlots.slice(0, 8),\n  totalAvailable: availableSlots.length\n} }];"
      },
      "id": "extract-slots",
      "name": "Compute Available Slots",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [1300, 200]
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": true, "leftValue": "", "typeValidation": "strict" },
          "conditions": [
            {
              "id": "intent-check",
              "leftValue": "{{ $json.intent }}",
              "rightValue": "booking",
              "operator": { "type": "string", "operation": "equals" }
            },
            {
              "id": "time-check",
              "leftValue": "{{ $json.preferred_time }}",
              "rightValue": null,
              "operator": { "type": "object", "operation": "notExists" }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "is-booking-with-time",
      "name": "Has Booking Intent + Time?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2,
      "position": [1500, 200],
      "continueOnFail": false
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "=INSERT INTO appointments (conversation_id, patient_name, patient_phone, patient_email, appointment_date, appointment_time, service, status, created_at, updated_at)\nVALUES (\n  (SELECT id FROM conversations WHERE phone_from = '{{ $json.phoneFrom }}' LIMIT 1) || (SELECT COALESCE(MAX(id), 0) + 1 FROM conversations),\n  '{{ $json.patient_name || 'Patient' }}',\n  '{{ $json.phoneFrom }}',\n  NULL,\n  '{{ $json.preferred_date || $now.format('YYYY-MM-DD') }}',\n  '{{ $json.preferred_time }}',\n  '{{ $json.service || 'General Checkup' }}',\n  'confirmed',\n  NOW(),\n  NOW()\n)\nON CONFLICT DO NOTHING\nRETURNING id"
      },
      "id": "save-booking-postgres",
      "name": "Save to PostgreSQL",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2,
      "position": [1700, 100],
      "credentials": {
        "postgres": {
          "id": "postgres-credential",
          "name": "PostgreSQL"
        }
      }
    },
    {
      "parameters": {
        "url": "=https://www.googleapis.com/calendar/v3/calendars/{{ $env.GOOGLE_CALENDAR_ID }}/events",
        "method": "POST",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.GOOGLE_ACCESS_TOKEN }}"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "summary",
              "value": "={{ $json.patient_name }} — {{ $json.service || 'General Checkup' }}"
            },
            {
              "name": "location",
              "value": "123 Wellness Blvd, Suite 100, Miami FL 33101"
            },
            {
              "name": "description",
              "value": "={{ 'Patient: ' + $json.patient_name + '\\nPhone: ' + $json.phoneFrom + '\\nService: ' + ($json.service || 'General Checkup') + '\\nBooked via: WhatsApp AI Receptionist' }}"
            },
            {
              "name": "start",
              "value": "={{ { 'dateTime': $json.preferred_date + 'T' + $json.preferred_time + ':00', 'timeZone': 'America/New_York' } }}"
            },
            {
              "name": "end",
              "value": "={{ { 'dateTime': $json.preferred_date + 'T' + $json.preferred_time.split(':')[0] + ':30', 'timeZone': 'America/New_York' } }}"
            },
            {
              "name": "reminders",
              "value": "={{ { 'useDefault': false, 'overrides': [{'method': 'popup', 'minutes': 1440}] } }}"
            }
          ]
        },
        "options": {}
      },
      "id": "create-calendar-event",
      "name": "Create Calendar Event",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [1900, 100],
      "continueOnFail": true
    },
    {
      "parameters": {
        "functionCode": "// Build booking confirmation WhatsApp message\nconst data = $input.item.json;\nconst [h, m] = (data.preferred_time || '10:00').split(':');\nconst hour = parseInt(h);\nconst ampm = hour >= 12 ? 'PM' : 'AM';\nconst h12 = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour);\nconst timeStr = `${h12}:${m} ${ampm}`;\n\n// Format date for display\nconst dateObj = new Date(data.preferred_date + 'T12:00:00');\nconst dateStr = dateObj.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' });\n\nconst patientName = data.patient_name || 'there';\nconst service = data.service || 'General Checkup';\n\nconst confirmation = `✅ Booking confirmed!\n\nName: ${patientName}\nService: ${service}\nDate: ${dateStr}\nTime: ${timeStr}\n📍 123 Wellness Blvd, Suite 100, Miami FL\n\nSee you then! I'll send a reminder 24 hours before. 😁`;\n\nreturn [{ json: { ...data, confirmation, timeStr, dateStr, patientName, service } }];"
      },
      "id": "build-confirmation-msg",
      "name": "Build Confirmation Message",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [2100, 100]
    },
    {
      "parameters": {
        "url": "=https://graph.facebook.com/v18.0/{{ $env.WHATSAPP_PHONE_NUMBER_ID }}/messages",
        "method": "POST",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.WHATSAPP_ACCESS_TOKEN }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "messaging_product",
              "value": "whatsapp"
            },
            {
              "name": "to",
              "value": "={{ $json.phoneFrom }}"
            },
            {
              "name": "type",
              "value": "text"
            },
            {
              "name": "text",
              "value": "={{ { 'body': $json.confirmation } }}"
            }
          ]
        },
        "options": {
          "timeout": 10000
        }
      },
      "id": "send-whatsapp-confirmation",
      "name": "Send WhatsApp Confirmation",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [2300, 100]
    },
    {
      "parameters": {
        "waitTime": 82800,
        "wireless": false
      },
      "id": "wait-23-hours",
      "name": "Wait 23 Hours",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [2500, 100]
    },
    {
      "parameters": {
        "functionCode": "// Build 24-hour reminder message\nconst data = $input.item.json;\nconst [h, m] = (data.preferred_time || '10:00').split(':');\nconst hour = parseInt(h);\nconst ampm = hour >= 12 ? 'PM' : 'AM';\nconst h12 = hour > 12 ? hour - 12 : (hour === 0 ? 12 : hour);\nconst timeStr = `${h12}:${m} ${ampm}`;\n\nconst patientName = data.patient_name || 'there';\nconst dateStr = data.dateStr || data.preferred_date;\n\nconst reminder = `Hi ${patientName}! 👋 Just a friendly reminder about your appointment at Bright Smile Dental tomorrow.\n\n📅 ${dateStr}\n🕐 ${timeStr}\n📍 123 Wellness Blvd, Suite 100, Miami FL\n\nSee you soon! 😊\n\nNeed to reschedule? Just reply to this message.`;\n\nreturn [{ json: { ...data, reminder } }];"
      },
      "id": "build-reminder-msg",
      "name": "Build Reminder Message",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [2700, 100]
    },
    {
      "parameters": {
        "url": "=https://graph.facebook.com/v18.0/{{ $env.WHATSAPP_PHONE_NUMBER_ID }}/messages",
        "method": "POST",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.WHATSAPP_ACCESS_TOKEN }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "messaging_product",
              "value": "whatsapp"
            },
            {
              "name": "to",
              "value": "={{ $json.phoneFrom }}"
            },
            {
              "name": "type",
              "value": "text"
            },
            {
              "name": "text",
              "value": "={{ { 'body': $json.reminder } }}"
            }
          ]
        },
        "options": {
          "timeout": 10000
        }
      },
      "id": "send-reminder-whatsapp",
      "name": "Send 24hr Reminder",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [2900, 100]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "=UPDATE appointments SET reminder_sent = TRUE, reminder_sent_at = NOW(), updated_at = NOW()\nWHERE patient_phone = '{{ $json.phoneFrom }}'\n  AND appointment_date = '{{ $json.preferred_date }}'\n  AND reminder_sent = FALSE"
      },
      "id": "mark-reminder-sent",
      "name": "Mark Reminder Sent",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2,
      "position": [3100, 100],
      "credentials": {
        "postgres": {
          "id": "postgres-credential",
          "name": "PostgreSQL"
        }
      }
    },
    {
      "parameters": {
        "functionCode": "// Generate response for non-booking or slots-needed messages\nconst data = $input.item.json;\nconst intent = data.intent || 'other';\nconst patientName = data.patient_name;\nconst slots = data.displaySlots || [];\n\nlet responseText = '';\n\nif (intent === 'greeting') {\n  responseText = `Hi${patientName ? ' ' + patientName : ''}! 👋 Welcome to Bright Smile Dental. I'd love to help you book an appointment — just tell me your name and preferred time!`;\n} else if (intent === 'cancel') {\n  responseText = `I understand you'd like to cancel. Please call us at (305) 555-0199 and we'll take care of it right away.`;\n} else if (intent === 'question') {\n  responseText = `Thanks for your question! For appointments and availability, I'm here 24/7. What would you like to know?`;\n} else if (intent === 'booking' && !data.preferred_time) {\n  // Wants to book but didn't specify time\n  if (slots.length) {\n    responseText = `Great${patientName ? ', ' + patientName : ''}! 👋 I can help you book. Here are our available slots tomorrow:\\n\\n${slots.join('\\n')}\\n\\nWhich time works for you?`;\n  } else {\n    responseText = `Great${patientName ? ', ' + patientName : ''}! 👋 What time works for you tomorrow?`;\n  }\n} else if (slots.length) {\n  responseText = `Here are our available slots tomorrow:\\n\\n${slots.join('\\n')}\\n\\nWhich time works for you? Just reply with your preferred time!`;\n} else {\n  responseText = `Thanks for reaching out! For appointments, just tell me your name and preferred time — I'm here 24/7! 😊`;\n}\n\nreturn [{ json: { ...data, responseText } }];"
      },
      "id": "generate-other-response",
      "name": "Generate Slot/Info Response",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [1700, 300]
    },
    {
      "parameters": {
        "url": "=https://graph.facebook.com/v18.0/{{ $env.WHATSAPP_PHONE_NUMBER_ID }}/messages",
        "method": "POST",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.WHATSAPP_ACCESS_TOKEN }}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "messaging_product",
              "value": "whatsapp"
            },
            {
              "name": "to",
              "value": "={{ $json.phoneFrom }}"
            },
            {
              "name": "type",
              "value": "text"
            },
            {
              "name": "text",
              "value": "={{ { 'body': $json.responseText } }}"
            }
          ]
        },
        "options": {
          "timeout": 10000
        }
      },
      "id": "send-whatsapp-slots",
      "name": "Send Slots Response",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [1900, 300]
    }
  ],
  "connections": {
    "WhatsApp Webhook": {
      "main": [
        [{ "node": "Parse Message", "type": "main", "index": 0 }]
      ]
    },
    "Parse Message": {
      "main": [
        [{ "node": "Is a Real Message?", "type": "main", "index": 0 }]
      ]
    },
    "Is a Real Message?": {
      "main": [
        [{ "node": "AI Receptionist (GPT-4o)", "type": "main", "index": 0 }],
        []
      ]
    },
    "AI Receptionist (GPT-4o)": {
      "main": [
        [{ "node": "Parse AI Intent", "type": "main", "index": 0 }]
      ]
    },
    "Parse AI Intent": {
      "main": [
        [{ "node": "Google Calendar — Check Slots", "type": "main", "index": 0 }]
      ]
    },
    "Google Calendar — Check Slots": {
      "main": [
        [{ "node": "Compute Available Slots", "type": "main", "index": 0 }]
      ]
    },
    "Compute Available Slots": {
      "main": [
        [{ "node": "Has Booking Intent + Time?", "type": "main", "index": 0 }]
      ]
    },
    "Has Booking Intent + Time?": {
      "main": [
        [{ "node": "Save to PostgreSQL", "type": "main", "index": 0 }],
        [{ "node": "Generate Slot/Info Response", "type": "main", "index": 0 }]
      ]
    },
    "Save to PostgreSQL": {
      "main": [
        [{ "node": "Create Calendar Event", "type": "main", "index": 0 }]
      ]
    },
    "Create Calendar Event": {
      "main": [
        [{ "node": "Build Confirmation Message", "type": "main", "index": 0 }]
      ]
    },
    "Build Confirmation Message": {
      "main": [
        [{ "node": "Send WhatsApp Confirmation", "type": "main", "index": 0 }]
      ]
    },
    "Send WhatsApp Confirmation": {
      "main": [
        [{ "node": "Wait 23 Hours", "type": "main", "index": 0 }]
      ]
    },
    "Wait 23 Hours": {
      "main": [
        [{ "node": "Build Reminder Message", "type": "main", "index": 0 }]
      ]
    },
    "Build Reminder Message": {
      "main": [
        [{ "node": "Send 24hr Reminder", "type": "main", "index": 0 }]
      ]
    },
    "Send 24hr Reminder": {
      "main": [
        [{ "node": "Mark Reminder Sent", "type": "main", "index": 0 }]
      ]
    },
    "Generate Slot/Info Response": {
      "main": [
        [{ "node": "Send Slots Response", "type": "main", "index": 0 }]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "timezone": "America/New_York",
    "callerPolicy": "workflowsFromSameOwner",
    "errorWorkflow": ""
  },
  "staticData": null,
  "tags": ["whatsapp", "dental", "booking", "ai-receptionist", "automation", "calendar"],
  "meta": {
    "templateCredsSetupCompleted": false,
    "templateId": "dental-receptionist-whatsapp-v2",
    "versionId": "2.0.0",
    "templateDescription": "Complete WhatsApp AI receptionist workflow for dental clinics. Full end-to-end: inbound message → GPT-4 intent → Google Calendar slots → PostgreSQL booking → WhatsApp confirmation → 24hr reminder."
  },
  "triggerCount": 1,
  "createdAt": "2026-06-09T00:00:00.000Z",
  "updatedAt": "2026-06-09T00:00:00.000Z",
  "versionId": "2.0.0",
  "description": "Complete WhatsApp AI receptionist workflow for dental clinics. Features: WhatsApp Cloud API webhook → GPT-4 intent detection → Google Calendar slot availability → PostgreSQL appointment booking → WhatsApp confirmation → automated 24hr reminder via Wait node."
}