{
  "id": "WdcCalendarAuditDemo",
  "name": "Report overlapping appointments from Google Calendar by email",
  "active": false,
  "nodes": [
    {
      "parameters": {},
      "id": "9ba182e5-43e3-4f09-81a0-246508846918",
      "name": "Run a manual check",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 8
            }
          ]
        }
      },
      "id": "bf0cab1c-4474-4659-922d-5d219e4c1322",
      "name": "Review each morning",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        0,
        160
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "dd014a23-ffa5-498b-a74d-218196313814",
              "name": "demo_mode",
              "value": true,
              "type": "boolean"
            },
            {
              "id": "aa9669a8-491e-4e9a-9f86-532717e471d1",
              "name": "calendar_id",
              "value": "primary",
              "type": "string"
            },
            {
              "id": "7e4ce4a6-4626-4122-93e0-4b609b77e9da",
              "name": "recipient_email",
              "value": "",
              "type": "string"
            },
            {
              "id": "a7afc848-cd5a-4020-926d-f1dceaa14679",
              "name": "report_timezone",
              "value": "Europe/Madrid",
              "type": "string"
            },
            {
              "id": "24280619-8928-49a2-9c29-46a2cde462eb",
              "name": "lookahead_days",
              "value": 7,
              "type": "number"
            },
            {
              "id": "c98ea4de-b7ce-462b-9812-f814e042e5d0",
              "name": "max_report_pairs",
              "value": 25,
              "type": "number"
            }
          ]
        },
        "options": {}
      },
      "id": "5b355e1d-7872-4e50-b920-41b89f23856b",
      "name": "Configure calendar and report",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        260,
        80
      ]
    },
    {
      "parameters": {
        "jsCode": "const config = $input.first().json;\nif (typeof config.demo_mode !== 'boolean') throw new Error('demo_mode must be a boolean.');\nif (!Number.isInteger(config.lookahead_days) || config.lookahead_days < 1 || config.lookahead_days > 30) throw new Error('lookahead_days must be an integer between 1 and 30.');\nif (!Number.isInteger(config.max_report_pairs) || config.max_report_pairs < 1 || config.max_report_pairs > 100) throw new Error('max_report_pairs must be an integer between 1 and 100.');\nnew Intl.DateTimeFormat('en-GB', { timeZone: config.report_timezone });\nif (!config.demo_mode) {\n  if (typeof config.calendar_id !== 'string' || !config.calendar_id.trim()) throw new Error('Choose a calendar ID.');\n  if (!/^[^\\s@,;]+@[^\\s@,;]+\\.[^\\s@,;]+$/.test(String(config.recipient_email || ''))) throw new Error('Choose one internal recipient email address.');\n}\nreturn [{ json: config }];"
      },
      "id": "0999389d-c759-4f55-b29a-8250050689a5",
      "name": "Validate audit settings",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        450,
        80
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "6f070ddd-245d-44bb-b129-a45d121ce6a9",
              "leftValue": "={{ $json.demo_mode }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "d9cf2162-1721-4812-b280-eaa86b7fd451",
      "name": "Use fictional appointments?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        640,
        80
      ]
    },
    {
      "parameters": {
        "jsCode": "const base = $now.startOf('day').plus({ days: 1, hours: 10 });\nconst make = (id, title, startMinutes, endMinutes, extra = {}) => ({ json: {\n  id, summary: title, status: 'confirmed',\n  start: { dateTime: base.plus({ minutes: startMinutes }).toISO() },\n  end: { dateTime: base.plus({ minutes: endMinutes }).toISO() }, ...extra,\n} });\nreturn [\n  make('demo-a', 'Example appointment A', 0, 60),\n  make('demo-b', 'Example appointment B', 30, 90),\n  make('demo-c', 'Example appointment C', 90, 120),\n  make('demo-cancelled', 'Example cancelled appointment', 15, 45, { status: 'cancelled' }),\n  make('demo-free', 'Example free event', 15, 45, { transparency: 'transparent' }),\n];"
      },
      "id": "0ae21519-41d8-4738-9f5c-9880d79a5f6a",
      "name": "Build fictional appointments",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        760,
        -20
      ]
    },
    {
      "parameters": {
        "operation": "getAll",
        "calendar": {
          "__rl": true,
          "value": "={{ $('Configure calendar and report').first().json.calendar_id }}",
          "mode": "id"
        },
        "returnAll": true,
        "timeMin": "={{ $now.toISO() }}",
        "timeMax": "={{ $now.plus({ days: Number($('Configure calendar and report').first().json.lookahead_days) }).toISO() }}",
        "options": {
          "recurringEventHandling": "expand",
          "timeZone": {
            "__rl": true,
            "mode": "id",
            "value": "={{ $('Configure calendar and report').first().json.report_timezone }}"
          }
        }
      },
      "id": "00ff43b6-5b66-4c30-911c-d4663a2421f4",
      "name": "Read upcoming calendar appointments",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        760,
        200
      ],
      "alwaysOutputData": true
    },
    {
      "parameters": {
        "jsCode": "const config = $('Configure calendar and report').first().json;\nconst timeZone = String(config.report_timezone || 'Europe/Madrid');\nconst formatter = new Intl.DateTimeFormat('en-GB', {\n  timeZone, dateStyle: 'medium', timeStyle: 'short',\n});\nconst demoMode = config.demo_mode === true;\nif (typeof config.demo_mode !== 'boolean') throw new Error('demo_mode must be a boolean.');\nif (!demoMode && !/^[^\\s@,;]+@[^\\s@,;]+\\.[^\\s@,;]+$/.test(String(config.recipient_email || ''))) {\n  throw new Error('Configure one recipient email address before turning demo mode off.');\n}\nconst displayLimit = Number(config.max_report_pairs ?? 25);\nif (!Number.isInteger(displayLimit) || displayLimit < 1 || displayLimit > 100) {\n  throw new Error('max_report_pairs must be an integer between 1 and 100.');\n}\nconst items = $input.all();\nconst events = [];\nconst seen = new Set();\nconst invalid = [];\nconst skipped = { cancelled: 0, transparent: 0, declined: 0, allDay: 0, duplicate: 0 };\nconst label = value => String(value || 'Untitled appointment').replace(/[\\r\\n\\u0000-\\u001f\\u007f]+/g, ' ').trim().slice(0, 180);\nconst parseInstant = value => {\n  if (typeof value !== 'string' || !/(?:Z|[+-]\\d{2}:\\d{2})$/i.test(value)) return NaN;\n  return Date.parse(value);\n};\nfor (const { json: event } of items) {\n  if (!event || Object.keys(event).length === 0) continue;\n  if (event.status === 'cancelled') { skipped.cancelled++; continue; }\n  if (event.transparency === 'transparent') { skipped.transparent++; continue; }\n  if (event.attendees?.some(attendee => attendee.self === true && attendee.responseStatus === 'declined')) {\n    skipped.declined++; continue;\n  }\n  if (event.start?.date && !event.start.dateTime) { skipped.allDay++; continue; }\n  const start = parseInstant(event.start?.dateTime);\n  const end = parseInstant(event.end?.dateTime);\n  if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) {\n    invalid.push(label(event.summary)); continue;\n  }\n  const key = `${event.id || ''}|${start}|${end}`;\n  if (event.id && seen.has(key)) { skipped.duplicate++; continue; }\n  if (event.id) seen.add(key);\n  events.push({ title: label(event.summary), start, end });\n}\nevents.sort((left, right) => left.start - right.start || left.end - right.end);\nlet conflictCount = 0;\nconst pairs = [];\nlet active = [];\nfor (const event of events) {\n  active = active.filter(previous => previous.end > event.start);\n  for (const previous of active) {\n    conflictCount++;\n    if (pairs.length < displayLimit) pairs.push({\n      first: previous.title,\n      second: event.title,\n      overlapStart: new Date(Math.max(previous.start, event.start)).toISOString(),\n      overlapEnd: new Date(Math.min(previous.end, event.end)).toISOString(),\n    });\n  }\n  active.push(event);\n}\nconst lines = [\n  `Calendar audit: ${conflictCount} overlapping appointment pair(s).`,\n  `Checked ${events.length} timed appointment(s). Times shown in ${timeZone}.`,\n  '',\n];\nfor (let index = 0; index < pairs.length; index++) {\n  const pair = pairs[index];\n  lines.push(`${index + 1}. ${pair.first} / ${pair.second}`,\n    `   ${formatter.format(new Date(pair.overlapStart))} to ${formatter.format(new Date(pair.overlapEnd))}`);\n}\nif (conflictCount > pairs.length) lines.push('', `Showing ${pairs.length} of ${conflictCount} pairs. Increase max_report_pairs to see more.`);\nif (invalid.length) lines.push('', `${invalid.length} event(s) could not be checked because their timed dates were invalid or missing a UTC offset.`,\n  ...invalid.slice(0, 10).map(title => `- ${title}`));\nconst ignoredCount = Object.values(skipped).reduce((sum, count) => sum + count, 0);\nif (ignoredCount) lines.push('', `Excluded: ${skipped.cancelled} cancelled, ${skipped.transparent} free, ${skipped.declined} declined, ${skipped.allDay} all-day, ${skipped.duplicate} duplicate event(s).`);\nif (!conflictCount && !invalid.length) lines.push('No timed appointment overlaps found in the retrieved events.');\nlines.push('', 'Review the affected appointments in Google Calendar. This workflow leaves the calendar unchanged.');\nreturn [{ json: {\n  demoMode, checkedEvents: events.length, conflictCount, pairs, invalidEvents: invalid.length, skipped,\n  needsAttention: conflictCount > 0 || invalid.length > 0,\n  subject: `Calendar audit: ${conflictCount} overlap(s)${invalid.length ? `, ${invalid.length} unchecked event(s)` : ''}`,\n  reportText: lines.join('\\n'), recipientEmail: String(config.recipient_email || ''),\n} }];\n"
      },
      "id": "08aa5d32-2f5d-449e-adde-332b67f1ed30",
      "name": "Find timed appointment overlaps",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1040,
        80
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "5e0a0330-955d-416d-a94e-3723856accb5",
              "leftValue": "={{ $json.demoMode }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              }
            },
            {
              "id": "d33965f3-9163-4be7-ae31-825ebe07e0b3",
              "leftValue": "={{ $json.needsAttention }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        }
      },
      "id": "89190e78-9d46-49f1-8ce9-8730111a4b12",
      "name": "Send a live attention report?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1300,
        80
      ]
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "send",
        "sendTo": "={{ $json.recipientEmail }}",
        "subject": "={{ $json.subject }}",
        "emailType": "text",
        "message": "={{ $json.reportText }}",
        "options": {
          "appendAttribution": true
        }
      },
      "id": "3c767336-7120-4a7d-aa1d-4596853750b9",
      "name": "Email the internal calendar owner",
      "type": "n8n-nodes-base.gmail",
      "typeVersion": 2.1,
      "position": [
        1560,
        -20
      ]
    },
    {
      "parameters": {},
      "id": "35cf0d31-0d48-4b21-be19-dabe6e40c467",
      "name": "Inspect the audit result",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1560,
        220
      ]
    },
    {
      "parameters": {
        "content": "# Report overlapping appointments from Google Calendar by email\n\n## Who this is for\n\nBusinesses using one Google Calendar for a single person or bookable resource. If appointments arrive through several channels, a daily review can catch clashes that were added outside the booking form.\n\n## How it works\n\nThe workflow reads upcoming timed events, expands recurring appointments and finds overlapping pairs. Appointments that meet exactly at their start and end times are compatible. Cancelled events, events marked free and invitations you declined are excluded.\n\nWhen a clash or an invalid timed date needs attention, it emails one internal recipient a summary. It leaves appointments unchanged.\n\n## Setup\n\n1. Run the included example with `demo_mode` enabled. This uses fictional appointments and sends no email.\n2. In **Configure calendar and report**, set the calendar, recipient, reporting timezone and review window.\n3. Connect your Google Calendar and Gmail credentials. Switch `demo_mode` off and run a manual check.\n4. Review the schedule and workflow timezone before publishing or activating it.\n\n## Requirements and scope\n\nUse n8n, Google Calendar and Gmail. Choose a calendar for one resource; intentional overlaps on a shared team calendar will also be reported. All-day events are excluded. The same unresolved clash can appear in the next daily report.\n\nCreated by [Webs del Camp](https://websdelcamp.cat/es/automatizaciones?utm_source=n8n&utm_medium=referral&utm_campaign=calendar_audit_2026_10_03&utm_content=template_author), web and business automation services in Spain.\n",
        "width": 680,
        "height": 820,
        "color": 1
      },
      "id": "b213215e-23be-49ed-a4ba-7b5ddce44e00",
      "name": "Template guide",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -60,
        400
      ]
    },
    {
      "parameters": {
        "content": "## Start here\nKeep `demo_mode` true for the first manual run. The example produces one overlapping pair and sends no email.\n\nSet one calendar and one internal recipient. The report timezone controls how dates appear. Review the workflow timezone and daily schedule before enabling live runs.",
        "width": 620,
        "height": 260,
        "color": 7
      },
      "id": "bf811045-5e3c-4962-850f-b1f368514700",
      "name": "Configure and test",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        190,
        -280
      ]
    },
    {
      "parameters": {
        "content": "## Read-only calendar review\nConnect Google Calendar only for live runs. Read all events in the selected window and expand recurring appointments. Use a calendar for one person or one resource.\n\nThe audit checks timed events only. It excludes all-day, cancelled, free and self-declined events. Back-to-back appointments do not overlap.",
        "width": 510,
        "height": 300,
        "color": 7
      },
      "id": "8cc032ab-e822-440e-9b79-c67abf00dfcb",
      "name": "Calendar scope",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        730,
        400
      ]
    },
    {
      "parameters": {
        "content": "## Review the report\nConnect Gmail for the live email. Only live reports with conflicts or invalid dates are sent. Open the Code or final result node to inspect demo output.\n\nEmails contain appointment titles. Choose an authorized internal recipient. The workflow does not create, change, cancel or email attendees about appointments. Unresolved clashes can appear in later daily reports.",
        "width": 530,
        "height": 300,
        "color": 7
      },
      "id": "818dd10f-827d-4c68-95f0-0b5a27e657d4",
      "name": "Internal report",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        1270,
        400
      ]
    }
  ],
  "connections": {
    "Run a manual check": {
      "main": [
        [
          {
            "node": "Configure calendar and report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Review each morning": {
      "main": [
        [
          {
            "node": "Configure calendar and report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Configure calendar and report": {
      "main": [
        [
          {
            "node": "Validate audit settings",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate audit settings": {
      "main": [
        [
          {
            "node": "Use fictional appointments?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Use fictional appointments?": {
      "main": [
        [
          {
            "node": "Build fictional appointments",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Read upcoming calendar appointments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build fictional appointments": {
      "main": [
        [
          {
            "node": "Find timed appointment overlaps",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read upcoming calendar appointments": {
      "main": [
        [
          {
            "node": "Find timed appointment overlaps",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find timed appointment overlaps": {
      "main": [
        [
          {
            "node": "Send a live attention report?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send a live attention report?": {
      "main": [
        [
          {
            "node": "Email the internal calendar owner",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Inspect the audit result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Europe/Madrid",
    "saveDataSuccessExecution": "none",
    "saveDataErrorExecution": "none",
    "saveExecutionProgress": false
  },
  "pinData": {},
  "meta": {
    "templateCredsSetupCompleted": false
  },
  "tags": []
}