Book and manage appointments with Google Calendar and Gmail

## Overview This workflow automates the complete appointment booking process, from request validation to scheduling, notifications, and reminders. It checks calendar availability in real time, prevents double bookings, suggests alternative slots when unavailable, and automatically sends confirmations and reminders—ensuring a smooth and reliable booking experience. Perfect for service-based businesses, consultants, and teams managing appointments at scale. --- ## How It Works 1. **Webhook Trigger** - Receives booking requests (name, email, date, time, notes). 2. **Workflow Configuration** - Defines: - Google Calendar ID - Appointment duration - Business hours - Sender email 3. **Data Validation** - Parses and validates input fields - Ensures required data is present and correctly formatted 4. **Calendar Availability Check** - Fetches existing events from Google Calendar - Compares requested time with existing bookings 5. **Conflict Detection** - Detects overlapping events - Determines whether the slot is available 6. **Decision Logic** - If available → proceed with booking - If not available → trigger alternative flow --- ### Booking Flow (Available Slot) 7. **Create Calendar Event** - Schedules appointment in Google Calendar - Adds attendee and event details 8. **Confirmation Email** - Sends booking confirmation with event details - Includes calendar event link 9. **Webhook Response** - Returns success response to client/system 10. **Reminder System** - Schedules automated reminders: - 24-hour reminder before appointment - 1-hour reminder before appointment - Uses wait nodes to trigger emails at exact times - Includes appointment details to reduce no-shows --- ### Alternative Flow (Unavailable Slot) 11. **Generate Alternative Slots** - Finds next available time slots within business hours - Ensures slots are within a defined time window 12. **Alternat

26 nodeswebhook trigger0 views0 copiesProductivity
WebhookGoogleCalendarGmailRespondToWebhookWait

Workflow JSON

