AutomationFlowsData & Sheets › Vapi AI Voice Receptionist to Google Calendar Booking

Vapi AI Voice Receptionist to Google Calendar Booking

Original n8n title: Automate Call Scheduling with Voice AI Receptionist Using Vapi, Google Calendar & Airtable

BySuperAgent @customaistudio on n8n.io

This template is ideal for small businesses, agencies, and solo professionals who want to automate appointment scheduling and caller follow-up through a voice-based AI receptionist. If you’re using tools like Google Calendar, Airtable, and Vapi (Twilio), this setup is for you.

Webhook trigger★★★★★ complexity92 nodesItem ListsGoogle CalendarAirtable
Data & Sheets Trigger: Webhook Nodes: 92 Complexity: ★★★★★ Added:

This workflow corresponds to n8n.io template #3427 — 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
{
  "nodes": [
    {
      "id": "36d0b0d4-b454-4a9b-8168-bcc7942a7cc7",
      "name": "Input Arguments",
      "type": "n8n-nodes-base.set",
      "position": [
        520,
        740
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "ccabe9f4-7911-4488-a75b-7c5779fb2014",
              "name": "timeZone",
              "type": "string",
              "value": "=America/Chicago"
            },
            {
              "id": "b802d976-78f5-4c00-8764-f8c49eaded29",
              "name": "endtime",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.endtime }}"
            },
            {
              "id": "02d58122-6a0f-4bdb-9914-6f50d2af6df4",
              "name": "starttime",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.starttime }}"
            },
            {
              "id": "c1249493-a1d7-4a91-9468-9e5c49430d2e",
              "name": "body.message.toolCalls[0].id",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].id }}"
            },
            {
              "id": "2d1e0d9a-4c70-488e-b430-b8137fd54970",
              "name": "customer.number",
              "type": "string",
              "value": "={{ $json.body.message.call.customer.number }}"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "2d8485ad-9007-4664-9182-7eda25fc96ee",
      "name": "Format response",
      "type": "n8n-nodes-base.itemLists",
      "position": [
        2000,
        840
      ],
      "parameters": {
        "include": "allFieldsExcept",
        "options": {},
        "aggregate": "aggregateAllItemData",
        "operation": "concatenateItems",
        "fieldsToExclude": "sort",
        "destinationFieldName": "response"
      },
      "typeVersion": 3
    },
    {
      "id": "b23c75e0-3697-4137-a595-cf26fedaa898",
      "name": "Sort",
      "type": "n8n-nodes-base.itemLists",
      "position": [
        1760,
        840
      ],
      "parameters": {
        "options": {},
        "operation": "sort",
        "sortFieldsUi": {
          "sortField": [
            {
              "fieldName": "sort"
            }
          ]
        }
      },
      "typeVersion": 3
    },
    {
      "id": "660e3d2f-a424-4e76-8c13-5b62b9f22202",
      "name": "Available Start Times & Ranges",
      "type": "n8n-nodes-base.code",
      "position": [
        2240,
        840
      ],
      "parameters": {
        "jsCode": "// Input data\nconst inputData = $input.all()[0].json.response;\n\n// Define workday hours in CST\nconst WORKDAY_START = \"09:00:00 CST\";\nconst WORKDAY_END = \"18:00:00 CST\";\nconst SLOT_DURATION = 30 * 60 * 1000; // 30 minutes in milliseconds\n\n// Helper to parse CST datetime strings\nconst parseCST = (datetime) => {\n  const parsedDate = new Date(datetime.replace(\" CST\", \"-06:00\"));\n  return isNaN(parsedDate) ? null : parsedDate;\n};\n\n// Function to generate 30-minute start times\nconst generateStartTimes = (start, end) => {\n  const startTimes = [];\n  let current = new Date(start);\n\n  while (current < end) {\n    startTimes.push(\n      current.toLocaleTimeString('en-US', {\n        timeZone: 'CST',\n        hour: '2-digit',\n        minute: '2-digit',\n      })\n    );\n    current = new Date(current.getTime() + SLOT_DURATION);\n  }\n\n  return startTimes;\n};\n\n// Function to find wide open ranges\nconst findWideOpenRanges = (startTimes) => {\n  if (startTimes.length < 3) return []; // Not enough slots for a wide open range\n\n  const ranges = [];\n  let rangeStart = null;\n  let consecutiveCount = 0;\n\n  for (let i = 0; i < startTimes.length - 1; i++) {\n    const currentTime = parseCST(`2000-01-01 ${startTimes[i]} CST`);\n    const nextTime = parseCST(`2000-01-01 ${startTimes[i + 1]} CST`);\n    const diff = nextTime - currentTime;\n\n    if (diff === SLOT_DURATION) {\n      consecutiveCount += 1;\n      if (rangeStart === null) rangeStart = startTimes[i];\n    } else {\n      if (consecutiveCount >= 2) {\n        ranges.push(`${rangeStart} to ${startTimes[i]}`);\n      }\n      rangeStart = null;\n      consecutiveCount = 0;\n    }\n  }\n\n  // Handle the final range\n  if (consecutiveCount >= 2) {\n    ranges.push(`${rangeStart} to ${startTimes[startTimes.length - 1]}`);\n  }\n\n  return ranges;\n};\n\n// Group meetings by date, ignoring invalid dates\nconst meetingsByDate = inputData.reduce((acc, meeting) => {\n  const start = parseCST(meeting.start);\n  const end = parseCST(meeting.end);\n\n  if (!start || !end) {\n    return acc; // Ignore invalid dates\n  }\n\n  const dateKey = start.toISOString().split('T')[0];\n\n  if (!acc[dateKey]) {\n    acc[dateKey] = [];\n  }\n\n  acc[dateKey].push({ start, end });\n  return acc;\n}, {});\n\n// Generate availability\nconst availability = Object.keys(meetingsByDate)\n  .filter((date) => {\n    // Exclude Saturdays (6) and Sundays (0)\n    const dayOfWeek = new Date(date).getUTCDay();\n    return dayOfWeek !== 0 && dayOfWeek !== 6;\n  })\n  .map((date) => {\n    const workdayStart = parseCST(`${date} ${WORKDAY_START}`);\n    const workdayEnd = parseCST(`${date} ${WORKDAY_END}`);\n\n    const dayMeetings = meetingsByDate[date].sort((a, b) => a.start - b.start);\n\n    let availableStartTimes = [];\n    let lastEnd = workdayStart;\n\n    for (const meeting of dayMeetings) {\n      if (meeting.start > lastEnd) {\n        availableStartTimes = availableStartTimes.concat(generateStartTimes(lastEnd, meeting.start));\n      }\n      lastEnd = meeting.end > lastEnd ? meeting.end : lastEnd;\n    }\n\n    if (lastEnd < workdayEnd) {\n      availableStartTimes = availableStartTimes.concat(generateStartTimes(lastEnd, workdayEnd));\n    }\n\n    const wideOpenRanges = findWideOpenRanges(availableStartTimes);\n\n    return {\n      date: new Date(date).toLocaleDateString('en-US', {\n        weekday: 'long',\n        year: 'numeric',\n        month: 'long',\n        day: 'numeric',\n      }),\n      availableStartTimes,\n      wideOpenRanges,\n    };\n  });\n\n// Format output as plaintext\nconst availableTimes = availability\n  .map(({ date, availableStartTimes, wideOpenRanges }) => {\n    const times = availableStartTimes.map((time) => `- ${time}`).join('\\n');\n    const ranges = wideOpenRanges.length\n      ? `Wide Open Ranges:\\n${wideOpenRanges.map((range) => `- ${range}`).join('\\n')}`\n      : \"Wide Open Ranges: None\";\n\n    return `### ${date}\\nAvailable Start Times:\\n${times}\\n\\n${ranges}`;\n  })\n  .join('\\n\\n');\n\n// Set the output\nreturn {\n  json: {\n    availableTimes,\n  },\n};\n"
      },
      "typeVersion": 2
    },
    {
      "id": "f3110658-2f90-4b19-9874-7d6c4e108895",
      "name": "Flatten Slots",
      "type": "n8n-nodes-base.code",
      "position": [
        2460,
        840
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const flattenSlots = (data) => {\n  // If data is missing or empty, return an empty array of slots\n  if (!data) {\n    return { slots: [] };\n  }\n\n  // data is an object whose keys are dates\n  // each date key has an array of slot objects\n  // we just need to flatten them all into one array\n  const flattened = Object.values(data).flat();  // merges all arrays from each date key\n\n  // Return a new object with a single 'slots' array\n  return { slots: flattened };\n};\n\n// Then assign the flattened slots back to $input.item.json.data\n$input.item.json.data = flattenSlots($input.item.json.data);\nreturn $input.item;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "5065439e-34e3-4eaf-8226-8ba7393a5cf3",
      "name": "Enrich Date",
      "type": "n8n-nodes-base.code",
      "position": [
        2680,
        840
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "function formatTimeSlot(dateString) {\n  // Format options for date/time with America/Chicago timezone\n  const options = {\n    timeZone: 'America/Chicago',\n    weekday: 'long',\n    month: 'long',\n    day: 'numeric',\n    hour: 'numeric',\n    minute: 'numeric',\n    hour12: true\n  };\n\n  // Create a formatter with timezone support\n  const dateFormatter = new Intl.DateTimeFormat('en-US', options);\n  \n  // Format the date/time string\n  return dateFormatter.format(new Date(dateString));\n}\n\n// Process each slot and add formatted time strings to the result\nconst slots = $input.item.json.data.slots;\nconst formattedSlots = slots.map(slot => formatTimeSlot(slot.start));\n\n// Attach formatted results to the output\n$input.item.json.data.slots = formattedSlots;\n\nreturn $input.item;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "d8ed3a92-697b-4718-b65f-5276c9a9bfaf",
      "name": "Build Response Payload",
      "type": "n8n-nodes-base.set",
      "position": [
        2900,
        840
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "5cb05b10-e916-459e-84a2-9c314a859a07",
              "name": "results[0].toolCallId",
              "type": "string",
              "value": "={{ $('Input Arguments').item.json.body.message.toolCalls[0].id }}"
            },
            {
              "id": "552246f9-7afd-404e-9fb3-cb38c7447359",
              "name": "results[0].result",
              "type": "string",
              "value": "={{ $json.availableTimes }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "a0697944-c5a6-4ca1-9948-8248940841b2",
      "name": "Booking Payload",
      "type": "n8n-nodes-base.set",
      "position": [
        1980,
        1400
      ],
      "parameters": {
        "options": {
          "ignoreConversionErrors": true
        },
        "assignments": {
          "assignments": [
            {
              "id": "05bbc797-b781-489c-ab70-e234fe17eb62",
              "name": "id",
              "type": "number",
              "value": "={{ $json.id }}"
            },
            {
              "id": "4bb68abf-18c8-4445-b446-21667abd95aa",
              "name": "description",
              "type": "string",
              "value": "={{ $json.description }}"
            },
            {
              "id": "74a98b77-b9fe-40cc-84c8-fc7303c5cfa6",
              "name": "startTime",
              "type": "string",
              "value": "={{ $json.start.dateTime }}"
            },
            {
              "id": "2934d6a7-9e6b-4038-891c-0b05ba18cb21",
              "name": "endTime",
              "type": "string",
              "value": "={{ $json.end.dateTime }}"
            },
            {
              "id": "10f091c8-5e52-40dc-a294-87625be9af99",
              "name": "status",
              "type": "string",
              "value": "={{ $json.status }}"
            },
            {
              "id": "cdc5e1ab-a29b-447f-8343-ff1c1b168717",
              "name": "Timezone",
              "type": "string",
              "value": "={{ $json.end.timeZone }}"
            },
            {
              "id": "f5b6820c-ab4b-496c-9957-f86753243388",
              "name": "attendees",
              "type": "array",
              "value": "={{ $json.attendees }}"
            },
            {
              "id": "b39a06a5-4fbf-4fdf-9d9a-a07dcb37d157",
              "name": "hangoutLink",
              "type": "string",
              "value": "={{ $json.hangoutLink }}"
            },
            {
              "id": "345f49fc-93bc-48b8-9ced-326139a82119",
              "name": "Title",
              "type": "string",
              "value": "={{ $json.summary }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "4f7b157c-f657-48fa-8bb5-a1e074b042eb",
      "name": "Success Response",
      "type": "n8n-nodes-base.set",
      "position": [
        2200,
        1400
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "2c3894da-7bf7-4a35-95c0-d3d9199dd0ad",
              "name": "results[0].toolCallId",
              "type": "string",
              "value": "={{ $('Input Arguments from booking tools').item.json.toolCallId }}"
            },
            {
              "id": "685c67c7-a30b-4bcc-b9ba-827c4b570548",
              "name": "results[0].result",
              "type": "string",
              "value": "={{ $json.status }}"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "b7fe16e3-b625-4cb4-b971-9c26698af89b",
      "name": "Add Friendly Error",
      "type": "n8n-nodes-base.code",
      "position": [
        1980,
        1760
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "function replaceValue(value) {\n  if (error.message.include('no_available_users_found_error')) {\n    return \"This time slot is no longer available.\";\n  }\n  return value;\n}\n\n$input.item.json.message = replaceValue($input.item.json.error.description);\n\nreturn $input.item;"
      },
      "typeVersion": 2
    },
    {
      "id": "b5bff0df-2bef-4c43-9fcf-91cadc68b7ca",
      "name": "Error Response",
      "type": "n8n-nodes-base.set",
      "position": [
        2200,
        1760
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "2c3894da-7bf7-4a35-95c0-d3d9199dd0ad",
              "name": "results[0].toolCallId",
              "type": "string",
              "value": "={{ $('Input Arguments from booking tools').item.json.toolCallId }}"
            },
            {
              "id": "93e45166-de94-4fa5-9148-2b8d0e4b960c",
              "name": "results[0].result",
              "type": "string",
              "value": "={{ $json.message || $json.status }}"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "fe62c0bc-2d73-4f14-8e76-02847ef4e14a",
      "name": "Escape Json",
      "type": "n8n-nodes-base.code",
      "position": [
        1260,
        1580
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "const escapeStringForJson = (str) => {\n    return str\n        .replace(/\\\\/g, '\\\\\\\\') // Escape backslashes\n        .replace(/\"/g, '\\\\\"')    // Escape double quotes\n        .replace(/\\n/g, '\\\\n')   // Escape newlines\n        .replace(/\\r/g, '\\\\r')   // Escape carriage returns\n        .replace(/\\t/g, '\\\\t');  // Escape tabs\n};\n\n// Escape the notes field\n$input.item.json.notes = escapeStringForJson($input.item.json.notes);\n\nreturn $input.item;\n"
      },
      "typeVersion": 2
    },
    {
      "id": "17927aa4-8f91-4134-b914-1160a724226f",
      "name": "Has all information",
      "type": "n8n-nodes-base.if",
      "position": [
        940,
        1800
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "e0af7f69-0c89-4a02-a49f-dd5a90e31dff",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ ($json.email || \"\").isEmail() }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "6f0bb9e6-9d82-4cc6-a98f-4d00c47ed910",
      "name": "Respond with Error",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1480,
        1900
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 1.1
    },
    {
      "id": "fdedba34-a374-405d-a86e-0b0a1759ede9",
      "name": "Build Error Response Payload",
      "type": "n8n-nodes-base.set",
      "position": [
        1260,
        1900
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "5cb05b10-e916-459e-84a2-9c314a859a07",
              "name": "results[0].toolCallId",
              "type": "string",
              "value": "={{ $('Input Arguments from booking tools').item.json.toolCallId }}"
            },
            {
              "id": "552246f9-7afd-404e-9fb3-cb38c7447359",
              "name": "results[0].result",
              "type": "string",
              "value": "=You must provide an email, name and notes to call this tool"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "1e111f9d-bb43-4126-b7a2-3353e7c7c72f",
      "name": "Build Error Response Payload2",
      "type": "n8n-nodes-base.set",
      "position": [
        1560,
        2840
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "5cb05b10-e916-459e-84a2-9c314a859a07",
              "name": "results[0].toolCallId",
              "type": "string",
              "value": "={{ $('Input Arguments from updateslot tool').item.json.toolCallId || $json.Calls[0].id }}"
            },
            {
              "id": "552246f9-7afd-404e-9fb3-cb38c7447359",
              "name": "results[0].result",
              "type": "string",
              "value": "=You must provide an email, name , previous starttime & endtime and resceduled starttime to call this tool"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "d6cbad26-d974-4a11-b0fd-2a35bb555378",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        260,
        620
      ],
      "parameters": {
        "color": 4,
        "width": 190,
        "height": 80,
        "content": "# Get Slots"
      },
      "typeVersion": 1
    },
    {
      "id": "bcccc8cb-2e9d-4f8b-9964-e4d656e794ed",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        740,
        920
      ],
      "parameters": {
        "width": 230,
        "height": 80,
        "content": "## Check Availability\n"
      },
      "typeVersion": 1
    },
    {
      "id": "30b34e37-ee7a-434c-ab4d-445df994459a",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1320,
        520
      ],
      "parameters": {
        "width": 310,
        "height": 80,
        "content": "## If time available Respond\n"
      },
      "typeVersion": 1
    },
    {
      "id": "725a9b59-ea66-4326-a410-93a723157ced",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1280,
        1020
      ],
      "parameters": {
        "width": 190,
        "height": 80,
        "content": "## Get All Events\n"
      },
      "typeVersion": 1
    },
    {
      "id": "1f2bf4a3-8aeb-4a56-8bff-0bb370e12718",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2140,
        1020
      ],
      "parameters": {
        "width": 350,
        "height": 100,
        "content": "## Get Available Slots\n\nFormat the slots and Enrich the date and timings\n"
      },
      "typeVersion": 1
    },
    {
      "id": "5909d88f-b9c6-4e62-b1e3-bdc1d05ad7aa",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3280,
        1000
      ],
      "parameters": {
        "width": 230,
        "height": 80,
        "content": "## Respond to Vapi"
      },
      "typeVersion": 1
    },
    {
      "id": "f627c5b0-f3b6-4f95-a3a3-2c1b7e2860c7",
      "name": "Sticky Note BookSlot Webhook",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        380,
        1680
      ],
      "parameters": {
        "color": 5,
        "width": 190,
        "height": 80,
        "content": "# Book Slot"
      },
      "typeVersion": 1
    },
    {
      "id": "9c3e8b9f-3fe3-4380-8cbc-413146d752b9",
      "name": "Sticky Note BookSlot Check",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        880,
        1700
      ],
      "parameters": {
        "width": 230,
        "height": 80,
        "content": "Checks if required booking info (email, name, etc.) is provided."
      },
      "typeVersion": 1
    },
    {
      "id": "c723bbd0-5a04-4efb-ba67-59bc722b9d4e",
      "name": "Sticky Note BookSlot Error",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1440,
        2060
      ],
      "parameters": {
        "width": 190,
        "height": 80,
        "content": "If info missing, sends error back."
      },
      "typeVersion": 1
    },
    {
      "id": "a843e795-8046-4538-93e0-2de2e688c863",
      "name": "Sticky Note BookSlot GCal",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1660,
        1740
      ],
      "parameters": {
        "width": 190,
        "height": 80,
        "content": "Books the appointment in Google Calendar."
      },
      "typeVersion": 1
    },
    {
      "id": "a7627281-15fc-438a-b031-b00cbc4b9fa4",
      "name": "Sticky Note BookSlot Error Handle",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1920,
        1920
      ],
      "parameters": {
        "width": 230,
        "height": 80,
        "content": "Handles potential booking errors (e.g., slot taken)."
      },
      "typeVersion": 1
    },
    {
      "id": "71c0c722-b5df-47d7-97e6-3d23533a4a4e",
      "name": "Sticky Note BookSlot Response",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2420,
        1740
      ],
      "parameters": {
        "width": 210,
        "height": 80,
        "content": "Sends confirmation/error back to Vapi."
      },
      "typeVersion": 1
    },
    {
      "id": "4e598ebb-cfdb-432f-a01a-bb76d1d20f24",
      "name": "Sticky Note BookSlot Airtable",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3100,
        1740
      ],
      "parameters": {
        "width": 230,
        "height": 80,
        "content": "Logs the confirmed booking details to Airtable."
      },
      "typeVersion": 1
    },
    {
      "id": "cc085c75-d45f-4453-b78b-1b9b480fb02c",
      "name": "Sticky Note CancelSlot Webhook",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        440,
        3480
      ],
      "parameters": {
        "color": 3,
        "width": 250,
        "height": 80,
        "content": "# Cancel Slots"
      },
      "typeVersion": 1
    },
    {
      "id": "9504b7e8-7964-4a0e-bfa4-32540c1fb895",
      "name": "Sticky Note CancelSlot Check",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        960,
        3780
      ],
      "parameters": {
        "width": 230,
        "height": 80,
        "content": "Checks if required info (email, name, start time) is provided."
      },
      "typeVersion": 1
    },
    {
      "id": "3bb9d976-d922-4016-839c-22e8b1adcf35",
      "name": "Sticky Note CancelSlot Error",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1520,
        3940
      ],
      "parameters": {
        "width": 150,
        "height": 80,
        "content": "If info missing, sends error back."
      },
      "typeVersion": 1
    },
    {
      "id": "87442b3a-b8eb-43e6-b15d-0240a58bff79",
      "name": "Sticky Note CancelSlot Search",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1300,
        3440
      ],
      "parameters": {
        "width": 190,
        "height": 100,
        "content": "Finds the appointment record in Airtable by phone number to get event ID."
      },
      "typeVersion": 1
    },
    {
      "id": "4e0cec59-1acc-4604-80ce-09479c7a6652",
      "name": "Sticky Note CancelSlot GCal Delete",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1720,
        3720
      ],
      "parameters": {
        "width": 190,
        "height": 80,
        "content": "Deletes the event from Google Calendar using event ID."
      },
      "typeVersion": 1
    },
    {
      "id": "68e00556-93d2-45a8-9fee-deb1477ffff2",
      "name": "Sticky Note CancelSlot Airtable Update",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2060,
        3400
      ],
      "parameters": {
        "width": 190,
        "height": 80,
        "content": "Updates Airtable record status to 'Canceled'."
      },
      "typeVersion": 1
    },
    {
      "id": "5853b0f6-1e73-435e-aff8-5d9d8de53693",
      "name": "Sticky Note CancelSlot Response",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2380,
        3740
      ],
      "parameters": {
        "width": 190,
        "height": 80,
        "content": "Sends cancellation confirmation/error back to Vapi."
      },
      "typeVersion": 1
    },
    {
      "id": "660cdb51-84ac-434e-b7d8-f7b17ef7ef5b",
      "name": "Sticky Note UpdateSlot Webhook",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        460,
        2600
      ],
      "parameters": {
        "color": 6,
        "width": 250,
        "height": 80,
        "content": "# Update Slots"
      },
      "typeVersion": 1
    },
    {
      "id": "030e4bce-4b8b-42b7-8cf4-86b3a88f375b",
      "name": "Sticky Note UpdateSlot Check",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1000,
        2900
      ],
      "parameters": {
        "width": 230,
        "height": 80,
        "content": "Checks if required info (email, name, old/new times) is provided."
      },
      "typeVersion": 1
    },
    {
      "id": "02e9f8e3-7561-4ace-95a2-2b1807940f1a",
      "name": "Sticky Note UpdateSlot Error",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1500,
        3020
      ],
      "parameters": {
        "width": 190,
        "height": 80,
        "content": "If info missing, sends error back."
      },
      "typeVersion": 1
    },
    {
      "id": "4820cb6c-de15-4e9a-bca7-e3f172af6b80",
      "name": "Sticky Note UpdateSlot Search",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1580,
        2460
      ],
      "parameters": {
        "width": 190,
        "height": 80,
        "content": "Finds original appointment in Airtable by old phone number"
      },
      "typeVersion": 1
    },
    {
      "id": "5dd3075e-8e0b-4f76-8d21-39aa66d449da",
      "name": "Sticky Note UpdateSlot GCal Update",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1940,
        2720
      ],
      "parameters": {
        "width": 190,
        "height": 80,
        "content": "Updates the event time in Google Calendar."
      },
      "typeVersion": 1
    },
    {
      "id": "2e08b4f6-f279-4a9e-9bd3-d6a6283b45f4",
      "name": "Sticky Note UpdateSlot Airtable Update",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2240,
        2320
      ],
      "parameters": {
        "width": 170,
        "height": 100,
        "content": "Updates Airtable record with new times & 'Updated' status."
      },
      "typeVersion": 1
    },
    {
      "id": "d1369c19-401e-4ce2-a21e-0d5ae01af119",
      "name": "Sticky Note UpdateSlot Response",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2720,
        2460
      ],
      "parameters": {
        "width": 230,
        "height": 80,
        "content": "Sends rescheduling confirmation/error back to Vapi."
      },
      "typeVersion": 1
    },
    {
      "id": "ca064fbe-b175-443f-b8e8-70a2a7551ba9",
      "name": "Sticky Note CallResults Webhook",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        440,
        4160
      ],
      "parameters": {
        "color": 2,
        "width": 390,
        "height": 120,
        "content": "# Call Result logs\nReceives call summary and recording details post-call."
      },
      "typeVersion": 1
    },
    {
      "id": "4941246b-82d1-4f16-b7b8-e1fdb6e7c833",
      "name": "Sticky Note CallResults Airtable",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        860,
        4460
      ],
      "parameters": {
        "width": 230,
        "height": 80,
        "content": "Logs call transcript, recording URL, summary, cost, customer number to Airtable."
      },
      "typeVersion": 1
    },
    {
      "id": "e5622e9e-9b0a-43b2-ab80-e3e33a4b0409",
      "name": "Getslot_tool",
      "type": "n8n-nodes-base.webhook",
      "position": [
        260,
        740
      ],
      "parameters": {
        "path": "getslots",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "e15781cf-5405-4f60-aa6d-ba19d1b7dabc",
      "name": "Check Availability",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        800,
        740
      ],
      "parameters": {
        "options": {},
        "timeMax": "={{ $json.endtime.toDateTime() || $now.plus(1, 'hour').toISO() }}",
        "timeMin": "={{ $json.starttime.toDateTime() }}",
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "user@example.com",
          "cachedResultName": "user@example.com"
        },
        "resource": "calendar"
      },
      "credentials": {},
      "typeVersion": 1.3
    },
    {
      "id": "1e064283-2964-4eba-a893-e4270157c603",
      "name": "Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        1540,
        640
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={\n    \"results\":[\n        {\n            \"toolCallId\":\"{{ $('Getslot_tool').first().json.body.message.toolCalls[0].id }}\",\n            \"result\":\"available:{{ $json.available }}\"\n        }\n    ]\n}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "498401cb-00e5-4fdd-b6a9-dd3e91376993",
      "name": "Check if time is available or not",
      "type": "n8n-nodes-base.if",
      "position": [
        1020,
        740
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "4a8741a2-a903-4fb7-b0a3-5c74c7eea6ca",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.available }}",
              "rightValue": "="
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "96e43c15-a332-4acf-af04-80dd989d5660",
      "name": "Time available (true) & Call_id",
      "type": "n8n-nodes-base.set",
      "position": [
        1320,
        640
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "f582d965-af15-4ecf-8a8c-d8bf6c0d15c1",
              "name": "body.message.toolCalls[0].id",
              "type": "string",
              "value": "={{ $('Input Arguments').item.json.body.message.toolCalls[0].id }}"
            },
            {
              "id": "834ee925-5c8d-4e46-aeee-f399dc1ff40c",
              "name": "available",
              "type": "boolean",
              "value": "={{ $json.available }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "fb7ad8c6-9f78-4518-b955-60f3f7088cb9",
      "name": "Get All Calendar Events",
      "type": "n8n-nodes-base.googleCalendar",
      "position": [
        1320,
        840
      ],
      "parameters": {
        "options": {
          "orderBy": "startTime",
          "timeMax": "={{ $now.plus(1, 'week').toISO() }}",
          "timeMin": "={{ $now.toISO() }}",
          "singleEvents": true
        },
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "user@example.com",
          "cachedResultName": "user@example.com"
        },
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {},
      "typeVersion": 1,
      "alwaysOutputData": true
    },
    {
      "id": "390599ee-ddeb-4628-af0b-36fbdd357cee",
      "name": "Extract start, end and name",
      "type": "n8n-nodes-base.set",
      "position": [
        1540,
        840
      ],
      "parameters": {
        "options": {
          "ignoreConversionErrors": true
        },
        "assignments": {
          "assignments": [
            {
              "id": "1045b97f-c76f-450e-8f57-+1234567890",
              "name": "start",
              "type": "string",
              "value": "={{ DateTime.fromISO($json.start.dateTime).toLocaleString(DateTime.DATE_HUGE) }}, {{ DateTime.fromISO($json.start.dateTime).toLocaleString(DateTime.TIME_24_WITH_SHORT_OFFSET) }}"
            },
            {
              "id": "457e3a2b-d33e-4a65-b2da-d19ad9d754ac",
              "name": "end",
              "type": "string",
              "value": "={{ DateTime.fromISO($json.end.dateTime).toLocaleString(DateTime.DATE_HUGE) }}, {{ DateTime.fromISO($json.end.dateTime).toLocaleString(DateTime.TIME_24_WITH_SHORT_OFFSET) }}"
            },
            {
              "id": "b6802452-557e-4568-af14-4574e8ecc013",
              "name": "name",
              "type": "string",
              "value": "={{ $json.summary }}"
            },
            {
              "id": "799b656f-68b6-467c-88a1-217ff7c7801b",
              "name": "sort",
              "type": "string",
              "value": "={{ $json.start.dateTime }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "2c9a73da-37b7-4abd-af5e-695036cd2c2b",
      "name": "Convert into Json format for Vapi",
      "type": "n8n-nodes-base.code",
      "position": [
        3120,
        840
      ],
      "parameters": {
        "jsCode": "// Get the input data for the first item\nconst inputData = $input.first().json;\nconsole.log(\"Input Data:\", inputData); // Log input for debugging\n\n// Access the message string from the correct path within the input structure.\n// The input comes from the \"Build Response Payload\" node, which structures data under 'results'.\n// Use optional chaining (?.) for safety in case the structure is not as expected.\nlet message = inputData.results?.[0]?.result;\n\n// Check if the message was found and is a string\nif (typeof message !== 'string') {\n    console.error(\"Could not find the message string at inputData.results[0].result or it's not a string. Input:\", inputData);\n    // Return an object with an empty message or an error indicator\n    return { message: \"\" }; // Or potentially throw an error: throw new Error(\"Input message not found or not a string\");\n}\n\n// Start cleaning the message string\n\n// 1. Replace the literal string \"\\\\n\" (backslash followed by n) with a space.\n// This handles the newline representation seen in the input screenshot.\nlet cleanedMessage = message.replace(/\\\\n/g, ' ');\n\n// 2. Remove spaces immediately surrounding colons (e.g., \"Times : \" becomes \"Times:\").\ncleanedMessage = cleanedMessage.replace(/\\s*:\\s*/g, ':');\n\n// 3. Replace sequences of multiple whitespace characters (including spaces from replaced \\n)\n// with a single space. Then, trim any leading or trailing whitespace from the result.\ncleanedMessage = cleanedMessage.replace(/\\s+/g, ' ').trim();\n\n// Create the final output JSON object containing the cleaned message.\nconst output = {\n    message: cleanedMessage\n};\n\n// Return the output object. This will be the output of the Code node.\nreturn output;"
      },
      "typeVersion": 2
    },
    {
      "id": "e00cf72a-af6a-441b-9b76-81bd8096d3df",
      "name": "Response to Vapi",
      "type": "n8n-nodes-base.respondToWebhook",
      "position": [
        3360,
        840
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={\n    \"results\":[\n        {\n            \"toolCallId\":\"{{ $('Getslot_tool').first().json.body.message.toolCalls[0].id }}\",\n            \"result\":\"The original time is not available, here are available slots:{{ $json.message }}\"\n        }\n    ]\n}"
      },
      "typeVersion": 1.1
    },
    {
      "id": "facf3bf9-e05e-4953-a221-bf7f566a3b0f",
      "name": "bookslots_tool",
      "type": "n8n-nodes-base.webhook",
      "position": [
        400,
        1800
      ],
      "parameters": {
        "path": "bookslots",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "9266904b-300f-4c83-a518-4cd69b13de41",
      "name": "Input Arguments from booking tools",
      "type": "n8n-nodes-base.set",
      "position": [
        720,
        1800
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "eac930a3-ba65-4b0d-b236-aa167d7edb3f",
              "name": "toolCallId",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].id }}"
            },
            {
              "id": "492186b8-e3a3-4ab9-87f4-45d8cbc38c13",
              "name": "timeZone",
              "type": "string",
              "value": "=America/Chicago"
            },
            {
              "id": "12aeec42-9414-4d43-8837-1ff747f49305",
              "name": "name",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.name || \"John Smith\" }}"
            },
            {
              "id": "36673f27-c026-4ad9-81da-ad11e71bbfb6",
              "name": "email",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.email }}"
            },
            {
              "id": "469ddc00-a399-47a5-8c55-97cd3adf4143",
              "name": "language",
              "type": "string",
              "value": "en"
            },
            {
              "id": "b191cd98-f3f7-48b1-a2e0-2c9e248a4983",
              "name": "notes",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.notes || \"\"}}"
            },
            {
              "id": "783cb161-65e4-4829-ac90-5c6c2c55585f",
              "name": "starttime",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.starttime }}"
            },
            {
              "id": "bfcdade9-14c8-4867-8a22-3865a2bcc116",
              "name": "endtime",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.endtime }}"
            },
            {
              "id": "26ca39ef-48f5-41ed-990e-40c2a26d6132",
              "name": "Tittle",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.Title }}"
            },
            {
              "id": "43575f7a-3873-4d74-90c5-4467c7779514",
              "name": "customer_number",
              "type": "string",
              "value": "={{ $json.body.message.call.customer.number }}"
            }
          ]
        }
      },
      "typeVersion": 3.3
    },
    {
      "id": "b4bc5cee-d631-4aa8-a3ff-59a0b647d36a",
      "name": "Convert time to CST America / Chicago",
      "type": "n8n-nodes-base.code",
      "position": [
        1480,
        1580
      ],
      "parameters": {
        "jsCode": "// Get all input items\nconst items = $input.all();\n\n// Loop through each item\nfor (const item of items) {\n  // Get the values from the current item's JSON data\n  const startTimeUTC = item.json.starttime;\n  const endTimeUTC = item.json.endtime;\n  const targetTimeZone = item.json.timeZone; // e.g., \"America/Chicago\"\n\n  // Basic validation: ensure the necessary fields exist\n  if (!startTimeUTC || !endTimeUTC || !targetTimeZone) {\n    console.warn(`Skipping item due to missing time data or timezone. Item JSON: ${JSON.stringify(item.json)}`);\n    item.json.conversionError = \"Missing starttime, endtime, or timeZone\";\n    continue; // Move to the next item\n  }\n\n  try {\n    // --- Start Time Conversion ---\n    // Parse the original UTC ISO string using Luxon (NO $ prefix)\n    const startDt = luxon.DateTime.fromISO(startTimeUTC, { zone: 'utc' });\n\n    // Convert the DateTime object to the target timezone\n    const startDtTargetZone = startDt.setZone(targetTimeZone);\n\n    // Check if the conversion was valid\n    if (!startDtTargetZone.isValid) {\n      throw new Error(`Failed to convert start time. Reason: ${startDtTargetZone.invalidReason || 'Unknown'}`);\n    }\n\n    // Format the result back into an ISO string with the correct offset\n    item.json.starttime = startDtTargetZone.toISO();\n\n    // --- End Time Conversion ---\n    // Parse the original UTC ISO string using Luxon (NO $ prefix)\n    const endDt = luxon.DateTime.fromISO(endTimeUTC, { zone: 'utc' });\n\n    // Convert the DateTime object to the target timezone\n    const endDtTargetZone = endDt.setZone(targetTimeZone);\n\n     // Check if the conversion was valid\n    if (!endDtTargetZone.isValid) {\n      throw new Error(`Failed to convert end time. Reason: ${endDtTargetZone.invalidReason || 'Unknown'}`);\n    }\n\n    // Format the result back into an ISO string with the correct offset\n    item.json.endtime = endDtTargetZone.toISO();\n\n    // Optionally remove the error flag if conversion was successful this time\n    delete item.json.conversionError;\n\n  } catch (error) {\n    console.error(`Error converting time for item: ${JSON.stringify(item.json)}. Error: ${error.message}`);\n    // Add/update the error flag to the item's JSON\n    item.json.conversionError = error.message;\n  }\n}\n// Return the modified array of items\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "2c8b2884-14d3-4bd4-92d8-6e402ca3a8de",
      "name": "Create Event",
      "type": "n8n-nodes-base.googleCalendar",
      "onError": "continueErrorOutput",
      "position": [
        1700,
        1580
      ],
      "parameters": {
        "end": "={{ $json.endtime }}",
        "start": "={{ $json.starttime }}",
        "calendar": {
          "__rl": true,
          "mode": "list",
          "value": "user@example.com",
          "cachedResultName": "user@example.com"
        },
        "additionalFields": {
          "allday": "no",
          "summary": "={{ $json.Tittle }}",
          "showMeAs": "opaque",
          "attendees": [
            "={{ $json.email }}"
          ],
          "description": "={{ $json.notes }}",
          "conferenceDataUi": {
            "conferenceDataValues": {
              "conferenceSolution": "hangoutsMeet"
            }
          }
        }
      },
      "credentials": {},
      "typeVersion": 1.3
    },
    {
      "id": "b8890ab2-9850-4608-996d-45c8a6d3a52e",
      "name": "Respond to Vapi",
      "type": "n8n-nodes-base.respondToWebhook",
      "onError": "continueRegularOutput",
      "position": [
        2480,
        1580
      ],
      "parameters": {
        "options": {},
        "respondWith": "json",
        "responseBody": "={\n    \"results\":[\n        {\n            \"toolCallId\":\"{{ $json.results[0].toolCallId }}\",\n            \"result\":\"available:{{ $json.results[0].result }}\"\n        }\n    ]\n}"
      },
      "typeVersion": 1.1,
      "alwaysOutputData": true
    },
    {
      "id": "77f75f42-46bb-47f5-8a43-55543ae46f10",
      "name": "If the booking is confirmed then true",
      "type": "n8n-nodes-base.if",
      "position": [
        2700,
        1580
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "932dd430-309b-4d3b-8bf6-768f84fd2dd2",
              "operator": {
                "name": "filter.operator.equals",
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.results[0].result }}",
              "rightValue": "=confirmed"
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "230ddb29-67f0-4486-a6f3-f4dd3dbbee42",
      "name": "Information to be Saved in Airtable",
      "type": "n8n-nodes-base.set",
      "position": [
        2940,
        1560
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "b103265d-86da-4256-994d-85a78f33f933",
              "name": "startTime",
              "type": "string",
              "value": "={{ $('Booking Payload').item.json.startTime }}"
            },
            {
              "id": "a8e6e9c5-6ebb-48d8-951f-b007bed2421d",
              "name": "endTime",
              "type": "string",
              "value": "={{ $('Booking Payload').item.json.endTime }}"
            },
            {
              "id": "d4bcb1d1-043a-4205-8488-0a67b4e7b582",
              "name": "status",
              "type": "string",
              "value": "={{ $('Booking Payload').item.json.status }}"
            },
            {
              "id": "92ac8c99-ad94-4b3c-9c5e-ba032dac2255",
              "name": "description",
              "type": "string",
              "value": "={{ $('Booking Payload').item.json.description }}"
            },
            {
              "id": "98c5653d-1e0e-4a6a-8630-17802d437593",
              "name": "attendees[0].email",
              "type": "string",
              "value": "={{ $('Booking Payload').item.json.attendees[0].email }}"
            },
            {
              "id": "f94bdfc1-dc74-4675-ad29-19244fb21ebe",
              "name": "attendees[0].responseStatus",
              "type": "string",
              "value": "={{ $('Booking Payload').item.json.attendees[0].responseStatus }}"
            },
            {
              "id": "12bd5ed5-4934-4c19-a9b9-54fe989eaa4f",
              "name": "hangoutLink",
              "type": "string",
              "value": "={{ $('Booking Payload').item.json.hangoutLink }}"
            },
            {
              "id": "5b1f9356-7d62-4999-ae4e-86f3f20d72bf",
              "name": "attendee.name",
              "type": "string",
              "value": "={{ $('bookslots_tool').item.json.body.message.toolCalls[0].function.arguments.name }}"
            },
            {
              "id": "6e93805e-8754-4f92-870f-7b46525f3eb3",
              "name": "call.id",
              "type": "string",
              "value": "={{ $('bookslots_tool').item.json.body.message.call.id }}"
            },
            {
              "id": "f174e2be-3230-4fc9-970b-971aff6e9b8e",
              "name": "assistant.name",
              "type": "string",
              "value": "={{ $('bookslots_tool').item.json.body.message.assistant.name }}"
            },
            {
              "id": "a4bc9d70-7d51-487f-b622-433e767ef71f",
              "name": "event.id",
              "type": "string",
              "value": "={{ $('Create Event').item.json.id }}"
            },
            {
              "id": "9259b1d3-3658-4ab5-b434-364e6a84d145",
              "name": "Title",
              "type": "string",
              "value": "={{ $('Booking Payload').item.json.Title }}"
            },
            {
              "id": "2102a7be-5d74-458f-bafd-21651e24adb1",
              "name": "customer_number",
              "type": "string",
              "value": "={{ $('Input Arguments from booking tools').item.json.customer_number}}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "f6c7774d-a8c7-466a-ba77-401194fe6fb4",
      "name": "Logs the confirmed booking details",
      "type": "n8n-nodes-base.airtable",
      "position": [
        3160,
        1560
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": "appnj853UnMRnJ8D3",
          "cachedResultUrl": "https://airtable.com/appnj853UnMRnJ8D3",
          "cachedResultName": "Voice Receptionist Agent"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "tblF8LF9lmkHMbk7v",
          "cachedResultUrl": "https://airtable.com/appnj853UnMRnJ8D3/tblF8LF9lmkHMbk7v",
          "cachedResultName": "Appointments"
        },
        "columns": {
          "value": {
            "Name": "={{ $json.attendee.name }}",
            "Email": "={{ $json.attendees[0].email }}",
            "endtime": "={{ $json.endTime }}",
            "eventId": "={{ $json.event.id }}",
            "meetlink": "={{ $json.hangoutLink }}",
            "starttime": "={{ $json.startTime }}",
            "Voice Agent": "={{ [$json.assistant.name] }}",
            "Phone Number": "={{ $json.customer_number }}",
            "Booking Status": "={{ $json.status }}",
            "CallRecordingId": "={{ [$json.call.id] }}",
            "meetdescription": "={{ $json.Title }} {{ $json.description }}"
          },
          "schema": [
            {
              "id": "Email",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Email",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Phone Number",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Phone Number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Name",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Name",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Booking Status",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Booking Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "CallRecordingId",
              "type": "array",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "CallRecordingId",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "starttime",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "starttime",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "endtime",
              "type": "dateTime",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "endtime",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "meetlink",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "meetlink",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "meetdescription",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "meetdescription",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Voice Agent",
              "type": "array",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Voice Agent",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "eventId",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "eventId",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Appointments",
              "type": "string",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "Appointments",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Email"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "typecast": true
        },
        "operation": "create"
      },
      "credentials": {},
      "typeVersion": 2.1
    },
    {
      "id": "154bee14-9281-4b92-8204-57c5436785ba",
      "name": "Updateslots_tool",
      "type": "n8n-nodes-base.webhook",
      "position": [
        460,
        2720
      ],
      "parameters": {
        "path": "updateslots",
        "options": {},
        "httpMethod": "POST",
        "responseMode": "responseNode"
      },
      "typeVersion": 2
    },
    {
      "id": "891fb4ec-3a82-4433-bebf-3f0616027e3d",
      "name": "Input Arguments from updateslot tool",
      "type": "n8n-nodes-base.set",
      "position": [
        840,
        2720
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "6f6388ab-a233-4643-9b28-917ad6bdfe22",
              "name": "Calls[0].id",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].id }}"
            },
            {
              "id": "40888d2c-b99d-401d-a6b9-944ba41543c6",
              "name": "name",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.name }}"
            },
            {
              "id": "17be6cf6-8c48-4a4e-a0e8-b5b714f94242",
              "name": "email",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.email }}"
            },
            {
              "id": "d06fd547-39c1-457b-8422-393f140aead6",
              "name": "starttime",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.starttime }}"
            },
            {
              "id": "c224df67-ec82-40f3-9af2-3472731a57fa",
              "name": "endtime",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.endtime }}"
            },
            {
              "id": "b2fb0887-5545-409c-bba8-fae76a71f660",
              "name": "call.id",
              "type": "string",
              "value": "={{ $json.body.message.call.id }}"
            },
            {
              "id": "19efa4c6-25e0-4fe8-a00e-0b37f16b6de0",
              "name": "Rescheduled_starttime",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.Rescheduled_starttime }}"
            },
            {
              "id": "ad47dfdb-66fa-478d-899f-1d9d202aac6f",
              "name": "Rescheduled_endttime",
              "type": "string",
              "value": "={{ $json.body.message.toolCalls[0].function.arguments.Rescheduled_endttime }}"
            },
            {
              "id": "6d1bf6c0-a4b4-41d4-826e-e7c73f920905",
              "name": "customer_number",
              "type": "string",
              "value": "={{ $json.body.message.call.customer.number }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "617a7742-299a-4c91-be82-cba598d1bb82",
      "name": "Checks if required info is provided.",
      "type": "n8n-nodes-base.if",
      "position": [
        1060,
        2720
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "87304425-5f17-4637-8aa3-cd84b2f8d856",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.name }}",
              "rightValue": ""
            },
            {
              "id": "fdc6ffb0-f234-4869-8f5e-482c394ab860",
              "operator": {
                "type": "string",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.email }}",
              "rightValue": ""
            },
            {
              "id": "7950d7bc-7416-48b6-8ec5-a635a9161013",
              "operator": {
                "type": "dateTime",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.Rescheduled_starttime }}",
              "rightValue": "={{ $json.Rescheduledtime }}"
            },
            {
              "id": "aa54ee15-1273-48b0-863f-939597af04e6",
              "operator": {
                "type": "dateTime",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.Rescheduled_endttime }}",
              "rightValue": ""
            },
            {
              "id": "8ceefa9d-360c-48b6-8faf-e156459f2c07",
              "operator": {
                "type": "dateTime",
                "operation": "exists",
                "singleValue": true
              },
              "leftValue": "={{ $json.starttime }}",
              "rightValue": ""
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "dded1cfa-ce89-481f-967b-6843854a32bd",
      "name": "Finds original appointment",
      "type": "n8n-nodes-base.airtable",
      "maxTries": 2,
      "position": [
        1600,
        2560
      ],
      "parameters": {
        "base": {
Pro

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

How this works

Small businesses and solo professionals can let callers book appointments directly through a voice AI receptionist instead of managing calls manually. The workflow receives incoming calls via webhook, checks real-time availability in Google Calendar, and records confirmed bookings in Airtable while sending confirmation details back to the caller. It handles slot selection, conflict checking, and follow-up logging without requiring staff to answer every enquiry.

Use this when your volume of calls justifies automated scheduling and you already rely on Google Calendar for availability. Avoid it if your service requires complex qualification questions or same-day changes that need human judgement. Variations include swapping Airtable for another CRM or adding SMS reminders after each booking.

About this workflow

This template is ideal for small businesses, agencies, and solo professionals who want to automate appointment scheduling and caller follow-up through a voice-based AI receptionist. If you’re using tools like Google Calendar, Airtable, and Vapi (Twilio), this setup is for you.

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

More Data & Sheets workflows → · Browse all categories →

Related workflows

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

Data & Sheets

This workflow automates the entire lifecycle of a service-based client, combining four distinct business flows into a single view: Intake Leads: Receives a webhook from your form builder, validates th

Airtable, Notion, Google Calendar +3
Data & Sheets

A warm, reliable onboarding system for small businesses and studios. Captures a form submission via webhook, creates a Client record in Notion, sends a concierge-style welcome email (with scheduler +

Notion, Email Send, Telegram +4
Data & Sheets

Webhook Code. Uses itemLists, respondToWebhook, stickyNote, googleCalendar. Webhook trigger; 92 nodes.

Item Lists, Google Calendar, Airtable
Data & Sheets

This premium n8n workflow harnesses the power of DataForSEO's API combined with Airtable's relational database capabilities to transform your keyword research process, providing deeper insights for co

HTTP Request, Airtable
Data & Sheets

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

Airtable, HTTP Request, Google Drive +1