{
  "name": "Dental AI Receptionist v3.0 — WhatsApp Booking + Reminder",
  "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-v3",
      "webhookMethods": ["POST"]
    },
    {
      "parameters": {
        "functionCode": "// Parse incoming WhatsApp Cloud API message format (v18.0+)\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\n// Handle status updates (delivered, read, sent) — skip them\nif (!messages || !messages.length) {\n  return [{ json: { skip: true, event: 'status_update', raw: JSON.stringify(body).substring(0, 80) } }];\n}\n\nconst msg = messages[0];\nconst msgStatus = msg.status;\nif (msgStatus) {\n  // Status update — skip\n  return [{ json: { skip: true, event: 'status_update', status: msgStatus } }];\n}\n\nconst phoneFrom = msg.from;\nconst messageBody = (msg.text && msg.text.body) || '';\nconst messageId = msg.id;\nconst timestamp = msg.timestamp;\n\n// Extract contact info\nconst contact = value && value.contacts && value.contacts[0];\nconst profileName = contact && contact.profile && contact.profile.name;\nconst waId = contact && contact.wa_id;\n\n// Validate: we need a phone number and message body\nif (!phoneFrom || !messageBody) {\n  return [{ json: { skip: true, event: 'invalid_message', reason: 'missing phone or body' } }];\n}\n\nreturn [{ json: {\n  skip: false,\n  phoneFrom,\n  messageBody,\n  messageId,\n  timestamp,\n  profileName: profileName || null,\n  waId: waId || null,\n  receivedAt: new Date().toISOString()\n} }];"
      },
      "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, Florida.\n\nCLINIC DETAILS:\n- Name: Bright Smile Dental\n- Address: 123 Wellness Blvd, Suite 100, Miami FL 33101\n- Phone: (305) 555-0199\n- Business Hours: Mon–Fri 8:00 AM – 6:00 PM, Sat 9:00 AM – 2:00 PM\n- Services offered:\n  • General Checkup\n  • Teeth Cleaning ($120)\n  • Teeth Whitening ($350)\n  • Root Canal ($800)\n  • Braces Consultation (Free)\n  • Emergency Care (call first)\n\nYOUR ROLE:\n- Respond to patient WhatsApp messages instantly\n- Help patients book appointments or get information\n- Always confirm booking details (name, date, time, location)\n- Be warm, professional, conversational — never robotic\n\nRESPONSE RULES:\n- Maximum 3 sentences per message\n- Use emojis naturally (not excessively)\n- Never invent appointment times — only use available slots\n- If unclear, ask the patient for the missing info\n- For cancellations: direct to call (305) 555-0199\n\nAVAILABLE SLOTS FORMAT: 09:00, 09:30, 10:00, 11:00, 11:30, 14:00, 14:30, 15:00, 16:00, 16:30 (standard 30-min appointments)\n\nTODAY IS: {{ $now.format('dddd, MMMM D, YYYY') }}\n\nExtract from each message:\n- intent: \"booking\" | \"greeting\" | \"cancel\" | \"question\" | \"confirm\" | \"other\"\n- patient_name: first name only, or null if not provided\n- preferred_date: YYYY-MM-DD format, or null\n- preferred_time: HH:MM format, or null\n- service: one of the service names above, or null\n- needs_name: true if patient wants to book but didn't give their name\n- needs_time: true if booking intent but no time specified\n\nReturn ONLY valid JSON. No markdown, no explanation, no text outside the JSON.\nResponse must be a single JSON object with these exact keys."
          },
          {
            "role": "user",
            "content": "={{ $json.messageBody }}"
          }
        ],
        "options": {
          "maxTokens": 400,
          "temperature": 0.2,
          "timeout": 12000
        }
      },
      "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 intent response safely\n// Handles: text response, JSON object, nested output, error cases\nconst input = $input.item.json;\nconst aiResponse = input.text || input.message || input.output || '';\n\n// Try to extract JSON from AI response\nlet intent = {\n  intent: 'other',\n  patient_name: null,\n  preferred_date: null,\n  preferred_time: null,\n  service: null,\n  needs_name: false,\n  needs_time: false\n};\n\ntry {\n  // Method 1: Direct JSON parse of full text\n  const parsed = JSON.parse(aiResponse);\n  if (parsed.intent) {\n    intent = { ...intent, ...parsed };\n  }\n} catch(e1) {\n  // Method 2: Extract JSON from text with potential markdown\n  const jsonMatch = aiResponse.match(/\\{[\\s\\S]*?\\}/);\n  if (jsonMatch) {\n    try {\n      const inner = JSON.parse(jsonMatch[0]);\n      if (inner.intent) {\n        intent = { ...intent, ...inner };\n      }\n    } catch(e2) {\n      // Method 3: Try to extract fields individually from text\n      const intentMatch = aiResponse.match(/\"intent\"\\s*:\\s*\"(\\w+)\"/);\n      if (intentMatch) intent.intent = intentMatch[1];\n\n      const nameMatch = aiResponse.match(/\"patient_name\"\\s*:\\s*\"([^\"]+)\"/);\n      if (nameMatch) intent.patient_name = nameMatch[1];\n\n      const dateMatch = aiResponse.match(/\"preferred_date\"\\s*:\\s*\"([^\"]+)\"/);\n      if (dateMatch) intent.preferred_date = dateMatch[1];\n\n      const timeMatch = aiResponse.match(/\"preferred_time\"\\s*:\\s*\"([^\"]+)\"/);\n      if (timeMatch) intent.preferred_time = timeMatch[1];\n\n      const serviceMatch = aiResponse.match(/\"service\"\\s*:\\s*\"([^\"]+)\"/);\n      if (serviceMatch) intent.service = serviceMatch[1];\n    }\n  }\n}\n\n// Fallback: check if intent exists directly in input\nif (input.intent && !intent.intent) {\n  intent.intent = input.intent;\n}\n\n// Normalize intent\nconst validIntents = ['booking', 'greeting', 'cancel', 'question', 'confirm', 'other'];\nif (!validIntents.includes(intent.intent)) {\n  intent.intent = 'other';\n}\n\n// Resolve date: if \"tomorrow\" or no date provided, use next available weekday\nif (!intent.preferred_date || intent.preferred_date === 'null') {\n  intent.preferred_date = null;\n} else {\n  // Check if AI returned a relative date (tomorrow, next week, etc.)\n  const msg = ($input.item.json.messageBody || '').toLowerCase();\n  const today = new Date();\n  const tomorrow = new Date(today);\n  tomorrow.setDate(tomorrow.getDate() + 1);\n\n  if (msg.includes('tomorrow') && !intent.preferred_date) {\n    // Format as YYYY-MM-DD\n    const y = tomorrow.getFullYear();\n    const m = String(tomorrow.getMonth() + 1).padStart(2, '0');\n    const d = String(tomorrow.getDate()).padStart(2, '0');\n    intent.preferred_date = `${y}-${m}-${d}`;\n  }\n}\n\n// Add input data to intent\nreturn [{ json: {\n  ...$input.item.json,\n  ...intent,\n  intent_normalized: intent.intent\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": { "timeout": 8000 }
      },
      "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 for the requested date\n// Standard dental hours: 9am–5:30pm (30-min slots)\nconst allSlots = [];\nfor (let h = 9; h <= 17; 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 calendarItems = input.data?.items || input.items || [];\n\n// Extract booked times from existing calendar events\nconst bookedTimes = calendarItems\n  .filter(item => item.start && (item.start.dateTime || item.start.date))\n  .map(item => {\n    const startStr = item.start.dateTime || item.start.date;\n    const d = new Date(startStr);\n    return `${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`;\n  });\n\n// Also check for blocked/busy times\nconst busyTimes = input.busy || [];\nbusyTimes.forEach(b => {\n  if (b.start) {\n    const d = new Date(b.start);\n    bookedTimes.push(`${String(d.getHours()).padStart(2,'0')}:${String(d.getMinutes()).padStart(2,'0')}`);\n  }\n});\n\nconst availableSlots = allSlots.filter(slot => !bookedTimes.includes(slot));\n\n// Format for display (12-hour format)\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\n// Check if requested time is available\nconst requestedTime = input.preferred_time;\nconst requestedTimeAvailable = requestedTime && availableSlots.includes(requestedTime);\n\n// If requested time is booked, find nearest alternative\nlet suggestion = null;\nif (requestedTime && !requestedTimeAvailable) {\n  const idx = allSlots.indexOf(requestedTime);\n  if (idx !== -1) {\n    // Find closest available before and after\n    let before = null, after = null;\n    for (let i = idx - 1; i >= 0; i--) {\n      if (availableSlots.includes(allSlots[i])) { before = allSlots[i]; break; }\n    }\n    for (let i = idx + 1; i < allSlots.length; i++) {\n      if (availableSlots.includes(allSlots[i])) { after = allSlots[i]; break; }\n    }\n    if (before || after) suggestion = [before, after].filter(Boolean);\n  }\n}\n\nreturn [{ json: {\n  ...input,\n  availableSlots,\n  displaySlots: displaySlots.slice(0, 8),\n  totalAvailable: availableSlots.length,\n  requestedTimeAvailable,\n  alternativeSlots: suggestion,\n  slotSource: calendarItems.length > 0 ? 'google_calendar' : 'mock'\n} }];"
      },
      "id": "extract-slots",
      "name": "Compute Available Slots",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [1300, 200]
    },
    {
      "parameters": {
        "conditions": {
          "options": { "caseSensitive": false, "leftValue": "", "typeValidation": "loose" },
          "conditions": [
            {
              "id": "booking-intent-check",
              "leftValue": "{{ $json.intent_normalized }}",
              "rightValue": "booking",
              "operator": { "type": "string", "operation": "equals" }
            },
            {
              "id": "time-present-check",
              "leftValue": "{{ $json.preferred_time }}",
              "rightValue": "",
              "operator": { "type": "string", "operation": "notEquals" }
            }
          ],
          "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  COALESCE(\n    (SELECT id FROM conversations WHERE phone_from = '{{ $json.phoneFrom }}' LIMIT 1),\n    (SELECT COALESCE(MAX(id), 0) + 1 FROM conversations)\n  ),\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)\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 || 'Patient') + ' — ' + ($json.service || 'General Checkup') }}"
            },
            {
              "name": "location",
              "value": "123 Wellness Blvd, Suite 100, Miami FL 33101"
            },
            {
              "name": "description",
              "value": "={{ 'Booked via: WhatsApp AI Receptionist\\nPhone: ' + $json.phoneFrom + '\\nService: ' + ($json.service || 'General Checkup') + '\\nConfirmation sent via WhatsApp' }}"
            },
            {
              "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": { "timeout": 8000 }
      },
      "id": "create-calendar-event",
      "name": "Create Calendar Event",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [1900, 100],
      "continueOnFail": true
    },
    {
      "parameters": {
        "functionCode": "// Build a professional 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 nicely\nconst dateObj = new Date((data.preferred_date || new Date().toISOString().split('T')[0]) + 'T12:00:00');\nconst dateStr = dateObj.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });\n\nconst patientName = data.patient_name || 'there';\nconst service = data.service || 'General Checkup';\n\n// Build the confirmation message\nconst confirmation = `✅ Booking Confirmed!\n\n🦷 Service: ${service}\n👤 Name: ${patientName}\n📅 Date: ${dateStr}\n🕐 Time: ${timeStr}\n📍 123 Wellness Blvd, Suite 100, Miami FL\n\nSee you then! I'll send you a reminder 24 hours before. 😊`;\n\nreturn [{ json: {\n  ...data,\n  confirmation,\n  timeStr,\n  dateStr,\n  patientName,\n  service\n} }];"
      },
      "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": 15000 }
      },
      "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 appointment reminder 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\nconst dateObj = new Date((data.preferred_date || '') + 'T12:00:00');\nconst dateStr = dateObj.toLocaleDateString('en-US', { weekday: 'long', month: 'long', day: 'numeric' });\n\nconst patientName = data.patient_name || 'there';\nconst service = data.service || 'General Checkup';\n\nconst reminder = `Hi ${patientName}! 👋 Friendly reminder about your appointment at Bright Smile Dental.\n\n🦷 ${service}\n📅 Tomorrow: ${dateStr}\n🕐 ${timeStr}\n📍 123 Wellness Blvd, Suite 100, Miami FL\n\nNeed to reschedule? Just reply to this message — we're happy to help! 😊`;\n\nreturn [{ json: {\n  ...data,\n  reminder,\n  reminderPatientName: patientName,\n  reminderDateStr: dateStr,\n  reminderTimeStr: timeStr\n} }];"
      },
      "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": 15000 }
      },
      "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\n  AND status = 'confirmed'"
      },
      "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": "// Handle all non-booking-with-time cases:\n// - greeting\n// - booking without time (show slots)\n// - question\n// - cancel\n// - confirm\n\nconst data = $input.item.json;\nconst intent = data.intent_normalized || 'other';\nconst patientName = data.patient_name;\nconst slots = data.displaySlots || [];\nconst needsName = data.needs_name;\nconst needsTime = data.needs_time;\n\nlet responseText = '';\n\nif (intent === 'greeting') {\n  responseText = `Hi${patientName ? ' ' + patientName : ''}! 👋 Welcome to Bright Smile Dental. I'm your AI receptionist — I can help you book an appointment in seconds. Just tell me your name and preferred time!`;\n}\nelse 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. Sorry for any inconvenience! 🙏`;\n}\nelse if (intent === 'confirm') {\n  responseText = `Great! 🎉 Your appointment is confirmed. You'll receive a WhatsApp reminder 24 hours before. See you then! 😊`;\n}\nelse if (intent === 'question') {\n  responseText = `Thanks for reaching out! 💬 For appointments, just tell me your name and preferred time — I can book one for you right now. For other questions, call us at (305) 555-0199.`;\n}\nelse if (intent === 'booking') {\n  // Patient wants to book — need name and/or time\n  if (needsName && needsTime) {\n    responseText = `Great! 👋 I'd love to help you book. What's your name, and what time works for you tomorrow?`;\n  } else if (needsName) {\n    responseText = `Great! What name should I put the appointment under?`;\n  } else if (needsTime || !data.preferred_time) {\n    // Show available slots\n    if (slots.length > 0) {\n      responseText = `Perfect${patientName ? ', ' + patientName : ''}! 🎉 Which time works for you tomorrow?\\n\\nAvailable slots:\\n${slots.join('\\n')}`;\n    } else {\n      responseText = `Great${patientName ? ', ' + patientName : ''}! 👋 What time works for you tomorrow?`;\n    }\n  } else if (data.alternativeSlots && data.alternativeSlots.length > 0 && !data.requestedTimeAvailable) {\n    const [b, a] = data.alternativeSlots;\n    responseText = `The time you requested isn't available, but here are the closest options:\\n\\n${b ? '🕐 ' + b : ''}\\n${a ? '🕑 ' + a : ''}\\n\\nWhich works for you?`;\n  } else {\n    responseText = `Thanks! I've noted your preferred time. You should receive a confirmation shortly. 😊`;\n  }\n}\nelse {\n  // Fallback for any other intent\n  if (slots.length > 0) {\n    responseText = `Thanks for reaching out! 💬 Here are our available slots tomorrow:\\n\\n${slots.join('\\n')}\\n\\nJust reply with your name and preferred time to book!`;\n  } else {\n    responseText = `Hi there! 👋 Thanks for messaging Bright Smile Dental. To book an appointment, just tell me your name and preferred time — I'll take care of the rest! 😊`;\n  }\n}\n\nreturn [{ json: {\n  ...data,\n  responseText,\n  intent_normalized: intent\n} }];"
      },
      "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": 15000 }
      },
      "id": "send-whatsapp-slots",
      "name": "Send Slots Response",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.1,
      "position": [1900, 300]
    },
    {
      "parameters": {
        "functionCode": "// Error handler — if any step fails, notify the clinic staff\n// and provide a fallback response to the patient\nconst data = $input.item.json;\nconst errorMsg = data.error || 'An unexpected error occurred';\nconst phoneFrom = data.phoneFrom || 'unknown';\n\n// Log the error (in production, this would go to a monitoring system)\nconsole.error('[Dental AI Receptionist Error]', JSON.stringify({\n  phone: phoneFrom,\n  error: errorMsg,\n  timestamp: new Date().toISOString(),\n  intent: data.intent_normalized\n}));\n\n// Fallback response to patient\nconst fallbackResponse = `Sorry about that! 🙏 Something didn't work as expected. Please call us at (305) 555-0199 and we'll book your appointment over the phone. Our team is available Mon–Fri 8am–6pm.`;\n\nreturn [{ json: {\n  ...data,\n  fallbackResponse,\n  errorOccurred: true,\n  errorMessage: errorMsg\n} }];"
      },
      "id": "error-handler",
      "name": "Error Handler (Fallback)",
      "type": "n8n-nodes-base.function",
      "typeVersion": 2,
      "position": [3100, 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 }]
      ]
    },
    "Send Slots Response": {
      "main": [
        [{ "node": "Error Handler (Fallback)", "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", "reminder"],
  "meta": {
    "templateCredsSetupCompleted": false,
    "templateId": "dental-receptionist-whatsapp-v3",
    "versionId": "3.0.0",
    "templateDescription": "Complete WhatsApp AI receptionist workflow v3.0 for dental clinics. Full end-to-end: WhatsApp Cloud API inbound → GPT-4o intent detection → Google Calendar real slot check → PostgreSQL booking → WhatsApp confirmation with name/date/time/location → 23-hour wait → automated WhatsApp reminder → DB update."
  },
  "triggerCount": 1,
  "createdAt": "2026-06-12T00:00:00.000Z",
  "updatedAt": "2026-06-12T00:00:00.000Z",
  "versionId": "3.0.0",
  "description": "Complete WhatsApp AI receptionist workflow v3.0 for dental clinics. Features: improved AI prompt with clinic details, robust intent parsing with 3 fallback methods, Google Calendar real slot availability check with alternative suggestions, PostgreSQL booking with proper error handling, formatted WhatsApp confirmation, 23-hour auto-reminder, error fallback handler."
}