Book and manage appointments with Google Calendar and Gmail

Automate the booking and management of appointments by receiving requests, checking Google Calendar availability, and creating events. It connects a webhook for incoming requests, Google Calendar for scheduling, and Gmail for sending confirmations and reminders. This is ideal for consultants scheduling client meetings, service providers managing bookings, or recruiters coordinating interviews.

26 nodeswebhook trigger123 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

Related Templates

Auto-create TikTok videos with VEED.io AI avatars, ElevenLabs & GPT-4

Automate the creation and distribution of trending TikTok videos using AI avatars. This workflow connects Telegram, Perplexity, OpenAI, ElevenLabs, VEED.io, and BLOTATO to generate scripts, synthesize voice, create video, and publish across multiple social platforms. Content creators and marketers can rapidly produce engaging short-form video content without manual editing.

35 nodes

Personal life manager with Telegram, Google services & voice-enabled AI

Manage your personal life using voice or text commands through Telegram. This workflow connects Telegram, Google Calendar, Gmail, Google Tasks, and an AI assistant powered by OpenRouter to process requests. It's ideal for busy individuals who want to quickly schedule events, send emails, or manage tasks on the go. This automation streamlines daily organization, saving significant time and mental effort.

25 nodes

Summarize Google Sheets form feedback via OpenAI's GPT-4

Efficiently summarize Google Sheets form feedback using OpenAI's GPT-4. This productivity workflow connects Google Sheets to retrieve form submissions, aggregates the responses into arrays, and then leverages OpenAI's advanced GPT-4 model to generate concise summaries. The generated summaries, initially in Markdown, are then converted to HTML for easy readability before being sent directly to your inbox via Gmail. This automation is ideal for product managers, marketing teams, or customer support departments who regularly collect feedback through Google Forms and need a quick, intelligent overview of responses without manually sifting through every entry. It dramatically reduces the time and effort spent analyzing feedback, allowing for faster insights and more informed decision-making.

10 nodes

Ready to automate with n8n?

Get affordable managed n8n hosting with 24/7 support.