AutomationFlowsGeneral › Book, Manage, and Check Appointments Using Vapi and Google Calendar

Book, Manage, and Check Appointments Using Vapi and Google Calendar

ByJohnpaul Nwagwu @johnpaulnwagwu on n8n.io

Use this workflow to book, cancel, or reschedule appointments using Vapi and Google Calendar

Webhook trigger★★★★★ complexity35 nodesGoogle Calendar
General Trigger: Webhook Nodes: 35 Complexity: ★★★★★ Added:

This workflow corresponds to n8n.io template #12831 — we link there as the canonical source.

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 →

Download .json
{
  "id": "ZEt8fHPKy3Ye5CYX",
  "name": "Voice AI Appointment Booking System with Vapi & Google Calendar - Template",
  "tags": [],
  "nodes": [
    {
      "id": "e7503d57-8408-4b88-bcc7-52b8b9a0751f",
      "name": "Book Appointment Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        2608,
        2432
      ],
      "parameters": {
        "path": "book-appointment",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "ffaa0c7e-ebe5-4794-9b08-7772630ab556",
      "name": "Extract Booking Data",
      "type": "n8n-nodes-base.set",
      "position": [
        2832,
        2432
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "888090a8-b557-4d1e-8ead-c247b701a37a",
              "name": "name",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.name }}"
            },
            {
              "id": "25750d43-8579-4eb5-bf84-bdd3753fd69d",
              "name": "email",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.email }}"
            },
            {
              "id": "57ae0306-b762-4e4e-8af3-45ed50c9dd59",
              "name": "phone",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.phoneNumber }}"
            },
            {
              "id": "fc18bf95-65f2-4d5c-81c3-59cbd8b33d06",
              "name": "appointmentDate",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.appointmentDate }}"
            },
            {
              "id": "1db91e90-015a-4dd5-829b-84588352e1eb",
              "name": "appointmentTime",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.appointmentTime }}"
            },
            {
              "id": "4260e29b-4098-46e5-9c17-0a0e823953da",
              "name": "duration",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.duration }}"
            },
            {
              "id": "54728c90-79d8-4a7e-90dc-b7d2fa023258",
              "name": "service",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.service }}"
            },
            {
              "id": "f26383cd-3d5c-45ea-b042-57b85261c3a4",
              "name": "notes",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.notes }}"
            },
            {
              "id": "ae3e11d8-bb91-4717-9a3b-4b4cad1f68bd",
              "name": "toolCallId",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].id }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "93cabadc-fc25-4779-9c59-f63662518615",
      "name": "Validate Required Fields",
      "type": "n8n-nodes-base.if",
      "position": [
        3056,
        2432
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "67d148c4-4e97-41cd-a98b-5e3cd9d18898",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.name }}",
              "rightValue": ""
            },
            {
              "id": "d78fff76-793d-40d1-b9e0-f25be4e69ce7",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.phone }}",
              "rightValue": ""
            },
            {
              "id": "343af58d-4768-4e74-870f-3f36d7911ae5",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.appointmentDate }}",
              "rightValue": ""
            },
            {
              "id": "f0ecd392-52d3-4b58-92e0-63d9e8e2e16d",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.appointmentTime }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "9b26b185-7285-4ed1-a47b-21046b0052c4",
      "name": "Respond - Validation Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3664,
        2528
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.5
    },
    {
      "id": "b5dc67a8-f6d6-4ff5-82d1-9345c98cb5c8",
      "name": "Create Calendar Event",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        3248,
        2272
      ],
      "parameters": {
        "end": "={{ DateTime.fromISO($json.appointmentDate + 'T' + $json.appointmentTime + ':00', {zone: 'America/Vancouver'}).plus({minutes: parseInt($json.duration) || 30}).toISO() }}",
        "start": "={{ DateTime.fromISO($json.appointmentDate + 'T' + $json.appointmentTime + ':00', {zone: 'America/Vancouver'}).toISO() }}",
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "user@example.com",
          "cachedResultName": "Bookings"
        },
        "additionalFields": {
          "summary": "={{ $json.service + ' - ' + $json.name }}",
          "attendees": [],
          "description": "=Customer: {{ $json.name }} Phone: {{ $json.phone }} Service: {{ $json.service }} Notes: {{ $json.notes }}"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "948e3ab6-bddc-432f-a918-c03d41c4836f",
      "name": "Respond - Booking Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3664,
        2272
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.5
    },
    {
      "id": "f4f3b539-ac7b-471d-bbb9-8d35dc9fc1df",
      "name": "Format Booking Success Response",
      "type": "n8n-nodes-base.code",
      "position": [
        3456,
        2272
      ],
      "parameters": {
        "jsCode": "// Get data from Extract Booking Data node\nconst editFieldsData = $('Extract Booking Data').first().json;\nconst webhookData = $('Book Appointment Webhook').first().json;\n\n// Get toolCallId from webhook body\nconst toolCallId = webhookData.body?.message?.toolCallList?.[0]?.id;\n\n// Create Vapi response format\nreturn [{\n  json: {\n    results: [{\n      toolCallId: toolCallId,\n      result: `The appointment for ${editFieldsData.name} at ${editFieldsData.appointmentDate} at ${editFieldsData.appointmentTime} was booked successfully.`\n    }]\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "d06933a5-aa49-462d-bc71-31e14cd39170",
      "name": "Format Validation Error Response",
      "type": "n8n-nodes-base.code",
      "position": [
        3440,
        2528
      ],
      "parameters": {
        "jsCode": "// Get data from Extract Booking Data node\nconst editFieldsData = $('Extract Booking Data').first().json;\nconst webhookData = $('Book Appointment Webhook').first().json;\n\n// Get toolCallId from webhook body\nconst toolCallId = webhookData.body?.message?.toolCallList?.[0]?.id;\n\n// Create Vapi response format\nreturn [{\n  json: {\n    results: [{\n      toolCallId: toolCallId,\n      result: `The input data from webhook is missing required fields.`\n    }]\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "9b9c9b54-8564-42b2-bedc-c08faceb61b9",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2128,
        2416
      ],
      "parameters": {
        "color": 6,
        "width": 284,
        "height": 244,
        "content": "## Tool 2: Book Appointment\n\nReceives booking data from Vapi, validates required fields, creates Google Calendar event, and returns formatted response."
      },
      "typeVersion": 1
    },
    {
      "id": "8fa0a809-d354-4a68-b33f-baff4d2196e6",
      "name": "Get Calendar Events for Date",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        3056,
        1456
      ],
      "parameters": {
        "options": {
          "timeZone": {
            "__rl": true,
            "mode": "list",
            "value": "America/Vancouver",
            "cachedResultName": "America/Vancouver"
          }
        },
        "timeMax": "={{ $json.appointmentDate }}T23:59:59-08:00",
        "timeMin": "={{ $json.appointmentDate }}T00:00:00-08:00",
        "calendar": {
          "__rl": true,
          "mode": "id",
          "value": "user@example.com",
          "__regex": "(^[a-zA-Z0-9.!#$%&\u2019*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\\.[a-zA-Z0-9-]+)*)"
        },
        "operation": "getAll"
      },
      "typeVersion": 1.3,
      "alwaysOutputData": true
    },
    {
      "id": "76b97cbd-354b-4793-a106-a1f706d1cd68",
      "name": "Check Availability Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        2608,
        1456
      ],
      "parameters": {
        "path": "availability",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "3dd18a71-b6fc-4ae9-a3c6-842a8b618b23",
      "name": "Extract Availability Request",
      "type": "n8n-nodes-base.set",
      "position": [
        2832,
        1456
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "74d32727-53c1-4097-be98-6890c7d42a45",
              "name": "appointmentDate",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.appointmentDate }}"
            },
            {
              "id": "58b24bc7-3cea-4596-9401-d028bca697a4",
              "name": "appointmentTime",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.appointmentTime }}"
            },
            {
              "id": "2d2d38d9-f680-4e4b-9bb1-270bc810dbcd",
              "name": "requestedDatetime",
              "type": "string",
              "value": "={{ DateTime.fromISO($json.body.message.toolCalls[0].function.arguments.appointmentDate + 'T' + $json.body.message.toolCalls[0].function.arguments.appointmentTime + ':00', { zone: 'America/Vancouver' }).toFormat(\"yyyy-MM-dd'T'HH:mm:ssZZ\") }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "324e6bdb-55bb-44d7-9efc-8a727ca8bc86",
      "name": "Check Time Slot Availability",
      "type": "n8n-nodes-base.if",
      "position": [
        3280,
        1456
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "20c2de12-9104-4211-99c1-623bcc344a1e",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.start.dateTime }}",
              "rightValue": "={{ $items('Edit Fields1')[0].json.requestedDatetime }}"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "7e25e200-ae98-412b-92cd-29d9267a5be3",
      "name": "Respond - Time Available",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3728,
        1552
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.5
    },
    {
      "id": "3aab7fb2-6089-49a8-9ceb-7a93febfed13",
      "name": "Respond - Time Unavailable",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3728,
        1360
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.5
    },
    {
      "id": "b168952e-55aa-4584-8558-bda1f5e28c63",
      "name": "Format Unavailable Time Response",
      "type": "n8n-nodes-base.code",
      "position": [
        3504,
        1360
      ],
      "parameters": {
        "jsCode": "// Get data from previous nodes\nconst calendarEvents = $('Get Calendar Events for Date').all().map(item => item.json);\nconst requestedDate = $('Extract Availability Request').first().json.appointmentDate;\nconst requestedTime = $('Extract Availability Request').first().json.appointmentTime;\nconst duration = $('Extract Availability Request').first().json.duration;\nconst timeZone = $('Extract Availability Request').first().json.timeZone;\nconst toolCallId = $('Check Availability Webhook').first().json.body.message.toolCallList[0].id;\n\n// Business hours (customize as needed)\nconst WORKDAY_START = \"09:00\";\nconst WORKDAY_END = \"18:00\";\nconst SLOT_DURATION_MINUTES = 30;\n\n// Helper function to format time as HH:MM\nfunction formatTime(date) {\n  return date.toLocaleTimeString('en-US', { \n    hour: '2-digit', \n    minute: '2-digit',\n    hour12: true,\n    timeZone: timeZone \n  });\n}\n\n// Generate all possible 30-minute slots for the day\nfunction generateDaySlots(date) {\n  const slots = [];\n  const [startHour, startMin] = WORKDAY_START.split(':').map(Number);\n  const [endHour, endMin] = WORKDAY_END.split(':').map(Number);\n  \n  let current = new Date(`${date}T${WORKDAY_START}:00`);\n  const end = new Date(`${date}T${WORKDAY_END}:00`);\n  \n  while (current < end) {\n    const slotEnd = new Date(current.getTime() + SLOT_DURATION_MINUTES * 60 * 1000);\n    slots.push({\n      start: current.toISOString(),\n      end: slotEnd.toISOString(),\n      startTime: formatTime(current),\n      endTime: formatTime(slotEnd)\n    });\n    current = slotEnd;\n  }\n  \n  return slots;\n}\n\n// Check if a slot overlaps with any calendar events\nfunction isSlotAvailable(slot, events) {\n  const slotStart = new Date(slot.start);\n  const slotEnd = new Date(slot.end);\n  \n  for (const event of events) {\n    const eventStart = new Date(event.start.dateTime);\n    const eventEnd = new Date(event.end.dateTime);\n    \n    // Check for overlap\n    if (slotStart < eventEnd && slotEnd > eventStart) {\n      return false;\n    }\n  }\n  \n  return true;\n}\n\n// Generate all slots for the requested day\nconst allSlots = generateDaySlots(requestedDate);\n\n// Filter to only available slots\nconst availableSlots = allSlots.filter(slot => \n  isSlotAvailable(slot, calendarEvents)\n);\n\n// Take first 3 available slots as suggestions\nconst suggestedSlots = availableSlots.slice(0, 3);\n\nreturn {\n    results: [{\n      toolCallId: toolCallId,\nresult: `UNAVAILABLE - That requested time slot is already booked. However, I have these available times that day: ${suggestedSlots.map(s => `${s.startTime} to ${s.endTime}`).join(', ')}`  }]};"
      },
      "typeVersion": 2
    },
    {
      "id": "ed8ce086-844b-4207-888e-7f5def845232",
      "name": "Format Available Time Response",
      "type": "n8n-nodes-base.code",
      "position": [
        3504,
        1552
      ],
      "parameters": {
        "jsCode": "// Get data from Extract Availability Request node\nconst editFieldsData = $('Extract Availability Request').first().json;\nconst webhookData = $('Check Availability Webhook').first().json;\n\n// Get toolCallId from webhook body\nconst toolCallId = webhookData.body?.message?.toolCallList?.[0]?.id || 'available-check';\n\n// Create Vapi response format\nreturn [{\n  json: {\n    results: [{\n      toolCallId: toolCallId,\n      result: `The time slot on ${editFieldsData.appointmentDate} at ${editFieldsData.appointmentTime} is available for booking.`\n    }]\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "92eec412-0717-40cc-bc2e-456e798ab190",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2080,
        1472
      ],
      "parameters": {
        "color": 4,
        "width": 300,
        "height": 212,
        "content": "## Tool 1: Check Availability\n\nQueries Google Calendar for requested date, checks if time slot is available, and suggests alternatives if booked."
      },
      "typeVersion": 1
    },
    {
      "id": "c57866e7-e8b8-4817-b664-54bea38622e8",
      "name": "Route by Action Type",
      "type": "n8n-nodes-base.switch",
      "position": [
        2992,
        3488
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "outputKey": "Update",
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "55619954-92f2-44ed-9c74-4f2bf3aae47b",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "update"
                  }
                ]
              },
              "renameOutput": true
            },
            {
              "outputKey": "Cancel",
              "conditions": {
                "options": {
                  "version": 3,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "66c8b926-7005-415d-9d4a-385f71d9aa2a",
                    "operator": {
                      "name": "filter.operator.equals",
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "cancel"
                  }
                ]
              },
              "renameOutput": true
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.4
    },
    {
      "id": "43ad4f84-5636-4ce7-833f-db74fb6cad01",
      "name": "Update Calendar Event",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        3440,
        3312
      ],
      "parameters": {
        "eventId": "={{ $json.id }}",
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "user@example.com",
          "cachedResultName": "Bookings"
        },
        "operation": "update",
        "updateFields": {
          "end": "={{ DateTime.fromISO($('Extract Appointment Management Data').item.json.newDate + 'T' + $('Extract Appointment Management Data').item.json.newTime + ':00', { zone: 'America/Vancouver' }).plus({minutes: 30}).toISO() }}",
          "start": "={{ DateTime.fromISO($('Extract Appointment Management Data').item.json.newDate + 'T' + $('Extract Appointment Management Data').item.json.newTime + ':00', { zone: 'America/Vancouver' }).toISO() }}"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "be76caef-429e-47da-8a3a-172608ef38a9",
      "name": "Find Appointment to Cancel",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        3216,
        3584
      ],
      "parameters": {
        "options": {},
        "timeMax": "={{ DateTime.fromISO($json.originalDate + 'T' + $json.originalTime + ':00', { zone: 'America/Vancouver' }).plus({ minutes: 1 }).toISO() }}",
        "timeMin": "={{ DateTime.fromISO($json.originalDate + 'T' + $json.originalTime + ':00', { zone: 'America/Vancouver' }).toISO() }}",
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "user@example.com",
          "cachedResultName": "Bookings"
        },
        "operation": "getAll"
      },
      "typeVersion": 1.3
    },
    {
      "id": "6815d0a0-b874-4f19-a46f-db8a40738685",
      "name": "Delete Calendar Event",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        3440,
        3584
      ],
      "parameters": {
        "eventId": "={{ $json.id }}",
        "options": {},
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "user@example.com",
          "cachedResultName": "Bookings"
        },
        "operation": "delete"
      },
      "typeVersion": 1.3
    },
    {
      "id": "7510b91c-2635-47e4-b8bc-1191c5f47e01",
      "name": "Manage Appointment Webhook",
      "type": "n8n-nodes-base.webhook",
      "position": [
        2544,
        3488
      ],
      "parameters": {
        "path": "manage-appointment",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2.1
    },
    {
      "id": "98b1dd20-fe60-476c-bbc3-518dd66aaf5a",
      "name": "Extract Appointment Management Data",
      "type": "n8n-nodes-base.set",
      "position": [
        2768,
        3488
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "88789c10-5026-48cf-8272-2779a9a9b99b",
              "name": "action",
              "type": "string",
              "value": "={{ $json.body.message.toolCallList[0].function.arguments.action }}"
            },
            {
              "id": "08604b29-ed0b-415a-9d65-1ade89e5d9c6",
              "name": "email",
              "type": "string",
              "value": "={{ $json.body.message.toolCallList[0].function.arguments.email }}"
            },
            {
              "id": "811ec028-3f20-442a-a228-938ef8b441ee",
              "name": "originalTime",
              "type": "string",
              "value": "={{ $json.body.message.toolCallList[0].function.arguments.appointmentTime }}"
            },
            {
              "id": "460cd369-65d2-4190-aeec-6b4a0fdd8693",
              "name": "originalDate",
              "type": "string",
              "value": "={{ $json.body.message.toolCallList[0].function.arguments.appointmentDate }}"
            },
            {
              "id": "d38bda2b-3d1c-407e-9277-2c4cd2613ee1",
              "name": "newDate",
              "type": "string",
              "value": "={{ $json.body.message.toolCallList[0].function.arguments.newDate }}"
            },
            {
              "id": "8fe3ce2d-3f5b-4ce3-9623-7afd0a7d9ca7",
              "name": "newTime",
              "type": "string",
              "value": "={{ $json.body.message.toolCallList[0].function.arguments.newTime }}"
            },
            {
              "id": "dda2ab30-6425-4296-8e48-0989828fd623",
              "name": "phoneNumber",
              "type": "string",
              "value": "={{ $json.body.message.toolCallList[0].function.arguments.phoneNumber }}"
            },
            {
              "id": "86b4031c-6b5d-4a59-84e3-f814d24123eb",
              "name": "name",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.name }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "44b36764-836d-4997-8a09-27667a51c482",
      "name": "Find Appointment to Update",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        3216,
        3312
      ],
      "parameters": {
        "options": {
          "timeZone": {
            "__rl": true,
            "mode": "list",
            "value": "America/Vancouver",
            "cachedResultName": "America/Vancouver"
          }
        },
        "timeMax": "={{ DateTime.fromISO($json.originalDate + 'T' + $json.originalTime + ':00', { zone: 'America/Vancouver' }).plus({ minutes: 1 }).toISO() }}",
        "timeMin": "={{ DateTime.fromISO($json.originalDate + 'T' + $json.originalTime + ':00', { zone: 'America/Vancouver' }).toISO() }}",
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "user@example.com",
          "cachedResultName": "Bookings"
        },
        "operation": "getAll"
      },
      "typeVersion": 1.3
    },
    {
      "id": "1d4de4a8-3e05-48fb-a598-be2b55f1010e",
      "name": "Respond - Reschedule Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3888,
        3312
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.5
    },
    {
      "id": "c3cea1dd-973e-47c9-887f-6f9580e8522b",
      "name": "Respond - Cancellation Success",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3888,
        3584
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.5
    },
    {
      "id": "df6347f3-3fb2-44d0-9f21-79a67d233d3a",
      "name": "Format Cancellation Success Response",
      "type": "n8n-nodes-base.code",
      "position": [
        3664,
        3584
      ],
      "parameters": {
        "jsCode": "const toolCallId = $('Manage Appointment Webhook').first().json.body.message.toolCallList[0].id;\n\nreturn {\n  results: [\n    {\n      toolCallId: toolCallId,\n      result: \"Appointment cancelled successfully\"\n    }\n  ]\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "02444c73-db46-4bf3-a1d7-13952e602a8c",
      "name": "Format Reschedule Success Response",
      "type": "n8n-nodes-base.code",
      "position": [
        3664,
        3312
      ],
      "parameters": {
        "jsCode": "const toolCallId = $('Manage Appointment Webhook').first().json.body.message.toolCallList[0].id;\n\nreturn {\n  results: [\n    {\n      toolCallId: toolCallId,\n      result: \"Appointment rescheduled successfully\"\n    }\n  ]\n};"
      },
      "typeVersion": 2
    },
    {
      "id": "f49778a4-e30d-4b02-8ac8-dfc6497d7fe9",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2016,
        3376
      ],
      "parameters": {
        "color": 3,
        "width": 300,
        "height": 260,
        "content": "## Tool 3: Manage Appointments\n\nHandles cancellations and rescheduling. Routes by action type, finds existing appointment, then updates or deletes calendar event."
      },
      "typeVersion": 1
    },
    {
      "id": "9501e517-7cb6-47b6-a219-21778d2d3f88",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5024,
        1520
      ],
      "parameters": {
        "color": 5,
        "width": 1232,
        "height": 1904,
        "content": "# VAPI VOICE ASSISTANT PROMPT\n\n### Booking Appointments\nBook the appointment using the **bookAppointment** tool.\nOnce the customer has confirmed their preferred date and time slot, collect **all required information** before booking.\n\n**Required information:**\n- Name (use the name you already collected)\n- Phone number (use the number you already collected)\n- Service type (identified during service identification)\n- Date: YYYY-MM-DD format (e.g., 2026-01-15)\n- Time: HH:MM 24-hour format (e.g., 14:30 for 2:30 PM)\n- Duration: Default to 30 minutes unless otherwise specified\n- Notes: Any special requests or additional information\n\n**CRITICAL RULES for booking:**\n- Only book after the customer explicitly confirms the date and time.\n- Always use **checkAvailability** first to verify the slot is available.\n- Never book a time slot that shows as **UNAVAILABLE**.\n- If **checkAvailability** returns **UNAVAILABLE**, offer the suggested alternative times.\n- After booking, confirm all details with the customer: date, time, service type, and address.\n\n\n### Managing Existing Appointments\nWhen a customer wants to cancel or reschedule an existing appointment, use the following process.\n\n#### Locating the Appointment\nFirst, collect the information needed to find their appointment:\n\n#### Determining the Action\nAsk if they want to cancel or reschedule their appointment:  \n\n#### For Cancellations\n1. Confirm the action\n2. Use the **manageAppointment** tool with `action=\"cancel\"` to cancel the existing appointment.\n\n#### For Rescheduling\n1. Confirm the change:from [date] at [time] to [newDate] at [newTime].\"\n2. Use the **manageAppointment** tool with `action=\"update\"` to update the existing appointment.\n\n### Using the manageAppointment Tool\nWhen calling the **manageAppointment** tool, you need:\n- `name`: Customer's name\n- `phoneNumber`: Customer's phone number\n- `appointmentDate`: Current appointment date in YYYY-MM-DD format\n- `appointmentTime`: Current appointment time in HH:MM format (24-hour)\n- `action`: \"cancel\" or \"update\"\n- `newDate`: New appointment date in YYYY-MM-DD format (rescheduling only)\n- `newTime`: New appointment time in HH:MM format (24-hour) (rescheduling only)\n\n**CRITICAL:** Always verify the appointment details with the customer before using the tool. Never cancel or reschedule without explicit confirmation.\n\n### Confirmation and Wrap-up\n1. Summarize details:\n\n## Response Guidelines\n- Use explicit confirmation for addresses, dates, and times.\n- Ask only one question at a time.\n- For addresses, confirm:\n  - \"That's [house number] [street name], [city], [zip code]. Is that correct?\"\n\n### For Rescheduling Requests\n- Locate the existing appointment:\n- Verify details:\n- Offer alternatives:\n- Confirm change:\n\n**When you receive a response from the checkAvailability tool:**\n- Always read and use the exact information provided in the tool response.\n- If the response says the time slot is available, inform the customer it is available.\n- If the response says the time slot is not available, tell the customer it is not available and offer the alternative time slots provided.\n- Do not make assumptions or provide different information than what the tool returned.\n- The tool response contains real-time availability data\u2014trust it completely."
      },
      "typeVersion": 1
    },
    {
      "id": "389acfc3-2633-47c1-92d2-b715bce1d48b",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        240,
        1840
      ],
      "parameters": {
        "width": 976,
        "height": 864,
        "content": "# Book Appointments using Vapi and Google Calendar\nUse this workflow to book, cancel, or reschedule appointments using Vapi and Google Calendar\n\n## How it works\nThe Check Availability workflow checks if appointments exist on Google Calendar and returns a response to Vapi.\nThe Book Appointment workflow schedules an appointment on Google Calendar.\nThe Manage Appointments workflow updates or cancels appointments based on the user\u2019s request.\n\n## Key Features:\n1. Ability to book, update, or cancel an appointment\n2. Validates required fields before booking\n3. Handles errors with helpful user messages\n4. Returns a Vapi-compatible response format\n\n\n## Set up steps\n### Prerequisites\n1. Vapi.ai account\n2. Google Calendar\n3. n8n instance - Self-hosted or cloud\n\n### Steps\n- Step 1: Configure Google Calendar Access\n- Step 2: Import and Configure Workflows\n- Step 3: Create Vapi Custom Tools\n- Step 4: Configure Vapi Assistant\n\n### Use Cases\nAppointment booking automation for service businesses\n\n\n\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "c8da4a29-3b68-46c1-b8b8-067c53412556",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3440,
        1168
      ],
      "parameters": {
        "color": 7,
        "width": 528,
        "height": 560,
        "content": "The top code node pulls all events, checks that time and data requested does not overlap with any events, returns time periods as unavailable and then suggests 3 alternate times closest to the requested time and returns a result in the required vapi response format. The bottom code node returns the result as available in the required vapi response format.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "c7ec83f6-17a4-4a70-baf9-1a774c16aeb5",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3376,
        2144
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 592,
        "content": "The top and bottom code node format the results in the required vapi response format"
      },
      "typeVersion": 1
    },
    {
      "id": "4b9ddc95-13f0-4b08-b577-dcf84f46ead8",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3600,
        3168
      ],
      "parameters": {
        "color": 7,
        "width": 512,
        "height": 592,
        "content": "The code nodes format results in the required vapi response format for easy debugging on vapi terminal"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "11f51e95-34ee-4c62-8ea3-8fd0b3f33b67",
  "connections": {
    "Extract Booking Data": {
      "main": [
        [
          {
            "node": "Validate Required Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Action Type": {
      "main": [
        [
          {
            "node": "Find Appointment to Update",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Find Appointment to Cancel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Calendar Event": {
      "main": [
        [
          {
            "node": "Format Booking Success Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Delete Calendar Event": {
      "main": [
        [
          {
            "node": "Format Cancellation Success Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Calendar Event": {
      "main": [
        [
          {
            "node": "Format Reschedule Success Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Book Appointment Webhook": {
      "main": [
        [
          {
            "node": "Extract Booking Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Required Fields": {
      "main": [
        [
          {
            "node": "Create Calendar Event",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Validation Error Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Availability Webhook": {
      "main": [
        [
          {
            "node": "Extract Availability Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Appointment to Cancel": {
      "main": [
        [
          {
            "node": "Delete Calendar Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find Appointment to Update": {
      "main": [
        [
          {
            "node": "Update Calendar Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manage Appointment Webhook": {
      "main": [
        [
          {
            "node": "Extract Appointment Management Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Time Slot Availability": {
      "main": [
        [
          {
            "node": "Format Unavailable Time Response",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Format Available Time Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Availability Request": {
      "main": [
        [
          {
            "node": "Get Calendar Events for Date",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Calendar Events for Date": {
      "main": [
        [
          {
            "node": "Check Time Slot Availability",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Available Time Response": {
      "main": [
        [
          {
            "node": "Respond - Time Available",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Booking Success Response": {
      "main": [
        [
          {
            "node": "Respond - Booking Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Unavailable Time Response": {
      "main": [
        [
          {
            "node": "Respond - Time Unavailable",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Validation Error Response": {
      "main": [
        [
          {
            "node": "Respond - Validation Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Reschedule Success Response": {
      "main": [
        [
          {
            "node": "Respond - Reschedule Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Appointment Management Data": {
      "main": [
        [
          {
            "node": "Route by Action Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Cancellation Success Response": {
      "main": [
        [
          {
            "node": "Respond - Cancellation Success",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

Use this workflow to book, cancel, or reschedule appointments using Vapi and Google Calendar

Source: https://n8n.io/workflows/12831/ — original creator credit. Request a take-down →

More General workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

General

Vacancy Launch: Calendar, ClickUp & AI LinkedIn Post. Uses googleCalendar, stickyNote, clickUp, linkedIn. Webhook trigger; 18 nodes.

Google Calendar, ClickUp, LinkedIn +1
General

GiveWP Donations to Beacon. Uses httpRequest, stopAndError. Webhook trigger; 43 nodes.

HTTP Request, Stop And Error
General

Remove Video Background & Compose on Custom Image Background with Google Drive. Uses httpRequest, googleDrive. Webhook trigger; 25 nodes.

HTTP Request, Google Drive
General

AI Website Chatbot — Main Handler. Uses httpRequest. Webhook trigger; 22 nodes.

HTTP Request
General

REST API with Google Sheets. Uses googleSheets, respondToWebhook, stickyNote. Webhook trigger; 17 nodes.

Google Sheets