{
  "name": "n8nCal \u00b7 /events",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "n8ncal/events",
        "responseMode": "responseNode",
        "options": {
          "allowedOrigins": "*"
        }
      },
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -700,
        0
      ],
      "id": "41f8644f-203a-4ae8-bc18-66ab06c3ae8e",
      "name": "Webhook \u00b7 /events"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "auth",
              "leftValue": "={{ $json.headers['x-n8ncal-token'] }}",
              "rightValue": "={{ $env.N8NCAL_TOKEN }}",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -480,
        0
      ],
      "id": "0b8a7db4-6b7f-4077-af1f-949be81937f3",
      "name": "Auth check"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={\n  \"error\": \"unauthorized\"\n}",
        "options": {
          "responseCode": 401
        }
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        -260,
        200
      ],
      "id": "5bd8aee2-6039-4028-99e5-671fdefcf92f",
      "name": "Respond 401"
    },
    {
      "parameters": {
        "jsCode": "// Pull from/to from the body. Default to a 7-day window starting today.\nconst body = $input.first().json.body || {};\nconst now = new Date();\nconst weekFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000);\n\nconst from = body.from ? new Date(body.from) : now;\nconst to   = body.to   ? new Date(body.to)   : weekFromNow;\n\nif (isNaN(from) || isNaN(to)) {\n  throw new Error('from/to must be ISO 8601 if provided');\n}\nif (to <= from) {\n  throw new Error('to must be after from');\n}\n\nreturn [{\n  json: {\n    from: from.toISOString(),\n    to:   to.toISOString(),\n    calendar: body.calendar || 'primary'\n  }\n}];"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -260,
        0
      ],
      "id": "c94ff647-6112-45a6-998e-e608e0116786",
      "name": "Parse window"
    },
    {
      "parameters": {
        "operation": "getAll",
        "calendar": {
          "__rl": true,
          "value": "={{ $json.calendar }}",
          "mode": "id"
        },
        "returnAll": false,
        "limit": 100,
        "options": {
          "timeMin": "={{ $json.from }}",
          "timeMax": "={{ $json.to }}",
          "singleEvents": true,
          "orderBy": "startTime"
        }
      },
      "type": "n8n-nodes-base.googleCalendar",
      "typeVersion": 1.3,
      "position": [
        -40,
        0
      ],
      "id": "7e9b1b15-cb68-4331-a100-9bcce8ef92f4",
      "name": "Google Calendar \u00b7 list",
      "credentials": {
        "googleCalendarOAuth2Api": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Compact for the dashboard AND classify each event into a 'kind' the UI can\n// color-code. The heuristic is intentionally simple \u2014 it can be replaced with\n// an AI classifier later without changing the response shape.\n//\n//   focus     \u2014 title looks like a deep-work block, OR no attendees + obvious cue\n//   personal  \u2014 0\u20131 attendees, doesn't look like work\n//   external  \u2014 attendees span >1 email domain (counts as customer/investor/etc)\n//   internal  \u2014 attendees all share the same domain\n//   meeting   \u2014 fallback\n\nfunction kindOf(e) {\n  const title = (e.summary || '').toLowerCase();\n  const attendees = (e.attendees || []).filter(a => a.email && !a.resource);\n  const domains = new Set(\n    attendees.map(a => (a.email.split('@')[1] || '').toLowerCase()).filter(Boolean)\n  );\n\n  if (/\\bfocus\\b|\\bdeep work\\b|\\bheads down\\b|\\bblock\\b|\\bno meetings\\b/.test(title)) {\n    return 'focus';\n  }\n  if (attendees.length <= 1) {\n    if (/\\blunch\\b|\\bdinner\\b|\\bcoffee\\b|\\bgym\\b|\\bdoctor\\b|\\bbirthday\\b/.test(title)) return 'personal';\n    if (attendees.length === 0) return 'focus';\n    return 'personal';\n  }\n  if (domains.size > 1) return 'external';\n  return 'internal';\n}\n\nreturn items.map(item => {\n  const e = item.json;\n  return {\n    json: {\n      id: e.id,\n      title: e.summary || '(no title)',\n      start: e.start?.dateTime || e.start?.date,\n      end:   e.end?.dateTime   || e.end?.date,\n      allDay: !e.start?.dateTime,\n      location: e.location || null,\n      attendees: (e.attendees || []).map(a => a.email).filter(Boolean),\n      kind: kindOf(e),\n      htmlLink: e.htmlLink,\n      status: e.status\n    }\n  };\n});"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        180,
        0
      ],
      "id": "1cfeccf0-d5ac-46fa-b274-2bf950885d81",
      "name": "Compact & classify"
    },
    {
      "parameters": {
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "events",
        "options": {}
      },
      "type": "n8n-nodes-base.aggregate",
      "typeVersion": 1,
      "position": [
        400,
        0
      ],
      "id": "719b3b2b-fdcf-4023-a3ae-fa08dd7c5bce",
      "name": "Roll up"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "ev",
              "name": "events",
              "value": "={{ $json.events }}",
              "type": "array"
            },
            {
              "id": "from",
              "name": "from",
              "value": "={{ $('Parse window').item.json.from }}",
              "type": "string"
            },
            {
              "id": "to",
              "name": "to",
              "value": "={{ $('Parse window').item.json.to }}",
              "type": "string"
            },
            {
              "id": "ts",
              "name": "ts",
              "value": "={{ $now.toISO() }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        620,
        0
      ],
      "id": "57ad49b9-5248-4826-acf2-928e650e63cd",
      "name": "Shape response"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ $json }}",
        "options": {}
      },
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        840,
        0
      ],
      "id": "b1868415-b9d3-4b57-86f2-c671adc57173",
      "name": "Respond"
    }
  ],
  "connections": {
    "Webhook \u00b7 /events": {
      "main": [
        [
          {
            "node": "Auth check",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Auth check": {
      "main": [
        [
          {
            "node": "Parse window",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Respond 401",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse window": {
      "main": [
        [
          {
            "node": "Google Calendar \u00b7 list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Google Calendar \u00b7 list": {
      "main": [
        [
          {
            "node": "Compact & classify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compact & classify": {
      "main": [
        [
          {
            "node": "Roll up",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Roll up": {
      "main": [
        [
          {
            "node": "Shape response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Shape response": {
      "main": [
        [
          {
            "node": "Respond",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "tags": [
    {
      "name": "n8nCal"
    }
  ]
}