Planificateur de rendez-vous vocal avec IA (Vapi + Google Calendar)
Ceci est unContent Creation, Multimodal AIworkflow d'automatisation du domainecontenant 13 nœuds.Utilise principalement des nœuds comme Set, Code, Switch, Webhook, GoogleCalendar. Réservation automatisée de rendez-vous vocaux avec Vapi AI et Google Calendar
- •Point de terminaison HTTP Webhook (généré automatiquement par n8n)
Nœuds utilisés (13)
Catégorie
{
"id": "77pJCZrTmSlJBxnh",
"meta": {
"instanceId": "994ed1f0b0ae417c58e72d5bdace12e35e5f984ec06703c9dbfe4b05882c3a52",
"templateCredsSetupCompleted": true
},
"name": "AI Voice Appointment Setter (Vapi + Google Calendar)",
"tags": [],
"nodes": [
{
"id": "30a2f5b7-b9b8-477d-817c-e180192646d7",
"name": "1. CONFIGURATION (À MODIFIER)",
"type": "n8n-nodes-base.set",
"notes": "This node holds all the settings for your appointment setter's availability.",
"position": [
-448,
96
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "1fec2f61-2072-4d2b-b323-b20dc8be1eee",
"name": "timeZone",
"type": "string",
"value": "America/New_York"
},
{
"id": "4262429e-b497-4b14-88a0-c1747465cb2c",
"name": "workdayStartHour",
"type": "number",
"value": 9
},
{
"id": "8ad05af7-a5ae-4838-bf60-e45b0c2021a7",
"name": "workdayEndHour",
"type": "number",
"value": 17
},
{
"id": "d618f077-a077-43ad-9bb5-55eb21445947",
"name": "meetingDurationMinutes",
"type": "number",
"value": 30
},
{
"id": "67453847-1958-45e5-aaf1-a042d9bb1c29",
"name": "bookingCadenceMinutes",
"type": "number",
"value": 30
},
{
"id": "f166930f-a05f-4382-8017-6b3c94717840",
"name": "bufferBeforeMinutes",
"type": "number",
"value": 15
},
{
"id": "dee10f61-c71e-4377-80a3-69de66c8a73a",
"name": "bufferAfterMinutes",
"type": "number",
"value": 15
}
]
}
},
"typeVersion": 3.4
},
{
"id": "774e305d-252f-4d54-a057-19aeb642e42b",
"name": "Webhook: URL de production = URL du serveur VAPI",
"type": "n8n-nodes-base.webhook",
"position": [
-688,
96
],
"webhookId": "c5fc47d7-60b8-49b1-a7b7-4b40a9ee02a0",
"parameters": {
"path": "AI-Appointment-Setter-Template",
"options": {},
"httpMethod": "POST",
"responseMode": "responseNode"
},
"typeVersion": 2
},
{
"id": "1f67cedc-9da7-4b7e-8286-6ee3358fd70a",
"name": "Router par nom d'outil",
"type": "n8n-nodes-base.switch",
"position": [
-208,
96
],
"parameters": {
"rules": {
"values": [
{
"outputKey": "checkAvailability",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "80297dae-adde-4206-a8c3-2b7f77c5b88c",
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $('Webhook: Production URL = VAPI Server URL').item.json.body.message.toolCalls[0].function.name }}",
"rightValue": "checkAvailability"
}
]
},
"renameOutput": true
},
{
"outputKey": "bookAppointment",
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "23430f94-763-403d-a7d6-eae05ad2b801",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $('Webhook: Production URL = VAPI Server URL').item.json.body.message.toolCalls[0].function.name }}",
"rightValue": "bookAppointment"
}
]
},
"renameOutput": true
}
]
},
"options": {}
},
"typeVersion": 3.2
},
{
"id": "ac181bae-191b-42a8-aedf-5ab53c040051",
"name": "Calculer les créneaux potentiels (ne pas modifier)",
"type": "n8n-nodes-base.code",
"position": [
208,
0
],
"parameters": {
"jsCode": "const potentialSlots = [];\n\nconst config = $('1. CONFIGURATION (EDIT ME)').item.json;\n\nconst initialISO = $('Webhook: Production URL = VAPI Server URL').item.json.body.message.toolCalls[0].function.arguments.initialSearchDateTime;\n\nconst datePart = initialISO.split('T')[0];\nconst offsetPart = initialISO.slice(-6);\nconst startHourString = String(config.workdayStartHour).padStart(2, '0');\nconst endHourString = String(config.workdayEndHour).padStart(2, '0');\n\nconst workdayStartISO = `${datePart}T${startHourString}:00:00${offsetPart}`;\nconst workdayEndISO = `${datePart}T${endHourString}:00:00${offsetPart}`;\n\nlet currentSlot = new Date(workdayStartISO);\nconst endOfWorkday = new Date(workdayEndISO);\n\nwhile (currentSlot.getTime() < endOfWorkday.getTime()) {\n const slotEnd = new Date(currentSlot.getTime() + config.bookingCadenceMinutes * 60000);\n potentialSlots.push({\n start: currentSlot.toISOString(),\n end: slotEnd.toISOString(),\n });\n currentSlot = slotEnd;\n}\n\nreturn [{\n json: {\n ...config, \n potentialSlots: potentialSlots\n }\n}];"
},
"typeVersion": 2
},
{
"id": "8e2cf90f-9f15-478b-9c4b-acae5c6ee2c9",
"name": "Filtrer les créneaux disponibles (ne pas modifier)",
"type": "n8n-nodes-base.code",
"position": [
432,
0
],
"parameters": {
"jsCode": "const config = $('Calculate Potential Slots (do not change)').item.json;\nconst potentialSlots = config.potentialSlots;\nconst busyEvents = $('2. Get Calendar Events (EDIT ME)').all();\nconst toolCallId = $('Webhook: Production URL = VAPI Server URL').first().json.body.message.toolCalls[0].id;\n\nconst busyIntervals = busyEvents\n .filter(event => event.json.start && event.json.start.dateTime) \n .map(event => {\n const start = new Date(event.json.start.dateTime);\n const end = new Date(event.json.end.dateTime);\n const startWithBuffer = new Date(start.getTime() - config.bufferBeforeMinutes * 60000);\n const endWithBuffer = new Date(end.getTime() + config.bufferAfterMinutes * 60000);\n return { start: startWithBuffer, end: endWithBuffer };\n });\n\nconst availableSlots = potentialSlots.filter(slot => {\n const meetingStart = new Date(slot.start); \n const meetingEnd = new Date(meetingStart.getTime() + config.meetingDurationMinutes * 60000);\n if (meetingStart < new Date()) { return false; }\n const isOverlapping = busyIntervals.some(busy => (meetingStart < busy.end) && (meetingEnd > busy.start));\n return !isOverlapping;\n});\n\nconst formattedSlots = availableSlots.map(slot => {\n const utcDate = new Date(slot.start); \n\n const humanReadable = utcDate.toLocaleString('en-US', {\n timeZone: config.timeZone,\n weekday: 'long', month: 'long', day: 'numeric',\n hour: 'numeric', minute: '2-digit', hour12: true,\n });\n\n\n const localFormatter = new Intl.DateTimeFormat('en-US', {\n year: 'numeric', month: '2-digit', day: '2-digit',\n hour: '2-digit', minute: '2-digit', second: '2-digit',\n hourCycle: 'h23', \n timeZone: config.timeZone\n });\n\n const localParts = localFormatter.formatToParts(utcDate);\n let localYear, localMonth, localDay, localHour, localMinute, localSecond;\n\n for (const part of localParts) {\n switch (part.type) {\n case 'year': localYear = part.value; break;\n case 'month': localMonth = part.value; break;\n case 'day': localDay = part.value; break;\n case 'hour': localHour = part.value; break;\n case 'minute': localMinute = part.value; break;\n case 'second': localSecond = part.value; break;\n }\n }\n\n const fakeUtcDateFromLocal = new Date(\n `${localYear}-${localMonth}-${localDay}T${localHour}:${localMinute}:${localSecond}.000Z`\n );\n\n const offsetMillis = fakeUtcDateFromLocal.getTime() - utcDate.getTime();\n const offsetMinutes = offsetMillis / 60000; \n\n const absOffsetMinutes = Math.abs(offsetMinutes);\n const offsetHours = Math.floor(absOffsetMinutes / 60);\n const remainingOffsetMinutes = absOffsetMinutes % 60;\n\n const offsetSign = offsetMinutes < 0 ? '-' : '+';\n\n const isoOffset = `${offsetSign}${String(offsetHours).padStart(2, '0')}:${String(remainingOffsetMinutes).padStart(2, '0')}`;\n\n const isoDateTime = `${localYear}-${localMonth}-${localDay}T${localHour}:${localMinute}:${localSecond}${isoOffset}`;\n\n return { humanReadable, isoDateTime };\n});\n\nreturn [{\n json: {\n toolCallId: toolCallId,\n availableSlots: formattedSlots\n }\n}];"
},
"typeVersion": 2
},
{
"id": "d14d093c-b468-454f-85a4-815c7b99c287",
"name": "Répondre avec les horaires disponibles (ne pas modifier)",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
656,
0
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "={{\n {\n \"results\": [\n {\n \"toolCallId\": $json.toolCallId,\n \"result\": JSON.stringify({ \"availableSlots\": $json.availableSlots })\n }\n ]\n }\n}}"
},
"typeVersion": 1.4
},
{
"id": "7ea10fb1-8966-4bf3-ad42-ed3b52d41c66",
"name": "Je suis une note",
"type": "n8n-nodes-base.stickyNote",
"position": [
432,
192
],
"parameters": {
"color": 7,
"width": 348,
"height": 396,
"content": "This will get you going!\n\nNext level? Add:\n- SMS & Email Confirmations\n- Multilingual Support\n- In-depth Lead Qualification\n- Dynamic FAQ Answering\n- CRM Integration\n- Call Transferring\n- Rescheduling & Cancelling\n- Call Summaries & Analysis\n\nQuestions? Feel free to reach out:\nhttps://streetlamp.agency"
},
"typeVersion": 1
},
{
"id": "4f80f53f-c639-448d-ab87-5e58778efece",
"name": "Note adhésive",
"type": "n8n-nodes-base.stickyNote",
"position": [
-64,
-288
],
"parameters": {
"color": 7,
"height": 272,
"content": "## Edit Google Calendar nodes \n**1** Connect your Google calendar. Click: Credentials to connect with > Choose your credential OR Create new credential\n**2** Click: Calendar > From list > Choose your calendar"
},
"typeVersion": 1
},
{
"id": "c52db039-40de-4458-aaf7-d3c770db190a",
"name": "Note adhésive1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-528,
-224
],
"parameters": {
"color": 7,
"width": 256,
"height": 304,
"content": "## Edit Fields\n(Using numbers, no text)\n**1.** Enter your timezone (e.g. \"America/New_York\" for EST, without quotes)\n**2.** Enter your work start hour and your work end hour (e.g. 8 for 8AM, 17 for 5PM)\n**3.** Enter meeting duration, buffer time before and after meeting (e.g. 60 for 1 hour meeting, 15 for 15 minutes buffer)"
},
"typeVersion": 1
},
{
"id": "1f925858-22bb-4143-8a57-d37f10508858",
"name": "3. Réserver un rendez-vous dans le calendrier (À MODIFIER)",
"type": "n8n-nodes-base.googleCalendar",
"notes": "1. Select your Google Account from the 'Credential' dropdown.\n\n2. Select the **SAME** calendar you chose in the previous node to book the new appointment.",
"position": [
0,
192
],
"parameters": {
"end": "={{ $('Webhook: Production URL = VAPI Server URL').item.json.body.message.toolCalls[0].function.arguments.endDateTime }}",
"start": "={{ $('Webhook: Production URL = VAPI Server URL').item.json.body.message.toolCalls[0].function.arguments.startDateTime }}",
"calendar": {
"__rl": true,
"mode": "list"
},
"additionalFields": {
"summary": "=Roof Inspection: {{ $('Webhook: Production URL = VAPI Server URL').item.json.body.message.toolCalls[0].function.arguments.clientName }}",
"description": "=Job Details:\nService Type: {{ $('Webhook: Production URL = VAPI Server URL').item.json.body.message.toolCalls[0].function.arguments.serviceType }}\nProperty Address: {{ $('Webhook: Production URL = VAPI Server URL').item.json.body.message.toolCalls[0].function.arguments.propertyAddress }}\n\nClient Contact:\nName: {{ $('Webhook: Production URL = VAPI Server URL').item.json.body.message.toolCalls[0].function.arguments.clientName }}\nPhone: {{ $('Webhook: Production URL = VAPI Server URL').item.json.body.message.customer.number }}\n\nCall Log Id:\n{{ $('Webhook: Production URL = VAPI Server URL').item.json.body.message.call.id }}"
}
},
"retryOnFail": true,
"typeVersion": 1.3
},
{
"id": "d147ba98-9086-4ae0-b5c8-43e8cd2eb8ba",
"name": "2. Obtenir les événements du calendrier (À MODIFIER)",
"type": "n8n-nodes-base.googleCalendar",
"notes": "1. Select your Google Account from the 'Credential' dropdown.\n\n2. Select the calendar you want to check for availability from the 'Calendar' dropdown list.",
"position": [
0,
0
],
"parameters": {
"options": {},
"timeMax": "={{ DateTime.fromISO($('Webhook: Production URL = VAPI Server URL').item.json.body.message.toolCalls[0].function.arguments.initialSearchDateTime).endOf('day').toISO() }}",
"timeMin": "={{ $('Webhook: Production URL = VAPI Server URL').item.json.body.message.toolCalls[0].function.arguments.initialSearchDateTime }}",
"calendar": {
"__rl": true,
"mode": "list"
},
"operation": "getAll",
"returnAll": true
},
"retryOnFail": true,
"typeVersion": 1.3,
"alwaysOutputData": true
},
{
"id": "4355474c-9edb-4a4f-891d-0a349c75dc43",
"name": "Confirmation de réservation (ne pas modifier)",
"type": "n8n-nodes-base.respondToWebhook",
"position": [
208,
192
],
"parameters": {
"options": {},
"respondWith": "json",
"responseBody": "={{ { \"results\": [ { \"toolCallId\": $('Webhook: Production URL = VAPI Server URL').item.json.body.message.toolCalls[0].id, \"result\": \"The appointment has been successfully booked.\" } ] } }}"
},
"typeVersion": 1.4
},
{
"id": "2f332449-141c-4d6f-926f-78281aea2920",
"name": "Note adhésive2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1792,
-672
],
"parameters": {
"width": 896,
"height": 2288,
"content": "## What this template does\n\nConnect a Vapi AI voice agent to Google Calendar to capture contact details and auto-book appointments.\nThe agent asks for name, address, service type, and a preferred time. The workflow checks availability and either proposes times or books the slot—no code needed.\n\n## How it works (node map)\n\n- Webhook: Production URL = VAPI Server URL — receives tool calls from Vapi and returns results.\n- **1. CONFIGURATION (EDIT ME)** — your timezone, work hours, meeting length, buffers, and cadence.\n- **Route by Tool Name** — routes Vapi tool calls:\n\t- `checkAvailability` → calendar lookup path\n\t- `bookAppointment` → create event path\n- **2. Get Calendar Events (EDIT ME)** — reads events for the requested day.\n- **Calculate Potential Slots / Filter for Available Slots** — builds conflict-free options with buffers.\n- Respond with Available Times — returns formatted slots to Vapi.\n- **3. Book Appointment in Calendar (EDIT ME)** — creates the calendar event with details.\n- **Booking Confirmation** — returns success back to Vapi.\n\n> Sticky notes in the canvas show exactly what to edit (required by n8n).\nNo API keys are hardcoded; Google uses OAuth credentials.\n\n## Requirements\n\n- n8n (Cloud or self-hosted)\n- Google account with Calendar (OAuth credential in n8n)\n- Vapi account + one Assistant\n\n## Setup (5 minutes)\n### A) Vapi → n8n connection\n\n1. Open the **Webhook** node and copy the **Production URL**.\n2. In **Vapi** → **Assistant** → **Messaging**, set **Server URL** = that Production URL.\n3. In **Server Messages**, enable **only** `toolCalls`.\n\n### B) Vapi tools (names must match exactly)\n\nCreate two **Custom Tools** in Vapi and attach them to the assistant:\n\n**Tool 1:** `checkAvailability`\n\n- **Arguments**\n\t- `initialSearchDateTime` (string, ISO-8601 with timezone offset, e.g. `2025-09-09T09:00:00-05:00`)\n\n**Tool 2:** ```bookAppointment```\n\n- **Arguments**\n\t- `startDateTime` (string, ISO-8601 with tz)\n\t- `endDateTime` (string, ISO-8601 with tz)\n\t- `clientName` (string)\n\t- `propertyAddress` (string)\n\t- `serviceType` (string)\n\n> The Switch node routes based on ```message.toolCalls[0].function.name```. If the names differ, nothing will run.\n\n### C) Configure availability\n\nOpen **1. CONFIGURATION (EDIT ME)** and set:\n\n- ```timeZone``` (e.g. ```America/New_York```)\n- ```workdayStartHour``` / ```workdayEndHour``` (24h integers)\n- ```meetingDurationMinutes``` (e.g. ```30``` or ```60```)\n- ```bufferBeforeMinutes``` / ```bufferAfterMinutes``` (e.g. ```15```)\n- ```bookingCadenceMinutes``` (e.g. ```30```)\n\n### D) Connect Google Calendar\n\n1. Open **2. Get Calendar Events (EDIT ME)** → Credentials: select/create Google Calendar OAuth.\nThen choose the calendar to check availability.\n2. Open **3. Book Appointment in Calendar (EDIT ME)** → use the same credential and same calendar to book.\n\n### E) Activate & test\n\n- Toggle the workflow **Active**.\n- Call your Vapi number (or start a session) and book a test slot.\n- Verify the event appears with description fields (client, address, service type, call id).\n\n### Customizing\n\n- Change summary/description format in **3. Book Appointment**.\n- Add SMS/Email confirmations, CRM sync, rescheduling, or analytics as follow-ups (see sticky note “I’m a note”).\n\n### Troubleshooting\n\n- **No response back to Vapi** → confirm Vapi is set to send toolCalls only and the Server URL matches the Production URL.\n- **Switch doesn’t route** → tool names must be exactly checkAvailability and bookAppointment.\n- **No times returned** → ensure timezone + work hours + cadence generate at least one future slot; confirm Google credential and calendar selection.\n- **Event not created** → use the same Google credential & calendar in both nodes; check OAuth scopes/consent.\n\n### Security & privacy\n\n- Google uses OAuth; credentials live in n8n.\n- No API keys hardcoded.\n- Webhook receives only the fields needed to check times or book."
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"callerPolicy": "workflowsFromSameOwner",
"executionOrder": "v1"
},
"versionId": "a8b22bbd-b9b6-4d43-b1c7-10d0c5b9aa79",
"connections": {
"1f67cedc-9da7-4b7e-8286-6ee3358fd70a": {
"main": [
[
{
"node": "d147ba98-9086-4ae0-b5c8-43e8cd2eb8ba",
"type": "main",
"index": 0
}
],
[
{
"node": "1f925858-22bb-4143-8a57-d37f10508858",
"type": "main",
"index": 0
}
]
]
},
"30a2f5b7-b9b8-477d-817c-e180192646d7": {
"main": [
[
{
"node": "1f67cedc-9da7-4b7e-8286-6ee3358fd70a",
"type": "main",
"index": 0
}
]
]
},
"d147ba98-9086-4ae0-b5c8-43e8cd2eb8ba": {
"main": [
[
{
"node": "ac181bae-191b-42a8-aedf-5ab53c040051",
"type": "main",
"index": 0
}
]
]
},
"1f925858-22bb-4143-8a57-d37f10508858": {
"main": [
[
{
"node": "4355474c-9edb-4a4f-891d-0a349c75dc43",
"type": "main",
"index": 0
}
]
]
},
"ac181bae-191b-42a8-aedf-5ab53c040051": {
"main": [
[
{
"node": "8e2cf90f-9f15-478b-9c4b-acae5c6ee2c9",
"type": "main",
"index": 0
}
]
]
},
"774e305d-252f-4d54-a057-19aeb642e42b": {
"main": [
[
{
"node": "30a2f5b7-b9b8-477d-817c-e180192646d7",
"type": "main",
"index": 0
}
]
]
},
"8e2cf90f-9f15-478b-9c4b-acae5c6ee2c9": {
"main": [
[
{
"node": "d14d093c-b468-454f-85a4-815c7b99c287",
"type": "main",
"index": 0
}
]
]
}
}
}Comment utiliser ce workflow ?
Copiez le code de configuration JSON ci-dessus, créez un nouveau workflow dans votre instance n8n et sélectionnez "Importer depuis le JSON", collez la configuration et modifiez les paramètres d'authentification selon vos besoins.
Dans quelles scénarios ce workflow est-il adapté ?
Intermédiaire - Création de contenu, IA Multimodale
Est-ce payant ?
Ce workflow est entièrement gratuit et peut être utilisé directement. Veuillez noter que les services tiers utilisés dans le workflow (comme l'API OpenAI) peuvent nécessiter un paiement de votre part.
Workflows recommandés
Francisco Rivera
@soyfricoAI voice solutions expert, trusted by multi-location brands and influencers with 1M+ followers, 24+ years in marketing, launching a newspaper, building an agency, and 11+ years as a missionary.
Partager ce workflow