{
  "name": "Scheduling Assistant - 04 Conflict Scan",
  "nodes": [
    {
      "parameters": {},
      "id": "workflow-trigger",
      "name": "When called by another workflow",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1,
      "position": [
        250,
        300
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "resource": "calendar",
        "operation": "getAll",
        "calendarId": "primary",
        "options": {
          "timeMin": "={{$json.validatedData.normalized.startTime}}",
          "timeMax": "={{$json.validatedData.normalized.endTime}}"
        }
      },
      "id": "google-calendar-check",
      "name": "Check Google Calendar",
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 3,
      "position": [
        450,
        150
      ],
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "mode": "appendItems"
      },
      "id": "merge-calendar-data",
      "name": "Merge Calendar Data",
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3,
      "position": [
        650,
        300
      ],
      "notes": "Input 0: original validated data (always 1 item, from trigger). Input 1: calendar events (0 or more items). appendItems guarantees Analyze Conflicts runs even when the calendar returns 0 results (no conflicts = happy path)."
    },
    {
      "parameters": {
        "jsCode": "// Analyze calendar conflicts\n//\n// Input structure after Merge (appendItems mode):\n//   items[0]     = original validated scheduling data (from trigger, guaranteed 1 item)\n//   items[1..n]  = calendar events in the requested time window (0 or more items)\n//\n// The Merge node ensures this code always runs, including when the calendar returns\n// 0 events (no conflicts), which is the happy path and the most common case.\n// Without the Merge, a 0-item getAll result would silently stop execution here.\n\nconst originalData = items[0].json.validatedData || items[0].json;\nconst requestedStart = new Date(originalData.normalized.startTime);\nconst requestedEnd = new Date(originalData.normalized.endTime);\n\n// Calendar events begin at index 1. Index 0 is the original scheduling data.\nconst calendarEvents = items.slice(1).map(item => item.json);\n\nconst conflicts = [];\n\n// Check each calendar event for time overlap with the requested window\nfor (const event of calendarEvents) {\n  const eventStart = new Date(event.start?.dateTime || event.start?.date);\n  const eventEnd = new Date(event.end?.dateTime || event.end?.date);\n\n  if (requestedStart < eventEnd && requestedEnd > eventStart) {\n    conflicts.push({\n      title: event.summary,\n      start: eventStart.toISOString(),\n      end: eventEnd.toISOString(),\n      attendees: event.attendees?.map(a => a.email) || [],\n      location: event.location || 'Not specified'\n    });\n  }\n}\n\n// Generate up to 3 alternative time slots when conflicts are detected\nconst alternatives = [];\nif (conflicts.length > 0) {\n  const duration = originalData.normalized.duration;\n\n  const alt1Start = new Date(requestedStart);\n  alt1Start.setHours(alt1Start.getHours() - 1);\n  alternatives.push({\n    startTime: alt1Start.toISOString(),\n    endTime: new Date(alt1Start.getTime() + duration * 60000).toISOString(),\n    reason: 'Earlier slot on same day'\n  });\n\n  const alt2Start = new Date(requestedEnd);\n  alt2Start.setHours(alt2Start.getHours() + 1);\n  alternatives.push({\n    startTime: alt2Start.toISOString(),\n    endTime: new Date(alt2Start.getTime() + duration * 60000).toISOString(),\n    reason: 'Later slot on same day'\n  });\n\n  const alt3Start = new Date(requestedStart);\n  alt3Start.setDate(alt3Start.getDate() + 1);\n  alternatives.push({\n    startTime: alt3Start.toISOString(),\n    endTime: new Date(alt3Start.getTime() + duration * 60000).toISOString(),\n    reason: 'Same time next day'\n  });\n}\n\nreturn [{\n  json: {\n    ...originalData,\n    conflictScan: {\n      hasConflicts: conflicts.length > 0,\n      conflictCount: conflicts.length,\n      conflicts,\n      alternatives,\n      scannedAt: new Date().toISOString(),\n      calendarSource: 'google_calendar'\n    },\n    stage: 'conflict_scanned',\n    processedAt: new Date().toISOString()\n  }\n}];"
      },
      "id": "code-analyze-conflicts",
      "name": "Analyze Conflicts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        850,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "boolean": [
            {
              "value1": "={{$json.conflictScan.hasConflicts}}",
              "value2": false
            }
          ]
        }
      },
      "id": "if-no-conflicts",
      "name": "No Conflicts?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 1,
      "position": [
        1050,
        300
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "clear-data",
              "name": "clearData",
              "value": "={{$json}}",
              "type": "object"
            },
            {
              "id": "next-workflow-export",
              "name": "nextWorkflow",
              "value": "05_route_export",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "set-clear-output",
      "name": "Set Clear Output",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.2,
      "position": [
        1250,
        200
      ]
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "conflict-data",
              "name": "conflictData",
              "value": "={{$json}}",
              "type": "object"
            },
            {
              "id": "next-workflow-notify",
              "name": "nextWorkflow",
              "value": "06_notify",
              "type": "string"
            },
            {
              "id": "notification-type-conflict",
              "name": "notificationType",
              "value": "conflict_detected",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "id": "set-conflict-output",
      "name": "Set Conflict Output",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.2,
      "position": [
        1250,
        400
      ]
    },
    {
      "parameters": {
        "workflowId": "={{$env.WORKFLOW_ID_ROUTE_EXPORT}}",
        "options": {
          "waitForSubWorkflow": false
        }
      },
      "id": "execute-route-export",
      "name": "Trigger Route & Export",
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1,
      "position": [
        1450,
        200
      ]
    },
    {
      "parameters": {
        "workflowId": "={{$env.WORKFLOW_ID_NOTIFY}}",
        "options": {
          "waitForSubWorkflow": false
        }
      },
      "id": "execute-notify-conflict",
      "name": "Notify Conflicts",
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1,
      "position": [
        1450,
        400
      ]
    }
  ],
  "connections": {
    "When called by another workflow": {
      "main": [
        [
          {
            "node": "Check Google Calendar",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merge Calendar Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Google Calendar": {
      "main": [
        [
          {
            "node": "Merge Calendar Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Merge Calendar Data": {
      "main": [
        [
          {
            "node": "Analyze Conflicts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze Conflicts": {
      "main": [
        [
          {
            "node": "No Conflicts?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "No Conflicts?": {
      "main": [
        [
          {
            "node": "Set Clear Output",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Set Conflict Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Clear Output": {
      "main": [
        [
          {
            "node": "Trigger Route & Export",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Conflict Output": {
      "main": [
        [
          {
            "node": "Notify Conflicts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveExecutionProgress": true,
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all"
  },
  "staticData": null,
  "tags": [
    {
      "name": "scheduling-assistant",
      "id": "scheduling-tag-001"
    },
    {
      "name": "conflict-detection",
      "id": "conflict-tag-001"
    }
  ],
  "triggerCount": 0,
  "updatedAt": "2026-01-19T20:15:00.000Z",
  "versionId": "v1.1.0"
}