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 →
{
"name": "Lab 4 - ElevenLabs Booking Tools (Google Calendar)",
"nodes": [
{
"parameters": {
"httpMethod": "POST",
"path": "check-availability",
"responseMode": "responseNode",
"options": {
"allowedOrigins": "*"
}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-220,
-160
],
"id": "a1111111-1111-4111-8111-111111111111",
"name": "Webhook - check_availability"
},
{
"parameters": {
"jsCode": "// An ElevenLabs server tool POSTs its parameters as a flat JSON body:\n// { service, date, time }. (`body.args ?? body` below also accepts the nested\n// shape, so the same flow still works if you test it with a wrapped payload.)\n// Turn the caller's words into a concrete 1-hour slot in the salon timezone.\nconst TZ_OFFSET = '+08:00'; // Singapore\nconst OPEN_HOUR = 8; // 8:00 AM\nconst CLOSE_HOUR = 21; // 9:00 PM (last slot starts 20:00)\n\nconst body = $input.first().json.body ?? {};\nconst args = body.args ?? body; // tolerate a flat test payload\n\nconst service = (args.service || 'Appointment').toString();\nconst date = (args.date || '').toString(); // expected YYYY-MM-DD\nconst time = (args.time || '').toString(); // expected HH:MM (24h)\n\nif (!/^\\d{4}-\\d{2}-\\d{2}$/.test(date) || !/^\\d{2}:\\d{2}$/.test(time)) {\n return [{ json: { valid: false, service, date, time,\n speak: 'I did not catch the day and time. What day and what time would you like?' } }];\n}\n\nconst start = `${date}T${time}:00${TZ_OFFSET}`;\nconst end = new Date(new Date(start).getTime() + 60 * 60 * 1000).toISOString();\nconst hour = Number(time.slice(0, 2));\n\n// Sunday = closed; outside opening hours = closed.\nconst dow = new Date(start).getDay();\nif (dow === 0 || hour < OPEN_HOUR || hour >= CLOSE_HOUR) {\n return [{ json: { valid: false, service, date, time, start, end,\n speak: 'We are open Monday to Saturday, eight in the morning to nine at night. Would another time work?' } }];\n}\n\nreturn [{ json: { valid: true, service, date, time, start, end } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
0,
-160
],
"id": "a2222222-2222-4222-8222-222222222222",
"name": "Parse Slot"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"version": 2
},
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.valid }}",
"rightValue": "",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
220,
-160
],
"id": "a3333333-3333-4333-8333-333333333333",
"name": "Slot understood?"
},
{
"parameters": {
"operation": "getAll",
"calendar": {
"__rl": true,
"value": "primary",
"mode": "list",
"cachedResultName": "primary"
},
"returnAll": true,
"timeMin": "={{ $json.start }}",
"timeMax": "={{ $json.end }}",
"options": {
"singleEvents": true
}
},
"type": "n8n-nodes-base.googleCalendar",
"typeVersion": 1.3,
"position": [
460,
-260
],
"id": "a4444444-4444-4444-8444-444444444444",
"name": "Google Calendar - Busy?",
"alwaysOutputData": true
},
{
"parameters": {
"jsCode": "// The Calendar node returns one item per event in the window (none = free).\n// alwaysOutputData gives us a single empty item when there are no events.\nconst events = $input.all().filter(i => i.json && i.json.id);\nconst slot = $('Parse Slot').first().json;\nconst free = events.length === 0;\n\nconst spokenTime = new Date(slot.start).toLocaleTimeString('en-SG', {\n hour: 'numeric', minute: '2-digit', hour12: true, timeZone: 'Asia/Singapore',\n});\n\nreturn [{ json: {\n available: free,\n service: slot.service,\n date: slot.date,\n time: slot.time,\n start: slot.start,\n end: slot.end,\n // `speak` is what Nina reads out. Keep it one short sentence.\n speak: free\n ? `Yes, ${spokenTime} on ${slot.date} is free for a ${slot.service}. Shall I book it?`\n : `Sorry, ${spokenTime} is already taken. Would an hour earlier or later work?`,\n} }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
680,
-260
],
"id": "a5555555-5555-4555-8555-555555555555",
"name": "Availability Answer"
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
900,
-160
],
"id": "a6666666-6666-4666-8666-666666666666",
"name": "Respond - availability"
},
{
"parameters": {
"httpMethod": "POST",
"path": "book-appointment",
"responseMode": "responseNode",
"options": {
"allowedOrigins": "*"
}
},
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-220,
220
],
"id": "b2222222-2222-4222-8222-222222222222",
"name": "Webhook - book_appointment"
},
{
"parameters": {
"jsCode": "// The ElevenLabs book_appointment tool POSTs { service, date, time, name, phone }.\nconst TZ_OFFSET = '+08:00';\nconst body = $input.first().json.body ?? {};\nconst args = body.args ?? body;\n\nconst service = (args.service || 'Appointment').toString();\nconst date = (args.date || '').toString();\nconst time = (args.time || '').toString();\nconst name = (args.name || 'Guest').toString();\nconst phone = (args.phone || '').toString();\n\nif (!/^\\d{4}-\\d{2}-\\d{2}$/.test(date) || !/^\\d{2}:\\d{2}$/.test(time)) {\n return [{ json: { valid: false,\n speak: 'I still need the day and the time before I can book that.' } }];\n}\n\nconst start = `${date}T${time}:00${TZ_OFFSET}`;\nconst end = new Date(new Date(start).getTime() + 60 * 60 * 1000).toISOString();\n\nreturn [{ json: { valid: true, service, date, time, name, phone, start, end,\n summary: `${service} - ${name}`,\n description: `Booked by Nina (ElevenLabs voice agent).\\nService: ${service}\\nCustomer: ${name}\\nPhone: ${phone}` } }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
0,
220
],
"id": "b4444444-4444-4444-8444-444444444444",
"name": "Build Event"
},
{
"parameters": {
"calendar": {
"__rl": true,
"value": "primary",
"mode": "list",
"cachedResultName": "primary"
},
"start": "={{ $json.start }}",
"end": "={{ $json.end }}",
"additionalFields": {
"summary": "={{ $json.summary }}",
"description": "={{ $json.description }}"
}
},
"type": "n8n-nodes-base.googleCalendar",
"typeVersion": 1.3,
"position": [
240,
220
],
"id": "b5555555-5555-4555-8555-555555555555",
"name": "Google Calendar - Create Event"
},
{
"parameters": {
"jsCode": "// Confirm back to ElevenLabs. The whole JSON is handed to the agent; `speak` is\n// the line it reads aloud.\nconst ev = $input.first().json;\nconst slot = $('Build Event').first().json;\n\nconst spokenTime = new Date(slot.start).toLocaleTimeString('en-SG', {\n hour: 'numeric', minute: '2-digit', hour12: true, timeZone: 'Asia/Singapore',\n});\n\nreturn [{ json: {\n booked: true,\n eventId: ev.id ?? null,\n htmlLink: ev.htmlLink ?? null,\n speak: `Done. I've booked your ${slot.service} on ${slot.date} at ${spokenTime} under ${slot.name}. See you then!`,\n} }];"
},
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
460,
220
],
"id": "b6666666-6666-4666-8666-666666666666",
"name": "Booking Answer"
},
{
"parameters": {
"options": {}
},
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
900,
220
],
"id": "b7777777-7777-4777-8777-777777777777",
"name": "Respond - booking"
}
],
"connections": {
"Webhook - check_availability": {
"main": [
[
{
"node": "Parse Slot",
"type": "main",
"index": 0
}
]
]
},
"Parse Slot": {
"main": [
[
{
"node": "Slot understood?",
"type": "main",
"index": 0
}
]
]
},
"Slot understood?": {
"main": [
[
{
"node": "Google Calendar - Busy?",
"type": "main",
"index": 0
}
],
[
{
"node": "Respond - availability",
"type": "main",
"index": 0
}
]
]
},
"Google Calendar - Busy?": {
"main": [
[
{
"node": "Availability Answer",
"type": "main",
"index": 0
}
]
]
},
"Availability Answer": {
"main": [
[
{
"node": "Respond - availability",
"type": "main",
"index": 0
}
]
]
},
"Webhook - book_appointment": {
"main": [
[
{
"node": "Build Event",
"type": "main",
"index": 0
}
]
]
},
"Build Event": {
"main": [
[
{
"node": "Google Calendar - Create Event",
"type": "main",
"index": 0
}
]
]
},
"Google Calendar - Create Event": {
"main": [
[
{
"node": "Booking Answer",
"type": "main",
"index": 0
}
]
]
},
"Booking Answer": {
"main": [
[
{
"node": "Respond - booking",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Lab 4 - ElevenLabs Booking Tools (Google Calendar). Uses googleCalendar. Webhook trigger; 11 nodes.
Source: https://github.com/tertiarycourses/TGS-2026062147-No-Code-and-Low-Code-Agentic-AI-Applications/blob/main/labs/activity11-voice-elevenlabs/elevenlabs-booking-tools-flow.json — 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.
AI Phone Agent with RetellAI. Uses telegram, googleCalendar, httpRequest, googleDrive. Webhook trigger; 36 nodes.
Build a Chatbot, Voice Agent and Phone Agent with Voiceflow, Google Calendar and RAG. Uses googleCalendar, httpRequest, googleDrive. Webhook trigger; 34 nodes.
Transform your appointment scheduling with this production-ready workflow that connects ElevenLabs voice AI to your Google Calendar. Patients can call and book appointments naturally through conversat
Agente Vendas Veículos - Completo. Uses redis, httpRequest, googleCalendar, googleSheetsTrigger. Webhook trigger; 21 nodes.
Crown Storage — Book Consultation. Uses googleCalendar, gmail. Webhook trigger; 7 nodes.