{
  "id": "KYehtlopDpAO49xz",
  "name": "ElevenLabs Voice Agent: Medical Clinic Appointment Booking",
  "tags": [],
  "nodes": [
    {
      "id": "3e4fd8c7-20a2-4e12-b628-e320a3d18fd8",
      "name": "Sticky Note - Webhook",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3936,
        -272
      ],
      "parameters": {
        "color": 5,
        "width": 510,
        "height": 481,
        "content": "## \ud83d\udce5 WEBHOOK ENTRY POINT\n\n**Receives data from ElevenLabs:**\n\n**Body parameters:**\n- `fullName` - Patient name (only for booking)\n- `email` - Contact email (only for booking)\n- `phone` - Phone number (only for booking)\n- `date` - Format: YYYY-MM-DD\n- `time` - Format: HH:MM\n- `appointmentType` - Service type\n- `location` - Clinic location\n\n**Query parameter:**\n- `request` - \"check availability\" or blank for booking\n\n**\u2699\ufe0f Setup Required:**\nChange the webhook path to your own unique ID for security."
      },
      "typeVersion": 1
    },
    {
      "id": "09316f64-01a4-41a6-a5d7-fe188596d08c",
      "name": "Sticky Note - Edit Fields",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3424,
        -272
      ],
      "parameters": {
        "color": 5,
        "width": 310,
        "height": 481,
        "content": "## \u270f\ufe0f DATA PREPARATION\n\n**Extracts and formats:**\n- Combines `date` + `time` into single datetime field\n- Prepares all fields for calendar and email nodes\n\n**Output format:**\nYYYY-MM-DD HH:MM\n(e.g., \"2025-10-08 15:00\")"
      },
      "typeVersion": 1
    },
    {
      "id": "e50d4da0-11c4-482b-bf94-3cbde2d83b7e",
      "name": "Sticky Note - Routing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3120,
        -272
      ],
      "parameters": {
        "color": 6,
        "width": 406,
        "height": 485,
        "content": "## \ud83d\udd00 ROUTING LOGIC\n\n**Determines action type:**\n\n**Path 1 (True):** Availability Check\n- Triggered when `fullName` OR `email` is missing\n- Only date/time/type provided\n- Returns available/unavailable status\n\n**Path 2 (False):** Book Appointment\n- All fields present (name, email, phone, etc.)\n- Creates calendar event\n- Sends confirmation email"
      },
      "typeVersion": 1
    },
    {
      "id": "3d3dc615-146c-4602-af78-ed7484856b90",
      "name": "Sticky Note - Booking",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2704,
        304
      ],
      "parameters": {
        "color": 3,
        "width": 255,
        "height": 274,
        "content": "## \u2705 BOOKING PATH\n\n**Creates Google Calendar Event**\n"
      },
      "typeVersion": 1
    },
    {
      "id": "c16c3b25-795a-4ec0-89f5-b11545ea16b5",
      "name": "Sticky Note - Email",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2448,
        304
      ],
      "parameters": {
        "color": 3,
        "width": 357,
        "height": 274,
        "content": "## \ud83d\udce7 EMAIL CONFIRMATION\n\n**Sends an ppointment confirmation via Gmail**\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "06216198-2728-4f2f-bf04-c981c25185a9",
      "name": "Sticky Note - Availability",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2720,
        -272
      ],
      "parameters": {
        "color": 7,
        "width": 453,
        "height": 482,
        "content": "## \ud83d\udd0d AVAILABILITY CHECK PATH\n\n**Check if specific time slot is free**\n\u2192 If available: Return success\n\u2192 If unavailable: Find alternative slots\n\n**\u2699\ufe0f Setup Required:**\nConnect Google Calendar and select your calendar"
      },
      "typeVersion": 1
    },
    {
      "id": "56ffb539-4752-4a58-ac64-245a9ff11138",
      "name": "Sticky Note - Quick Start",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4336,
        -272
      ],
      "parameters": {
        "color": 4,
        "width": 405,
        "height": 900,
        "content": "## \ud83d\ude80 QUICK START GUIDE\n\n**1. Configure Credentials:**\n   - [ ] Google Calendar OAuth2\n   - [ ] Gmail OAuth2\n\n**2. Select Calendar:**\n   - Open all Google Calendar nodes\n   - Choose your booking calendar from dropdown\n\n**3. Customize Settings:**\n   - [ ] Update webhook path (security)\n   - [ ] Adjust business hours in \"Sort Available Slots\" code\n   - [ ] Modify timezone \n   - [ ] Update clinic details in email template\n\n**4. Test Workflow:**\n   - Use pinned test data\n   - Test both availability check and booking\n\n**5. Connect to ElevenLabs:**\n   - Copy production webhook URL\n   - Configure agent functions\n   - Map parameters correctly\n\n**6. Go Live:**\n   - Activate workflow\n   - Test with real voice calls"
      },
      "typeVersion": 1
    },
    {
      "id": "600186bf-6b72-4daf-9e0a-00a8cb247b36",
      "name": "Sticky Note - Troubleshooting",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4336,
        352
      ],
      "parameters": {
        "color": 4,
        "width": 409,
        "height": 524,
        "content": "## \ud83d\udc1b TROUBLESHOOTING\n\n**Workflow not triggering:**\n- Check webhook URL is correct\n- Verify workflow is active\n- Check ElevenLabs function configuration\n\n**Calendar not syncing:**\n- Reconnect Google Calendar credential\n- Verify calendar ID is correct\n- Check OAuth permissions\n\n**Email not sending:**\n- Verify Gmail credential\n- Check spam folder\n- Confirm email address format\n\n**Timezone issues:**\n- Adjust timezone in your Calendar\n\n**Slots showing as unavailable:**\n- Check calendar has no conflicts"
      },
      "typeVersion": 1
    },
    {
      "id": "c5f730f6-8930-4efe-8773-5b93c6fd8e4e",
      "name": "Webhook1",
      "type": "n8n-nodes-base.webhook",
      "position": [
        -3696,
        240
      ],
      "parameters": {
        "path": "2be0d61e-a2a0-48de-867e-4892849296b4",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "aba4a40b-94f7-4f54-b1e1-87459cf96dca",
      "name": "Available?1",
      "type": "n8n-nodes-base.if",
      "position": [
        -2432,
        48
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "1d0c317e-477a-437e-918a-a761e9069115",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.available }}",
              "rightValue": "true"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "3cbee3f2-dd7a-4a09-84d7-0957aaac68b7",
      "name": "Check Availability Again",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        -2176,
        128
      ],
      "parameters": {
        "options": {},
        "timeMax": "={{ DateTime.fromISO($('Webhook1').item.json.body.date).plus({ days: 30 }).toISODate() }}",
        "timeMin": "={{ $('Webhook1').item.json.body.date }} ",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "="
        },
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "7cafaa98-68fb-4408-b37b-ba1e7edb436f",
      "name": "Set Up Variables",
      "type": "n8n-nodes-base.set",
      "position": [
        -3328,
        240
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "4478c006-d158-4854-bf88-5e0fa1c8b936",
              "name": "fullName",
              "type": "string",
              "value": "={{ $json.body.fullName }}"
            },
            {
              "id": "b7bb33fc-426d-40ce-91b9-93844a269bfd",
              "name": "email",
              "type": "string",
              "value": "={{ $json.body.email }}"
            },
            {
              "id": "66f90820-45ae-4a98-ad17-f5ee72042680",
              "name": "phone",
              "type": "string",
              "value": "={{ $json.body.phone }}"
            },
            {
              "id": "0992fb3c-06e9-49f2-aba1-49fcee27172a",
              "name": "location",
              "type": "string",
              "value": "={{ $json.body.location }}"
            },
            {
              "id": "a969fb63-fee2-4e07-96d4-1f1468d8ed43",
              "name": "appointmentType",
              "type": "string",
              "value": "={{ $json.body.appointmentType }}"
            },
            {
              "id": "4444d238-b4ae-4bba-823a-aec64b4ea9de",
              "name": "date",
              "type": "string",
              "value": "={{ $json.body.date }} {{ $json.body.time }}"
            },
            {
              "id": "a4ef9aa5-11ca-4a35-b1f6-f0a20c64026f",
              "name": "appointmentType",
              "type": "string",
              "value": "={{ $json.body.appointmentType }}"
            },
            {
              "id": "6fff6dc5-0389-41c9-abfe-eaa4ddf7a2d8",
              "name": "location",
              "type": "string",
              "value": "={{ $json.body.location }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "7e5cd44f-3b1e-4f6e-ab9f-59d9efd3ce84",
      "name": "Book Appointment",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        -2640,
        416
      ],
      "parameters": {
        "end": "={{ DateTime.fromFormat($('Set Up Variables').item.json.date, 'yyyy-MM-dd HH:mm').plus({ minutes: 30 }).toISO({ suppressMilliseconds: true }) }}",
        "start": "={{ DateTime.fromFormat($('Set Up Variables').item.json.date, 'yyyy-MM-dd HH:mm').toISO({ suppressMilliseconds: true }) }}",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "="
        },
        "additionalFields": {
          "summary": "={{ $json.fullName }}, {{ $('Webhook1').item.json.body.appointmentType }}",
          "location": "={{ $('Webhook1').item.json.body.location }} ",
          "attendees": [
            "={{ $('Webhook1').item.json.body.email }}"
          ],
          "description": "={{ $json.fullName }}\n\n{{ $('Webhook1').item.json.query.request }}"
        },
        "useDefaultReminders": false
      },
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "0bbc9ee0-6813-4688-9638-dc4e79162b1a",
      "name": "Get availability in a calendar",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        -2640,
        48
      ],
      "parameters": {
        "options": {},
        "timeMax": "={{ DateTime.fromFormat($('Set Up Variables').item.json.date, 'yyyy-MM-dd HH:mm').plus({ minutes: 30 }).toISO({ suppressMilliseconds: true }) }}",
        "timeMin": "={{ DateTime.fromFormat($('Set Up Variables').item.json.date, 'yyyy-MM-dd HH:mm').toISO({ suppressMilliseconds: true }) }}",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "="
        },
        "resource": "calendar"
      },
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "5c6da70a-f54f-4088-8ed1-4bf32c91cd86",
      "name": "Send a message",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -2432,
        416
      ],
      "parameters": {
        "message": "=Hello {{ $('Set Up Variables').item.json.fullName.split(' ')[0] }}! Your appointment for {{ $('Set Up Variables').item.json.appointmentType }} is confirmed \u2013 Evergreen Clinic on {{\n  (() => {\n    const dateStr = $('Set Up Variables').item.json.date; // e.g., \"2025-10-07 15:00\"\n    const [datePart, timePart] = dateStr.split(' ');\n    const [year, month, day] = datePart.split('-').map(Number);\n\n    // Month names\n    const months = [\n      'January', 'February', 'March', 'April', 'May', 'June',\n      'July', 'August', 'September', 'October', 'November', 'December'\n    ];\n\n    // Ordinal function\n    function ordinal(n) {\n      if (n > 3 && n < 21) return 'th';\n      switch (n % 10) {\n        case 1: return 'st';\n        case 2: return 'nd';\n        case 3: return 'rd';\n        default: return 'th';\n      }\n    }\n\n    return `${months[month - 1]} the ${day}${ordinal(day)} at ${timePart}`;\n  })()\n}}<br><br> We're looking forward to seeing you. Here are the key details:<br><br> <b>Provider:</b> Dr. Sava (Aesthetic Medicine)<br> <b>Location:</b> Evergreen Clinic, Hasenb\u00fchlstrasse 36<br> <b>Duration:</b> ~20\u201330 minutes<br> <b>Contact:</b> 027 545 15 82 \u00b7 evergreen@clinic.com<br><br> <b>Before you come (quick prep):</b> <ul>   <li>Arrive 5\u201310 minutes early for check-in and a brief consent review.</li>   <li>If possible, avoid alcohol, ibuprofen/aspirin, and intense workouts for 24 hours before (helps reduce bruising).</li>   <li>Come with a clean face (no heavy makeup on treatment areas).</li>   <li>Tell us in advance if you're pregnant/breastfeeding, have a skin infection, or take blood thinners\u2014your safety first.</li> </ul> See you on {{    DateTime.fromISO($('Set Up Variables').item.json.date)     .toFormat(\"MMMM d 'at' h:mm a\")     .replace(/(\\d{1,2})(?= at)/, (d) => d + (['th','st','nd','rd'][((d%100-20)%10)||d%10]||'th')) }}!<br><br> Warm regards,<br> Evergreen Clinic<br> Rosenweg 24 3931, Z\u00fcrich \u00b7 027 545 15 82",
        "options": {
          "appendAttribution": false
        },
        "subject": "={{ $('Set Up Variables').item.json.appointmentType }} at Evergreen Clinic on {{\n  (() => {\n    const dateStr = $('Set Up Variables').item.json.date; // e.g., \"2025-10-07 15:00\"\n    const [datePart, timePart] = dateStr.split(' ');\n    const [year, month, day] = datePart.split('-').map(Number);\n\n    // Month names\n    const months = [\n      'January', 'February', 'March', 'April', 'May', 'June',\n      'July', 'August', 'September', 'October', 'November', 'December'\n    ];\n\n    // Ordinal function\n    function ordinal(n) {\n      if (n > 3 && n < 21) return 'th';\n      switch (n % 10) {\n        case 1: return 'st';\n        case 2: return 'nd';\n        case 3: return 'rd';\n        default: return 'th';\n      }\n    }\n\n    return `${months[month - 1]} the ${day}${ordinal(day)} at ${timePart}`;\n  })()\n}}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "cda91dc7-8c9a-4ef5-ae69-38e894dd11ad",
      "name": "Confirm Booking ",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -2224,
        416
      ],
      "parameters": {
        "options": {},
        "respondWith": "text",
        "responseBody": "=Thank you. The appointment is scheduled on {{\n  (() => {\n    const dateStr = $('Set Up Variables').item.json.date; // e.g., \"2025-10-07 15:00\"\n    const [datePart, timePart] = dateStr.split(' ');\n    const [year, month, day] = datePart.split('-').map(Number);\n\n    // Month names\n    const months = [\n      'January', 'February', 'March', 'April', 'May', 'June',\n      'July', 'August', 'September', 'October', 'November', 'December'\n    ];\n\n    // Ordinal function\n    function ordinal(n) {\n      if (n > 3 && n < 21) return 'th';\n      switch (n % 10) {\n        case 1: return 'st';\n        case 2: return 'nd';\n        case 3: return 'rd';\n        default: return 'th';\n      }\n    }\n\n    return `${months[month - 1]} the ${day}${ordinal(day)} at ${timePart}`;\n  })()\n}}"
      },
      "typeVersion": 1.4
    },
    {
      "id": "ce9d6170-3b3f-486b-8667-0f0ec8e879dc",
      "name": "Confirm Available Times",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -2176,
        -32
      ],
      "parameters": {
        "options": {},
        "respondWith": "text",
        "responseBody": "=Thank you for waiting! This time is available"
      },
      "typeVersion": 1.4
    },
    {
      "id": "ade07557-b674-4bb3-9415-cf0c98d6d095",
      "name": "Sort Available Slots",
      "type": "n8n-nodes-base.code",
      "position": [
        -1968,
        128
      ],
      "parameters": {
        "jsCode": "// Collect all booked intervals from input items (previous node)\nconst bookedIntervals = [];\nfor (const item of $input.all()) {\n  bookedIntervals.push({\n    start: new Date(item.json.start.dateTime),\n    end: new Date(item.json.end.dateTime)\n  });\n}\n\n// Settings\nconst days = 30;\nconst workStartHour = 7;\nconst workEndHour = 17; // exclusive, so last slot starts at 16:30\nconst slotDurationMinutes = 30;\n\n// Helper to format date to ISO8601 with +02:00\nfunction toISOWithFixedOffset(date) {\n  const pad = n => String(n).padStart(2, '0');\n  return date.getFullYear() + '-' +\n    pad(date.getMonth() + 1) + '-' +\n    pad(date.getDate()) + 'T' +\n    pad(date.getHours()) + ':' +\n    pad(date.getMinutes()) + ':00+02:00';\n}\n\n// Helper to check if two intervals overlap\nfunction isOverlapping(slotStart, slotEnd, bookedStart, bookedEnd) {\n  return slotStart < bookedEnd && slotEnd > bookedStart;\n}\n\n// Get current time in +02:00 (Europe/Zurich) timezone\nconst now = new Date();\nconst nowUtc = now.getTime() + (now.getTimezoneOffset() * 60000);\nconst nowPlus2 = new Date(nowUtc + 2 * 60 * 60000);\n\nnowPlus2.setSeconds(0, 0); // ignore seconds/milliseconds\n\nlet available = [];\n\nfor (let d = 0; d < days; d++) {\n  for (let h = workStartHour; h < workEndHour; h++) {\n    for (let m = 0; m < 60; m += slotDurationMinutes) {\n      let slotStart = new Date(nowPlus2);\n      slotStart.setDate(slotStart.getDate() + d);\n      slotStart.setHours(h, m, 0, 0);\n\n      let slotEnd = new Date(slotStart);\n      slotEnd.setMinutes(slotEnd.getMinutes() + slotDurationMinutes);\n\n      // Exclude slots in the past\n      if (slotStart < nowPlus2) continue;\n\n      // Check for overlap with any booked interval\n      let overlaps = bookedIntervals.some(b =>\n        isOverlapping(slotStart, slotEnd, b.start, b.end)\n      );\n\n      if (!overlaps) {\n        available.push({ available: toISOWithFixedOffset(slotStart) });\n      }\n    }\n  }\n}\n\nreturn available.map(a => ({ json: a }));"
      },
      "typeVersion": 2
    },
    {
      "id": "7527abf9-5e6d-44d2-8250-02db3eee78fa",
      "name": "Aggregate",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        -1760,
        128
      ],
      "parameters": {
        "options": {},
        "fieldsToAggregate": {
          "fieldToAggregate": [
            {
              "fieldToAggregate": "available"
            }
          ]
        }
      },
      "typeVersion": 1
    },
    {
      "id": "729f6a0f-ad3e-4f4d-b5f4-d9695ecd1e03",
      "name": "Confirm The Time's Unavailable",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        -1552,
        128
      ],
      "parameters": {
        "options": {},
        "respondWith": "text",
        "responseBody": "=Sorry, this time slot is unavailable right now. You may have another time in your mind?"
      },
      "typeVersion": 1.4
    },
    {
      "id": "990395b5-8563-445a-bf87-9159427498ee",
      "name": "If",
      "type": "n8n-nodes-base.if",
      "position": [
        -2976,
        240
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "f586362a-6775-4692-b290-bc43bfdeb361",
              "operator": {
                "type": "string",
                "operation": "notExists",
                "singleValue": true
              },
              "leftValue": "={{ $json.fullName }}",
              "rightValue": "check availability"
            },
            {
              "id": "1b48aa09-72f2-4d20-8cac-e5e0adb80f28",
              "operator": {
                "type": "string",
                "operation": "notExists",
                "singleValue": true
              },
              "leftValue": "={{ $json.email }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "eb4efa41-4863-44b4-a691-9f1553d2c4a9",
  "connections": {
    "If": {
      "main": [
        [
          {
            "node": "Get availability in a calendar",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Book Appointment",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Webhook1": {
      "main": [
        [
          {
            "node": "Set Up Variables",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate": {
      "main": [
        [
          {
            "node": "Confirm The Time's Unavailable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Available?1": {
      "main": [
        [
          {
            "node": "Confirm Available Times",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Check Availability Again",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send a message": {
      "main": [
        [
          {
            "node": "Confirm Booking ",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Book Appointment": {
      "main": [
        [
          {
            "node": "Send a message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Up Variables": {
      "main": [
        [
          {
            "node": "If",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sort Available Slots": {
      "main": [
        [
          {
            "node": "Aggregate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Availability Again": {
      "main": [
        [
          {
            "node": "Sort Available Slots",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get availability in a calendar": {
      "main": [
        [
          {
            "node": "Available?1",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}