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 →
{
"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": "pratik@customaistudio.io",
"cachedResultName": "pratik@customaistudio.io"
},
"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": "pratik@customaistudio.io",
"cachedResultName": "pratik@customaistudio.io"
},
"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-008602000848",
"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": "pratik@customaistudio.io",
"cachedResultName": "pratik@customaistudio.io"
},
"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
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
How this works
This workflow enables users to automate appointment scheduling via a webhook, instantly checking availability in Google Calendar and Airtable before confirming bookings, saving hours of manual coordination for busy professionals like consultants or therapists. It processes incoming requests to generate real-time options for start times and ranges, ensuring seamless integration with existing calendars without double-bookings. The key step involves sorting and enriching date slots through custom code nodes, delivering a tailored response payload that fits your booking needs.
Use this workflow when handling high-volume inbound scheduling requests that require dynamic availability checks, such as client portals or API-driven apps, to maintain efficiency. Avoid it for simple one-off events without data enrichment, or if your setup lacks webhook endpoints for triggers. Common variations include adapting the code nodes for different time zones or integrating additional fields from Airtable for custom client details.
About this workflow
Webhook Code. Uses itemLists, respondToWebhook, stickyNote, googleCalendar. Webhook trigger; 92 nodes.
Source: https://github.com/Zie619/n8n-workflows — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
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 to
TEMPLATE - Multi Methods API Endpoint. Uses respondToWebhook, stickyNote, airtable. Webhook trigger; 18 nodes.
Webhook Respondtowebhook. Uses respondToWebhook, executeWorkflow, airtable, crypto. Webhook trigger; 16 nodes.
Amazon keywords. Uses airtable, httpRequest, splitOut, stickyNote. Webhook trigger; 12 nodes.
Use Redis To Rate Limit Your Low Code Api. Uses airtable, redis. Webhook trigger; 11 nodes.