{"meta":{"instanceId":"48aac30adfc5487a33ef16e0e958096f27cef40df3ed0febcbe1ca199e789786"},"nodes":[{"id":"2fb14430-f0bb-49da-9bb4-54b9ce1ba475","name":"Booking Request Webhook","type":"n8n-nodes-base.webhook","position":[-1792,192],"webhookId":"0d8c6146-0619-4a91-8030-5cf18e2f2fd4","parameters":{"path":"booking","options":{},"httpMethod":"POST","responseMode":"lastNode"},"typeVersion":2.1},{"id":"6e4794f2-91e6-41a9-85ec-41413b94651c","name":"Workflow Configuration","type":"n8n-nodes-base.set","position":[-1504,192],"parameters":{"options":{},"assignments":{"assignments":[{"id":"id-1","name":"calendarId","type":"string","value":"<__PLACEHOLDER_VALUE__Your Google Calendar ID__>"},{"id":"id-2","name":"appointmentDuration","type":"number","value":60},{"id":"id-3","name":"businessHoursStart","type":"number","value":9},{"id":"id-4","name":"businessHoursEnd","type":"number","value":17},{"id":"id-5","name":"senderEmail","type":"string","value":"<__PLACEHOLDER_VALUE__Your email address for sending confirmations__>"}]},"includeOtherFields":true},"typeVersion":3.4},{"id":"be0e66e9-a02b-4819-aa57-a3b126fb42d2","name":"Parse Booking Data","type":"n8n-nodes-base.code","position":[-1248,192],"parameters":{"jsCode":"// Parse and validate booking request data from webhook\nconst items = $input.all();\nconst output = [];\n\nfor (const item of items) {\n  const body = item.json.body || item.json;\n  \n  // Extract booking fields\n  const name = body.name || '';\n  const email = body.email || '';\n  const requestedDate = body.requestedDate || '';\n  const requestedTime = body.requestedTime || '';\n  const notes = body.notes || '';\n  \n  // Validate required fields\n  const errors = [];\n  \n  if (!name || name.trim() === '') {\n    errors.push('Name is required');\n  }\n  \n  if (!email || email.trim() === '') {\n    errors.push('Email is required');\n  } else if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) {\n    errors.push('Invalid email format');\n  }\n  \n  if (!requestedDate || requestedDate.trim() === '') {\n    errors.push('Requested date is required');\n  }\n  \n  if (!requestedTime || requestedTime.trim() === '') {\n    errors.push('Requested time is required');\n  }\n  \n  // Create output object\n  const bookingData = {\n    name: name.trim(),\n    email: email.trim(),\n    requestedDate: requestedDate.trim(),\n    requestedTime: requestedTime.trim(),\n    notes: notes.trim(),\n    isValid: errors.length === 0,\n    validationErrors: errors,\n    rawData: body\n  };\n  \n  output.push({\n    json: bookingData\n  });\n}\n\nreturn output;"},"typeVersion":2},{"id":"e35412be-3209-456b-bab0-78945966780e","name":"Check Calendar Availability","type":"n8n-nodes-base.googleCalendar","position":[-992,192],"parameters":{"options":{},"calendar":{"__rl":true,"mode":"id","value":"={{ $('Workflow Configuration').first().json.calendarId }}"},"operation":"getAll","returnAll":true},"typeVersion":1.3},{"id":"dc8feda8-6ac3-40c2-ae76-e2d24503e6a4","name":"Check for Conflicts","type":"n8n-nodes-base.code","position":[-672,192],"parameters":{"jsCode":"// Get the calendar events from the previous node\nconst calendarEvents = $('Check Calendar Availability').all();\n\n// Get the requested booking time from the parsed data\nconst requestedStart = $('Parse Booking Data').item.json.startTime;\nconst requestedEnd = $('Parse Booking Data').item.json.endTime;\n\n// Check if there are any conflicting events\nlet hasConflict = false;\nlet conflictingEvents = [];\n\nif (calendarEvents && calendarEvents.length > 0) {\n  for (const event of calendarEvents) {\n    if (event.json && event.json.start && event.json.end) {\n      const eventStart = new Date(event.json.start.dateTime || event.json.start.date);\n      const eventEnd = new Date(event.json.end.dateTime || event.json.end.date);\n      const reqStart = new Date(requestedStart);\n      const reqEnd = new Date(requestedEnd);\n      \n      // Check for overlap: events conflict if they overlap in any way\n      if (reqStart < eventEnd && reqEnd > eventStart) {\n        hasConflict = true;\n        conflictingEvents.push({\n          summary: event.json.summary,\n          start: event.json.start.dateTime || event.json.start.date,\n          end: event.json.end.dateTime || event.json.end.date\n        });\n      }\n    }\n  }\n}\n\n// Return the result\nreturn [{\n  json: {\n    isAvailable: !hasConflict,\n    hasConflict: hasConflict,\n    requestedStart: requestedStart,\n    requestedEnd: requestedEnd,\n    conflictingEvents: conflictingEvents,\n    totalEventsChecked: calendarEvents.length\n  }\n}];"},"typeVersion":2},{"id":"b33d1e94-f61e-439d-bbb7-5f963b515a05","name":"Is Available?","type":"n8n-nodes-base.if","position":[-480,192],"parameters":{"options":{},"conditions":{"options":{"leftValue":"","caseSensitive":false,"typeValidation":"loose"},"combinator":"and","conditions":[{"id":"id-1","operator":{"type":"boolean","operation":"true"},"leftValue":"={{ $('Check for Conflicts').item.json.isAvailable }}"}]}},"typeVersion":2.3},{"id":"d1905396-9921-4633-9489-dc1ba071c5db","name":"Create Calendar Event","type":"n8n-nodes-base.googleCalendar","position":[-80,16],"parameters":{"end":"={{ new Date(new Date($json.requestedDateTime).getTime() + $('Workflow Configuration').first().json.appointmentDuration*60*1000).toISOString() }}","start":"={{ $json.requestedDateTime }}","calendar":{"__rl":true,"mode":"id","value":"={{ $('Workflow Configuration').first().json.calendarId }}"},"additionalFields":{"summary":"=Appointment with {{ $json.name }}","attendees":["={{ $json.email }}"],"description":"={{ $json.notes }}"}},"typeVersion":1.3},{"id":"8e2656d3-25d8-4c7b-b1ef-7fa81ccef5af","name":"Send Confirmation Email","type":"n8n-nodes-base.gmail","position":[144,16],"webhookId":"eebd3b07-d80b-47dc-a9ed-b01a9bd3ef40","parameters":{"sendTo":"={{ $json.email }}","message":"=<p>Dear {{ $json.name }},</p>\n\n<p>Your appointment has been confirmed!</p>\n\n<p><strong>Appointment Details:</strong></p>\n<ul>\n  <li><strong>Date & Time:</strong> {{ new Date($json.requestedDateTime).toLocaleString() }}</li>\n  <li><strong>Duration:</strong> {{ $json.duration || '30' }} minutes</li>\n</ul>\n\n<p>A calendar invitation has been sent to your email. You can also view the event in your calendar using the link below:</p>\n\n<p><a href=\"{{ $('Create Calendar Event').item.json.htmlLink }}\">View Calendar Event</a></p>\n\n<p>If you need to reschedule or cancel, please contact us as soon as possible.</p>\n\n<p>We look forward to seeing you!</p>\n\n<p>Best regards,<br>Your Team</p>","options":{"senderName":"={{ $('Workflow Configuration').first().json.senderEmail }}"},"subject":"=Appointment Confirmed - {{ new Date($json.requestedDateTime).toLocaleString() }}"},"typeVersion":2.2},{"id":"6d351346-9862-47bb-8fb1-9fba8aff877e","name":"Respond Success","type":"n8n-nodes-base.respondToWebhook","position":[496,-144],"parameters":{"options":{},"respondWith":"json","responseBody":"{\n  \"success\": true,\n  \"message\": \"Booking confirmed successfully\"\n}"},"typeVersion":1.5},{"id":"d62b05c2-ea91-4b49-a1d0-3a393f8dfc7f","name":"Find Alternative Slots","type":"n8n-nodes-base.code","position":[-128,496],"parameters":{"jsCode":"// Get the requested booking time from the previous node\nconst requestedTime = new Date($('Parse Booking Data').item.json.bookingTime);\n\n// Business hours configuration\nconst businessHours = {\n  start: 9, // 9 AM\n  end: 17   // 5 PM\n};\nconst slotDuration = 60; // 60 minutes per slot\n\n// Function to check if a time is within business hours\nfunction isBusinessHours(date) {\n  const hour = date.getHours();\n  const day = date.getDay();\n  // Monday to Friday (1-5), during business hours\n  return day >= 1 && day <= 5 && hour >= businessHours.start && hour < businessHours.end;\n}\n\n// Generate alternative slots\nconst alternativeSlots = [];\nlet currentDate = new Date();\ncurrentDate.setHours(currentDate.getHours() + 1); // Start from next hour\n\nwhile (alternativeSlots.length < 3) {\n  // Move to next hour\n  currentDate.setHours(currentDate.getHours() + 1);\n  currentDate.setMinutes(0);\n  currentDate.setSeconds(0);\n  \n  // Check if within 7 days\n  const daysDiff = (currentDate - new Date()) / (1000 * 60 * 60 * 24);\n  if (daysDiff > 7) break;\n  \n  // Check if within business hours\n  if (isBusinessHours(currentDate)) {\n    alternativeSlots.push({\n      startTime: new Date(currentDate).toISOString(),\n      endTime: new Date(currentDate.getTime() + slotDuration * 60000).toISOString(),\n      formatted: currentDate.toLocaleString('en-US', {\n        weekday: 'long',\n        year: 'numeric',\n        month: 'long',\n        day: 'numeric',\n        hour: '2-digit',\n        minute: '2-digit'\n      })\n    });\n  }\n}\n\nreturn [{\n  json: {\n    originalRequest: requestedTime.toISOString(),\n    alternativeSlots: alternativeSlots,\n    message: `Found ${alternativeSlots.length} alternative time slots`\n  }\n}];"},"typeVersion":2},{"id":"28908770-a67e-49d2-ab8c-471823716479","name":"Send Alternative Slots Email","type":"n8n-nodes-base.gmail","position":[96,496],"webhookId":"df74e6b5-9190-4aa3-a9b7-acdcaa9eec29","parameters":{"sendTo":"={{ $json.email }}","message":"=Hello {{ $json.name }},\n\nUnfortunately, your requested appointment time on {{ $json.requestedDateTime }} is not available.\n\nHowever, we have the following alternative time slots available:\n\n{{ $json.alternativeSlots }}\n\nPlease let us know which time works best for you, and we'll be happy to schedule your appointment.\n\nBest regards","options":{"senderName":"={{ $('Workflow Configuration').first().json.senderEmail }}"},"subject":"=Appointment Unavailable - Alternative Times Available"},"typeVersion":2.2},{"id":"aba22ee7-a3f6-4927-8a56-bd08aba7e758","name":"Respond Unavailable","type":"n8n-nodes-base.respondToWebhook","position":[320,496],"parameters":{"options":{},"respondWith":"json","responseBody":"={\n  \"status\": \"unavailable\",\n  \"message\": \"The requested time slot is not available\",\n  \"alternativeSlots\": {{ $json.alternativeSlots }}\n}"},"typeVersion":1.5},{"id":"c2067282-e65e-4a74-a3a9-0cdb88de0e31","name":"Wait 24 Hours Before","type":"n8n-nodes-base.wait","position":[400,224],"webhookId":"8141a980-5de2-4438-8825-488f579638a6","parameters":{"resume":"specificTime","dateTime":"={{ new Date(new Date($json.requestedDateTime).getTime() - 24*60*60*1000).toISOString() }}"},"typeVersion":1.1},{"id":"21e9a67e-f03a-4fbe-bd65-cfee4ce73bb6","name":"Send 24h Reminder","type":"n8n-nodes-base.gmail","position":[592,224],"webhookId":"f427fddc-3311-4490-b2c1-c8e2cc9b178d","parameters":{"sendTo":"={{ $json.email }}","message":"=Hello {{ $json.name }},\n\nThis is a friendly reminder that you have an appointment scheduled for tomorrow:\n\n📅 Date & Time: {{ new Date($json.requestedDateTime).toLocaleString() }}\n📍 Service: {{ $json.service }}\n⏱️ Duration: {{ $json.duration }} minutes\n\nPlease make sure to arrive on time. If you need to reschedule or cancel, please let us know as soon as possible.\n\nLooking forward to seeing you!\n\nBest regards","options":{"senderName":"={{ $('Workflow Configuration').first().json.senderEmail }}"},"subject":"=Reminder: Appointment Tomorrow - {{ new Date($json.requestedDateTime).toLocaleString() }}"},"typeVersion":2.2},{"id":"c77cb5af-9a40-46e5-9ebc-a37da9e27b9d","name":"Wait 1 Hour Before","type":"n8n-nodes-base.wait","position":[832,224],"webhookId":"29402ce2-047d-4500-9758-aa1f04e3f058","parameters":{"resume":"specificTime","dateTime":"={{ new Date(new Date($json.requestedDateTime).getTime() - 60*60*1000).toISOString() }}"},"typeVersion":1.1},{"id":"be47cbad-215c-4b8b-a3a1-61b27276e50c","name":"Send 1h Reminder","type":"n8n-nodes-base.gmail","position":[1056,224],"webhookId":"b0769c4c-32b3-46c6-b78d-c8e57e3fb720","parameters":{"sendTo":"={{ $json.email }}","message":"=<p>Hello {{ $json.name }},</p>\n\n<p>This is a friendly reminder that your appointment is coming up in <strong>1 hour</strong>.</p>\n\n<p><strong>Appointment Details:</strong></p>\n<ul>\n  <li><strong>Date & Time:</strong> {{ new Date($json.requestedDateTime).toLocaleString() }}</li>\n  <li><strong>Duration:</strong> {{ $json.duration }} minutes</li>\n  <li><strong>Service:</strong> {{ $json.service }}</li>\n</ul>\n\n<p>Please make sure you're ready for your appointment. If you need to make any changes, please contact us as soon as possible.</p>\n\n<p>We look forward to seeing you soon!</p>\n\n<p>Best regards,<br>{{ $('Workflow Configuration').first().json.senderName }}</p>","options":{"senderName":"={{ $('Workflow Configuration').first().json.senderName }}"},"subject":"=Reminder: Appointment in 1 Hour - {{ new Date($json.requestedDateTime).toLocaleString() }}"},"typeVersion":2.2},{"id":"390d4718-7585-4015-b1a1-48b829d7f2bc","name":"Sticky Note1","type":"n8n-nodes-base.stickyNote","position":[-1920,64],"parameters":{"color":7,"width":304,"height":304,"content":"## Input Layer\nReceive booking request via webhook"},"typeVersion":1},{"id":"a6413ac2-d5ec-49e4-89c9-2729db109643","name":"Sticky Note","type":"n8n-nodes-base.stickyNote","position":[-1600,64],"parameters":{"color":7,"width":256,"height":304,"content":"## Configuration\nSet calendar, duration, and business hours"},"typeVersion":1},{"id":"0355f826-17b5-4a09-814d-be376350fc8d","name":"Sticky Note2","type":"n8n-nodes-base.stickyNote","position":[-1312,64],"parameters":{"color":7,"width":224,"height":304,"content":"## Data Validation\nParse and validate booking request fields"},"typeVersion":1},{"id":"f241f879-0ffd-4072-a0a1-31b76950899d","name":"Sticky Note3","type":"n8n-nodes-base.stickyNote","position":[-1056,64],"parameters":{"color":7,"width":304,"height":304,"content":"## Availability Check\nFetch calendar events and check conflicts"},"typeVersion":1},{"id":"b7529ca8-9469-44e0-b455-fcb46589135a","name":"Sticky Note4","type":"n8n-nodes-base.stickyNote","position":[-720,64],"parameters":{"color":7,"width":336,"height":320,"content":"## Decision Logic\nRoute based on availability status"},"typeVersion":1},{"id":"239407fe-3f8a-4aa5-a31d-439d74ce281a","name":"Sticky Note5","type":"n8n-nodes-base.stickyNote","position":[-144,-64],"parameters":{"color":7,"width":432,"height":304,"content":"## Booking Flow\nCreate calendar event and confirm booking"},"typeVersion":1},{"id":"62e26f02-c3f6-462a-a438-a933a72359e4","name":"Sticky Note6","type":"n8n-nodes-base.stickyNote","position":[432,-224],"parameters":{"color":7,"width":304,"height":240,"content":"## Confirmation\nSend confirmation email and response"},"typeVersion":1},{"id":"9ede2529-0a5a-4e94-a3d3-4fcbe2063b5b","name":"Sticky Note7","type":"n8n-nodes-base.stickyNote","position":[352,80],"parameters":{"color":7,"width":992,"height":288,"content":"## Reminder System\nSend 24h and 1h reminder emails. After a booking is confirmed, the workflow schedules two timed reminders:\n- A 24-hour reminder to notify the user one day before\n- A 1-hour reminder for last-minute confirmation"},"typeVersion":1},{"id":"7f9e1f6a-a524-4f94-8ad1-fd14c90f612a","name":"Sticky Note8","type":"n8n-nodes-base.stickyNote","position":[-176,400],"parameters":{"color":7,"width":672,"height":320,"content":"## Alternative Flow\nSuggest available time slots if unavailable"},"typeVersion":1},{"id":"9ce7d0cd-7e8d-49da-bd64-67e5af90b440","name":"Sticky Note9","type":"n8n-nodes-base.stickyNote","position":[-2672,-16],"parameters":{"width":544,"height":496,"content":"## How it works\nThis workflow automates appointment booking by validating requests, checking calendar availability, and scheduling confirmed slots.\n\nWhen a booking request is received via webhook, the system validates input data and checks for conflicts in Google Calendar. If the requested time is available, an event is created, and a confirmation email is sent.\n\nIf unavailable, alternative time slots are generated and shared with the user. The workflow also sends automated reminders 24 hours and 1 hour before the appointment.\n\n## Setup steps\n- Configure webhook endpoint for booking requests\n- Add Google Calendar credentials\n- Set Gmail credentials for notifications\n- Define business hours and appointment duration\n- Customize email templates and reminders"},"typeVersion":1}],"pinData":{},"connections":{"Is Available?":{"main":[[{"node":"Create Calendar Event","type":"main","index":0}],[{"node":"Find Alternative Slots","type":"main","index":0}]]},"Send 24h Reminder":{"main":[[{"node":"Wait 1 Hour Before","type":"main","index":0}]]},"Parse Booking Data":{"main":[[{"node":"Check Calendar Availability","type":"main","index":0}]]},"Wait 1 Hour Before":{"main":[[{"node":"Send 1h Reminder","type":"main","index":0}]]},"Check for Conflicts":{"main":[[{"node":"Is Available?","type":"main","index":0}]]},"Wait 24 Hours Before":{"main":[[{"node":"Send 24h Reminder","type":"main","index":0}]]},"Create Calendar Event":{"main":[[{"node":"Send Confirmation Email","type":"main","index":0}]]},"Find Alternative Slots":{"main":[[{"node":"Send Alternative Slots Email","type":"main","index":0}]]},"Workflow Configuration":{"main":[[{"node":"Parse Booking Data","type":"main","index":0}]]},"Booking Request Webhook":{"main":[[{"node":"Workflow Configuration","type":"main","index":0}]]},"Send Confirmation Email":{"main":[[{"node":"Respond Success","type":"main","index":0},{"node":"Wait 24 Hours Before","type":"main","index":0}]]},"Check Calendar Availability":{"main":[[{"node":"Check for Conflicts","type":"main","index":0}]]},"Send Alternative Slots Email":{"main":[[{"node":"Respond Unavailable","type":"main","index":0}]]}}}

How to Import This Workflow

  1. 1Copy the workflow JSON above using the Copy Workflow JSON button.
  2. 2Open your n8n instance and go to Workflows.
  3. 3Click Import from JSON and paste the copied workflow.

Don't have an n8n instance? Start your free trial at n8nautomation.cloud

Ready to automate with n8n?

Get affordable managed n8n hosting with 24/7 support.