{"workflow":{"id":14397,"name":"Sync workflow schedules between Google Sheets and Google Calendar","views":20,"recentViews":1,"totalViews":20,"createdAt":"2026-03-27T16:02:15.473Z","description":"# Sync n8n Workflow Schedules to Google Calendar\n\nReads every workflow on your n8n instance every 30 minutes, extracts their schedule triggers, and keeps a matching recurring event on Google Calendar — one event per workflow, forever in sync.\n\n## How it works\n\n```\nSchedule Trigger (30 min)\n  → GET /api/v1/workflows          — fetch all workflows\n  → Code: parsing                  — extract scheduleTrigger / cron nodes\n  → Sheets: Lookup                 — read saved state (schedule, On Calendar, EventID)\n  → Code: detect changes           — create / update / skip\n       ├─ create → build RRULE payload → Create event → write EventID to Sheets\n       └─ update → delete old event (parallel) + create new event → write to Sheets\n```\n\nState is stored in a Google Sheets tab (`n8n Scheduling`). The sheet acts as the single source of truth between runs.\n\n## What gets a Calendar event\n\n| Schedule type                          | Result                                                     |\n| -------------------------------------- | ---------------------------------------------------------- |\n| Daily                                  | DAILY recurring event                                      |\n| Weekly (with or without specific days) | WEEKLY recurring event                                     |\n| Monthly                                | MONTHLY recurring event                                    |\n| Hourly                                 | 1 DAILY event at `00:MM` (not 24 — avoids GCal rate limit) |\n| Cron / minutely                        | Skipped — not supported by Google Calendar RRULE           |\n| This workflow itself                   | Always skipped                                             |\n\n## Prerequisites\n\n- n8n instance with API enabled\n- Google Cloud project with:\n  - Service Account (for Sheets — never expires)\n  - OAuth 2.0 client (for Google Calendar — expires periodically)\n- A Google Sheets spreadsheet shared with the Service Account\n- A Google Calendar to write events to\n\n## Credentials\n\n| n8n credential type        | Used for                                   |\n| -------------------------- | ------------------------------------------ |\n| n8n API                    | Reading the workflow list                  |\n| Google Calendar OAuth2 API | Creating / deleting Calendar events        |\n| Google Service Account     | Reading and writing the Sheets state store |\n\n&gt; ⚠️ The Google Calendar OAuth2 credential expires. Reconnect it from **Settings → Credentials** when Calendar nodes start failing.\n\n## Setup\n\nFull step-by-step setup in [documentation.md](documentation.md).\n\n## Known limits\n\n- OAuth token expiry breaks sections D/E silently — set up the error workflow to get notified\n- Hourly schedules map to a single daily event (label includes `ogni ora :MM`)\n- The disconnected Webhook sub-flow (section F) is a manual maintenance utility — not part of the main pipeline","workflow":{"name":"Sync n8n Workflow Schedules to Google Calendar","nodes":[{"id":"9ae22e9d-0b4b-476c-9138-819eda9e0635","name":"Schedule Trigger","type":"n8n-nodes-base.scheduleTrigger","position":[-1728,512],"parameters":{"rule":{"interval":[{"triggerAtMinute":30}]}},"typeVersion":1.3},{"id":"3dcf261e-1442-470a-b828-8feb7ec668dd","name":"Code: parsing","type":"n8n-nodes-base.code","position":[-1152,512],"parameters":{"jsCode":"/**\n * Output: 1 row per trigger \"schedulabile\"\n * - include: scheduleTrigger, cron\n * - exclude: webhook, errorTrigger, manualTrigger (configurabile)\n */\n\nconst INCLUDE_MANUAL = false; // metti true se vuoi includere manualTrigger come \"Evento (manual)\"\n\nconst pad2 = (n) => String(n ?? 0).padStart(2, '0');\nconst DAY_NUM_TO_ICAL = { 1: 'MO', 2: 'TU', 3: 'WE', 4: 'TH', 5: 'FR', 6: 'SA', 7: 'SU' };\nconst DAY_STR_TO_ICAL = {\n  mon: 'MO', monday: 'MO', mo: 'MO',\n  tue: 'TU', tuesday: 'TU', tu: 'TU',\n  wed: 'WE', wednesday: 'WE', we: 'WE',\n  thu: 'TH', thursday: 'TH', th: 'TH',\n  fri: 'FR', friday: 'FR', fr: 'FR',\n  sat: 'SA', saturday: 'SA', sa: 'SA',\n  sun: 'SU', sunday: 'SU', su: 'SU',\n};\nconst icalToIt = { MO: 'lun', TU: 'mar', WE: 'mer', TH: 'gio', FR: 'ven', SA: 'sab', SU: 'dom' };\n\nfunction normalizeByDay(value) {\n  if (value == null) return null;\n  if (Array.isArray(value)) {\n    const mapped = value\n      .map(v => normalizeByDay(v))\n      .flatMap(v => (v ? v.split(',') : []));\n    const uniq = [...new Set(mapped)].filter(Boolean);\n    return uniq.length ? uniq.join(',') : null;\n  }\n  if (typeof value === 'number') return DAY_NUM_TO_ICAL[value] ?? null;\n  if (typeof value === 'string') {\n    const s = value.trim();\n    if (!s) return null;\n    if (/^(MO|TU|WE|TH|FR|SA|SU)(,(MO|TU|WE|TH|FR|SA|SU))*$/i.test(s)) return s.toUpperCase();\n    const lower = s.toLowerCase();\n    if (DAY_STR_TO_ICAL[lower]) return DAY_STR_TO_ICAL[lower];\n    const asNum = Number(lower);\n    if (!Number.isNaN(asNum)) return DAY_NUM_TO_ICAL[asNum] ?? null;\n  }\n  return null;\n}\n\nfunction humanSchedule(s) {\n  if (!s || !s.freq) return '';\n\n  const it = s.interval ?? 1;\n\n  if (s.freq === 'minutely') return it === 1 ? 'Ogni minuto' : `Ogni ${it} minuti`;\n  if (s.freq === 'hourly')   return `Ogni ora :${pad2(s.atMinute ?? 0)}`;\n  if (s.freq === 'daily')    return `Ogni giorno ${pad2(s.atHour)}:${pad2(s.atMinute)}`;\n\n  if (s.freq === 'weekly') {\n    const days = (s.byDay ?? '')\n      .split(',')\n      .filter(Boolean)\n      .map(d => icalToIt[d] ?? d)\n      .join(', ');\n    const dayTxt = days ? `Ogni ${days}` : 'Ogni settimana';\n    return `${dayTxt} ${pad2(s.atHour)}:${pad2(s.atMinute)}`;\n  }\n\n  if (s.freq === 'monthly') {\n    const dom = s.byMonthDay ?? '?';\n    return `Ogni mese (giorno ${dom}) ${pad2(s.atHour)}:${pad2(s.atMinute)}`;\n  }\n\n  if (s.freq === 'cron') {\n    return s.cronExpression ? `Cron: ${s.cronExpression}` : 'Cron';\n  }\n\n  if (s.freq === 'event') {\n    return s.eventType ? `Evento (${s.eventType})` : 'Evento';\n  }\n\n  return '';\n}\n\n/**\n * Robust scheduleTrigger parser:\n * handles:\n *  - rule.interval[0].field present (minutes/hours/days/weeks/months)\n *  - field missing but triggerAtHour/triggerAtMinute present (infer daily)\n *  - weekly/monthly signals via triggerAtDay / dayOfMonth / weekday / days\n */\nfunction parseScheduleTrigger(node) {\n  const rule = node?.parameters?.rule ?? null;\n  const tz = node?.parameters?.timezone || null;\n\n  const intervals = rule?.interval;\n  const i = Array.isArray(intervals) && intervals.length ? intervals[0] : (rule ?? {});\n\n  const out = {\n    triggerType: 'scheduleTrigger',\n    freq: null,\n    interval: 1,\n    atHour: null,\n    atMinute: null,\n    byDay: null,\n    byMonthDay: null,\n    timezone: tz,\n    scheduleRaw: { rule },\n    cronExpression: null,\n  };\n\n  // If cronExpression exists here\n  if (rule?.cronExpression) {\n    out.freq = 'cron';\n    out.cronExpression = rule.cronExpression;\n    out.scheduleHuman = humanSchedule(out);\n    return out;\n  }\n\n  const field = i.field;\n\n  // Helper reads (some versions use different keys)\n  const atHour = i.triggerAtHour ?? i.atHour ?? null;\n  const atMinute = i.triggerAtMinute ?? i.atMinute ?? null;\n  const trigDay = i.triggerAtDay ?? i.weekday ?? i.dayOfWeek ?? i.days ?? null;\n  const monthDay = i.dayOfMonth ?? i.monthDay ?? null;\n\n  // If field is present, use it\n  if (field === 'minutes') {\n    out.freq = 'minutely';\n    out.interval = i.minutesInterval ?? i.interval ?? 1;\n\n  } else if (field === 'hours') {\n    out.freq = 'hourly';\n    out.interval = i.hoursInterval ?? i.interval ?? 1;\n    out.atMinute = atMinute ?? 0;\n\n  } else if (field === 'days') {\n    out.freq = 'daily';\n    out.interval = i.daysInterval ?? i.interval ?? 1;\n    out.atHour = atHour ?? 0;\n    out.atMinute = atMinute ?? 0;\n\n  } else if (field === 'weeks') {\n    out.freq = 'weekly';\n    out.interval = i.weeksInterval ?? i.interval ?? 1;\n    out.atHour = atHour ?? 0;\n    out.atMinute = atMinute ?? 0;\n    out.byDay = normalizeByDay(trigDay);\n\n  } else if (field === 'months') {\n    out.freq = 'monthly';\n    out.interval = i.monthsInterval ?? i.interval ?? 1;\n    out.atHour = atHour ?? 0;\n    out.atMinute = atMinute ?? 0;\n    const dom = i.triggerAtDay ?? monthDay;\n    const domNum = typeof dom === 'number' ? dom : Number(dom);\n    out.byMonthDay = Number.isNaN(domNum) ? null : domNum;\n\n  } else {\n    // field missing -> infer\n    const hasHour = atHour !== null;\n    const hasMinute = atMinute !== null;\n\n    const byDay = normalizeByDay(trigDay);\n    const dom = i.triggerAtDay ?? monthDay;\n\n    if (byDay) {\n      // weekly-ish\n      out.freq = 'weekly';\n      out.atHour = atHour ?? 0;\n      out.atMinute = atMinute ?? 0;\n      out.byDay = byDay;\n    } else if (dom != null) {\n      // monthly-ish\n      out.freq = 'monthly';\n      out.atHour = atHour ?? 0;\n      out.atMinute = atMinute ?? 0;\n      const domNum = typeof dom === 'number' ? dom : Number(dom);\n      out.byMonthDay = Number.isNaN(domNum) ? null : domNum;\n    } else if (hasHour || hasMinute) {\n      // assume daily at HH:MM\n      out.freq = 'daily';\n      out.atHour = atHour ?? 0;\n      out.atMinute = atMinute ?? 0;\n    } else if (rule?.interval?.[0]?.field === undefined && Array.isArray(rule?.interval) && rule.interval.length) {\n      // still unknown shape; keep as unknown for debugging\n      out.freq = 'unknown';\n    } else {\n      out.freq = 'unknown';\n    }\n  }\n\n  out.scheduleHuman = humanSchedule(out);\n  return out;\n}\n\nfunction parseCronNode(node) {\n  const p = node?.parameters ?? {};\n  const cron =\n    p.cronExpression ||\n    p.expression ||\n    p.cron ||\n    p.rules?.[0]?.cronExpression ||\n    null;\n\n  return {\n    triggerType: 'cron',\n    freq: 'cron',\n    interval: 1,\n    atHour: null,\n    atMinute: null,\n    byDay: null,\n    byMonthDay: null,\n    timezone: p.timezone || null,\n    cronExpression: cron,\n    scheduleRaw: { parameters: p },\n    scheduleHuman: humanSchedule({ freq: 'cron', cronExpression: cron }),\n  };\n}\n\nfunction getTags(wf) {\n  const tags = wf.tags ?? [];\n  return tags.map(t => (typeof t === 'string' ? t : t.name)).filter(Boolean);\n}\n\nfunction getNodes(wf) {\n  return wf.activeVersion?.nodes || wf.nodes || [];\n}\n\nconst output = [];\n\nfor (const item of $input.all()) {\n  const wf = item.json;\n  const nodes = getNodes(wf);\n\n  // pick triggers\n  const triggers = nodes.filter(n => {\n    if (n.type === 'n8n-nodes-base.webhook') return false;          // EXCLUDE\n    if (n.type === 'n8n-nodes-base.errorTrigger') return false;     // EXCLUDE\n    if (n.type === 'n8n-nodes-base.manualTrigger') return INCLUDE_MANUAL; // optional\n    return (\n      n.type === 'n8n-nodes-base.scheduleTrigger' ||\n      n.type === 'n8n-nodes-base.cron'\n    );\n  });\n\n  // If no schedulable triggers: do nothing (keeps sheet clean)\n  if (triggers.length === 0) continue;\n\n  for (const t of triggers) {\n    const sched =\n      t.type === 'n8n-nodes-base.scheduleTrigger' ? parseScheduleTrigger(t)\n      : t.type === 'n8n-nodes-base.cron' ? parseCronNode(t)\n      : { triggerType: 'event', freq: 'event', scheduleHuman: 'Evento', scheduleRaw: { parameters: t.parameters ?? {} } };\n\n    output.push({\n      json: {\n        WorkflowID: wf.id,\n        Workflow: wf.name,\n        Tags: JSON.stringify(getTags(wf)),\n        Active: !!wf.active,\n\n        triggerType: sched.triggerType,\n        freq: sched.freq ?? '',\n        interval: sched.interval ?? '',\n        atHour: sched.atHour ?? '',\n        atMinute: sched.atMinute ?? '',\n        byDay: sched.byDay ?? '',\n        byMonthDay: sched.byMonthDay ?? '',\n        timezone: sched.timezone ?? (wf.settings?.timezone || ''),\n\n        schedule: sched.scheduleHuman ?? '',\n        \"Schedule-RAW\": JSON.stringify(sched.scheduleRaw ?? {}),\n      }\n    });\n  }\n}\n\nreturn output;\n"},"typeVersion":2},{"id":"807c9b17-1158-4da4-be3e-8de4751009d6","name":"Create an event","type":"n8n-nodes-base.googleCalendar","position":[1360,512],"parameters":{"end":"={{$json.calendarPayload.end}}","start":"={{$json.calendarPayload.start}}","calendar":{"__rl":true,"mode":"list","value":"YOUR_GOOGLE_CALENDAR_ID","cachedResultName":"YOUR_GOOGLE_CALENDAR_ID"},"additionalFields":{"color":"3","summary":"={{$json.calendarPayload.summary}}","visibility":"private","description":"={{$json.calendarPayload.description}}","repeatUntil":"={{$json.calendarPayload.repeatUntil}}","repeatFrecuency":"={{$json.calendarPayload.repeatFrequency}}"},"useDefaultReminders":false},"credentials":{"googleCalendarOAuth2Api":{"name":"Google Calendar OAuth2"}},"typeVersion":1.3},{"id":"f977bd11-3bc3-48f4-bba2-66531a6a5a90","name":"Code: RRULE","type":"n8n-nodes-base.code","position":[1040,512],"parameters":{"jsCode":"/**\n * Google Calendar payload builder.\n *\n * Schedules supportati:\n * - DAILY  \"Ogni giorno HH:MM\"              → 1 evento Daily\n * - WEEKLY \"Ogni settimana HH:MM\"           → 1 evento Weekly\n * - WEEKLY \"Ogni lun HH:MM\" / \"Ogni lun, mer HH:MM\" → 1 evento Weekly\n * - MONTHLY \"Ogni mese (giorno N) HH:MM\"   → 1 evento Monthly\n * - HOURLY \"Ogni ora :MM\"                   → 1 evento Daily (FIX: era 24, ora 1)\n * - MINUTELY / CRON                         → SKIP\n *\n * FIX: Hourly ora produce 1 solo evento (era 24 → rate limit).\n * FIX: Aggiunto supporto weekly con giorni e monthly.\n */\n\nconst DEFAULT_TIMEZONE  = 'Europe/Rome';\nconst EVENT_DURATION_MIN = 5;\n\nconst SKIP_WORKFLOW_NAME_CONTAINS = ['Workflow Scheduling Extraction'];\nconst SKIP_SCHEDULE_REGEXES = [\n  /ogni\\s+minuto/i,\n  /ogni\\s+\\d+\\s+minut/i,\n];\n\nconst FREQ = {\n  DAILY:   'Daily',\n  WEEKLY:  'Weekly',\n  MONTHLY: 'Monthly',\n};\n\nconst pad2 = (n) => String(n ?? 0).padStart(2, '0');\n\nfunction localDateTimeString(d) {\n  return `${d.getFullYear()}-${pad2(d.getMonth()+1)}-${pad2(d.getDate())}T${pad2(d.getHours())}:${pad2(d.getMinutes())}:${pad2(d.getSeconds())}`;\n}\n\nfunction startEndForToday(atHour, atMinute) {\n  const now = new Date();\n  const s = new Date(now.getFullYear(), now.getMonth(), now.getDate(), atHour ?? 0, atMinute ?? 0, 0);\n  const e = new Date(s);\n  e.setMinutes(e.getMinutes() + EVENT_DURATION_MIN);\n  return { start: localDateTimeString(s), end: localDateTimeString(e) };\n}\n\nfunction shouldSkip(r) {\n  const wfName = String(r.Workflow || r.workflow || r.name || '');\n  if (SKIP_WORKFLOW_NAME_CONTAINS.some(x => wfName.includes(x))) return true;\n  const sched = String(r.schedule || '');\n  if (SKIP_SCHEDULE_REGEXES.some(rx => rx.test(sched))) return true;\n  return false;\n}\n\n/**\n * Parses humanSchedule string → schedule type object.\n * Returns null if schedule type is unsupported/should be skipped.\n */\nfunction parseSchedule(scheduleHuman) {\n  if (!scheduleHuman) return null;\n  const txt = String(scheduleHuman).trim().toLowerCase();\n\n  // \"Ogni giorno 09:30\"\n  const mDaily = txt.match(/ogni\\s+giorno\\s+(\\d{1,2})\\s*[:.]\\s*(\\d{1,2})/);\n  if (mDaily) {\n    const h = Number(mDaily[1]), m = Number(mDaily[2]);\n    if (h >= 0 && h <= 23 && m >= 0 && m <= 59) return { type: 'daily', atHour: h, atMinute: m };\n  }\n\n  // \"Ogni settimana 07:00\" (nessun giorno specifico)\n  const mWeekly = txt.match(/ogni\\s+settimana\\s+(\\d{1,2})\\s*[:.]\\s*(\\d{1,2})/);\n  if (mWeekly) {\n    const h = Number(mWeekly[1]), m = Number(mWeekly[2]);\n    if (h >= 0 && h <= 23 && m >= 0 && m <= 59) return { type: 'weekly', atHour: h, atMinute: m };\n  }\n\n  // \"Ogni lun 07:00\" / \"Ogni lun, mer 07:00\" (FIX: weekly con giorni specifici)\n  const mWeeklyDay = txt.match(/^ogni\\s+(?:lun|mar|mer|gio|ven|sab|dom)[\\w,\\s]*\\s+(\\d{1,2})[:.]\\s*(\\d{1,2})/);\n  if (mWeeklyDay) {\n    const h = Number(mWeeklyDay[1]), m = Number(mWeeklyDay[2]);\n    if (h >= 0 && h <= 23 && m >= 0 && m <= 59) return { type: 'weekly', atHour: h, atMinute: m };\n  }\n\n  // \"Ogni mese (giorno N) HH:MM\" (FIX: aggiunto monthly)\n  const mMonthly = txt.match(/ogni\\s+mese.*\\s+(\\d{1,2}):(\\d{2})\\s*$/);\n  if (mMonthly) {\n    const h = Number(mMonthly[1]), m = Number(mMonthly[2]);\n    if (h >= 0 && h <= 23 && m >= 0 && m <= 59) return { type: 'monthly', atHour: h, atMinute: m };\n  }\n\n  // \"Ogni ora :45\" o \"Ogni ora .00\" (FIX: ora produce 1 evento invece di 24)\n  const mHourly = txt.match(/ogni\\s+ora\\s*[.:]\\s*(\\d{1,2})/);\n  if (mHourly) {\n    const m = Number(mHourly[1]);\n    if (m >= 0 && m <= 59) return { type: 'hourly', minute: m };\n  }\n\n  return null;  // cron, unknown, minutely → skip\n}\n\nfunction buildCommon(r) {\n  const wfName = r.Workflow || r.workflow || r.name || 'workflow';\n  const description =\n`WorkflowID: ${r.WorkflowID || r.workflowId || r.id || ''}\nTags: ${r.Tags || r.tags || ''}\nTriggerType: ${r.triggerType || ''}\nSchedule: ${r.schedule || ''}`;\n  return { summaryBase: `n8n | ${wfName}`, description };\n}\n\nconst out = [];\n\nfor (const item of $input.all()) {\n  const r = item.json;\n  if (shouldSkip(r)) continue;\n\n  const parsed = parseSchedule(r.schedule);\n  if (!parsed) {\n    console.log(`SKIP (schedule non supportato): ${r.WorkflowID} | ${r.schedule}`);\n    continue;\n  }\n\n  const timezone = r.timezone || DEFAULT_TIMEZONE;\n  const { summaryBase, description } = buildCommon(r);\n\n  if (parsed.type === 'daily') {\n    const { start, end } = startEndForToday(parsed.atHour, parsed.atMinute);\n    out.push({ json: { ...r, calendarPayload: { summary: summaryBase, description, start, end, timezone, repeatFrequency: FREQ.DAILY, repeatUntil: null } } });\n  }\n\n  if (parsed.type === 'weekly') {\n    const { start, end } = startEndForToday(parsed.atHour, parsed.atMinute);\n    out.push({ json: { ...r, calendarPayload: { summary: summaryBase, description, start, end, timezone, repeatFrequency: FREQ.WEEKLY, repeatUntil: null } } });\n  }\n\n  if (parsed.type === 'monthly') {\n    const { start, end } = startEndForToday(parsed.atHour, parsed.atMinute);\n    out.push({ json: { ...r, calendarPayload: { summary: summaryBase, description, start, end, timezone, repeatFrequency: FREQ.MONTHLY, repeatUntil: null } } });\n  }\n\n  if (parsed.type === 'hourly') {\n    // FIX: 1 evento Daily invece di 24 (riduceva a ~100 API calls → rate limit)\n    // L'evento è collocato a 00:MM e si ripete ogni giorno, con label \"ogni ora :MM\"\n    const { start, end } = startEndForToday(0, parsed.minute);\n    out.push({\n      json: {\n        ...r,\n        calendarPayload: {\n          summary: `${summaryBase} (ogni ora :${pad2(parsed.minute)})`,\n          description,\n          start,\n          end,\n          timezone,\n          repeatFrequency: FREQ.DAILY,\n          repeatUntil: null,\n        }\n      }\n    });\n  }\n}\n\nreturn out;\n"},"typeVersion":2},{"id":"7766b246-1694-43f4-8640-884dacb379e1","name":"Delete an event","type":"n8n-nodes-base.googleCalendar","onError":"continueRegularOutput","position":[-1136,1040],"parameters":{"eventId":"={{ $json.id }}","options":{"sendUpdates":"none"},"calendar":{"__rl":true,"mode":"list","value":"YOUR_GOOGLE_CALENDAR_ID","cachedResultName":"YOUR_GOOGLE_CALENDAR_ID"},"operation":"delete"},"credentials":{"googleCalendarOAuth2Api":{"name":"Google Calendar OAuth2"}},"typeVersion":1.3,"alwaysOutputData":true},{"id":"eada40f6-f268-434b-b3f2-2233bc0ae636","name":"Get many events2","type":"n8n-nodes-base.googleCalendar","position":[-1424,1040],"parameters":{"options":{"recurringEventHandling":"first"},"timeMax":"2100-12-31T00:00:00","timeMin":"2026-01-16T00:00:00","calendar":{"__rl":true,"mode":"list","value":"YOUR_GOOGLE_CALENDAR_ID","cachedResultName":"YOUR_GOOGLE_CALENDAR_ID"},"operation":"getAll","returnAll":true},"credentials":{"googleCalendarOAuth2Api":{"name":"Google Calendar OAuth2"}},"typeVersion":1.3},{"id":"b91a529d-f08e-4b17-86f5-a3972baff2ab","name":"Remove Duplicates","type":"n8n-nodes-base.removeDuplicates","position":[720,512],"parameters":{"compare":"selectedFields","options":{},"fieldsToCompare":"WorkflowID,current_schedule"},"typeVersion":2},{"id":"f4a38c2c-da29-47bb-a0b2-3bb2d5b5f396","name":"Sheets:Lookup-ExistOnCalendar","type":"n8n-nodes-base.googleSheets","position":[-528,512],"parameters":{"options":{},"sheetName":{"__rl":true,"mode":"list","value":607954075,"cachedResultName":"n8n Scheduling"},"documentId":{"__rl":true,"mode":"list","value":"YOUR_SPREADSHEET_ID","cachedResultName":"n8n"},"authentication":"serviceAccount"},"credentials":{"googleApi":{"name":"Google Service Account"}},"typeVersion":4.7,"alwaysOutputData":true},{"id":"5c8aae74-3d10-4432-bf1c-371fd65f1718","name":"Sheets:OnCalendar=YES","type":"n8n-nodes-base.googleSheets","position":[2016,512],"parameters":{"columns":{"value":{"Tags":"={{ $json.Tags }}","freq":"={{ $json.freq }}","byDay":"={{ $json.byDay }}","atHour":"={{ $json.atHour }}","Workflow":"={{ $json.Workflow }}","atMinute":"={{ $json.atMinute }}","interval":"={{ $json.interval }}","schedule":"={{ $json.schedule }}","WorkflowID":"={{ $json.WorkflowID }}","byMonthDay":"={{ $json.byMonthDay }}","On Calendar":"YES","triggerType":"={{ $json.triggerType }}","Calendar_EventID":"={{ $json.Calendar_EventID }}","calendarLastSync":"={{ $today }}"},"schema":[{"id":"WorkflowID","type":"string","display":true,"removed":false,"required":false,"displayName":"WorkflowID","defaultMatch":false,"canBeUsedToMatch":true},{"id":"Workflow","type":"string","display":true,"removed":false,"required":false,"displayName":"Workflow","defaultMatch":false,"canBeUsedToMatch":true},{"id":"Tags","type":"string","display":true,"removed":false,"required":false,"displayName":"Tags","defaultMatch":false,"canBeUsedToMatch":true},{"id":"triggerType","type":"string","display":true,"removed":false,"required":false,"displayName":"triggerType","defaultMatch":false,"canBeUsedToMatch":true},{"id":"schedule","type":"string","display":true,"removed":false,"required":false,"displayName":"schedule","defaultMatch":false,"canBeUsedToMatch":true},{"id":"Calendar_EventID","type":"string","display":true,"removed":false,"required":false,"displayName":"Calendar_EventID","defaultMatch":false,"canBeUsedToMatch":true},{"id":"On Calendar","type":"string","display":true,"required":false,"displayName":"On Calendar","defaultMatch":false,"canBeUsedToMatch":true},{"id":"calendarLastSync","type":"string","display":true,"required":false,"displayName":"calendarLastSync","defaultMatch":false,"canBeUsedToMatch":true},{"id":"freq","type":"string","display":true,"removed":false,"required":false,"displayName":"freq","defaultMatch":false,"canBeUsedToMatch":false},{"id":"atHour","type":"string","display":true,"removed":false,"required":false,"displayName":"atHour","defaultMatch":false,"canBeUsedToMatch":false},{"id":"atMinute","type":"string","display":true,"removed":false,"required":false,"displayName":"atMinute","defaultMatch":false,"canBeUsedToMatch":false},{"id":"byDay","type":"string","display":true,"removed":false,"required":false,"displayName":"byDay","defaultMatch":false,"canBeUsedToMatch":false},{"id":"byMonthDay","type":"string","display":true,"removed":false,"required":false,"displayName":"byMonthDay","defaultMatch":false,"canBeUsedToMatch":false},{"id":"interval","type":"string","display":true,"removed":false,"required":false,"displayName":"interval","defaultMatch":false,"canBeUsedToMatch":false}],"mappingMode":"defineBelow","matchingColumns":["WorkflowID"],"attemptToConvertTypes":false,"convertFieldsToString":false},"options":{},"operation":"appendOrUpdate","sheetName":{"__rl":true,"mode":"list","value":607954075,"cachedResultName":"n8n Scheduling"},"documentId":{"__rl":true,"mode":"list","value":"YOUR_SPREADSHEET_ID","cachedResultName":"n8n"},"authentication":"serviceAccount"},"credentials":{"googleApi":{"name":"Google Service Account"}},"typeVersion":4.7},{"id":"3618a0a7-5b23-4f5a-94a5-9c57999af221","name":"Get many workflows","type":"n8n-nodes-base.n8n","position":[-1472,512],"parameters":{"filters":{"activeWorkflows":true},"requestOptions":{}},"credentials":{"n8nApi":{"name":"n8n API"}},"typeVersion":1,"alwaysOutputData":false},{"id":"78b8189a-02c8-4598-b3ef-279a0765fe7c","name":"Webhook","type":"n8n-nodes-base.webhook","disabled":true,"position":[-1744,1040],"parameters":{"path":"delete-calendar","options":{}},"typeVersion":2.1},{"id":"7b47f7cf-a72b-44ce-a30e-4ce2695a6de9","name":"Code: detect changes","type":"n8n-nodes-base.code","position":[112,512],"parameters":{"jsCode":"/**\n * Detect Changes - confronta 'schedule' (stringa human) tra stato corrente e Sheets.\n *\n * Logica:\n *   - On Calendar vuoto O schedule salvato vuoto → CREATE\n *   - schedule corrente ≠ schedule salvato → UPDATE\n *   - schedule è cron/minutely E già su Calendar → SKIP (evita loop 410 su delete)\n *   - altrimenti → SKIP\n */\n\n// Schedule types che RRULE non può gestire — non creare nuovi eventi per questi\nconst UNSUPPORTED_REGEXES = [\n  /ogni\\s+minuto/i,\n  /ogni\\s+\\d+\\s+minut/i,\n  /^cron:/i,\n];\n\nfunction isUnsupported(schedule) {\n  return !schedule || UNSUPPORTED_REGEXES.some(rx => rx.test(String(schedule).trim()));\n}\n\nreturn items.map(item => {\n  const data = item.json;\n\n  const currentSchedule = String(data.current_schedule ?? '');\n  const savedSchedule   = String(data.schedule ?? '');\n  const onCalendar      = data['On Calendar'] || '';\n  const workflowId      = data.WorkflowID || data.current_WorkflowID || '';\n\n  console.log(`=== ${workflowId} | current=\"${currentSchedule}\" | saved=\"${savedSchedule}\" | onCal=\"${onCalendar}\"`);\n\n  if (!workflowId) {\n    return { json: { ...data, action: 'skip', scheduleChanged: false, changedFields: '' } };\n  }\n\n  let action = 'skip';\n  let scheduleChanged = false;\n\n  const savedIsEmpty  = !savedSchedule;\n  const isNewWorkflow = !onCalendar || savedIsEmpty;\n\n  if (isNewWorkflow) {\n    // Nuovo workflow: crea solo se lo schedule è supportato da RRULE\n    if (isUnsupported(currentSchedule)) {\n      action = 'skip';\n      console.log('→ skip (nuovo ma schedule non supportato da RRULE)');\n    } else {\n      action = 'create';\n      console.log('→ create (nuovo)');\n    }\n\n  } else if (onCalendar === 'YES') {\n\n    if (currentSchedule !== savedSchedule) {\n      // Schedule cambiato\n      if (isUnsupported(currentSchedule)) {\n        // Nuovo schedule non supportabile → skip, lascia evento vecchio su Calendar\n        // (l'evento sarà stale ma non causa loop 410)\n        action = 'skip';\n        console.log('→ skip (schedule cambiato in tipo non supportato - lascia evento esistente)');\n      } else {\n        action = 'update';\n        scheduleChanged = true;\n        console.log(`→ update (schedule cambiato: \"${currentSchedule}\" vs \"${savedSchedule}\")`);\n      }\n    } else {\n      action = 'skip';\n      console.log('→ skip (invariato)');\n    }\n\n  } else {\n    // On Calendar = 'NO'\n    if (isUnsupported(currentSchedule)) {\n      action = 'skip';\n      console.log('→ skip (On Calendar=NO, schedule non supportato)');\n    } else {\n      action = 'create';\n      console.log('→ create (On Calendar=NO)');\n    }\n  }\n\n  return {\n    json: {\n      ...data,\n      action,\n      scheduleChanged,\n      changedFields: scheduleChanged ? 'schedule' : '',\n      _debug: { workflowId, currentSchedule, savedSchedule, onCalendar, isNewWorkflow }\n    }\n  };\n});\n"},"typeVersion":2},{"id":"d6caddd9-77d7-42ee-b9c3-ac6ed24a6bdb","name":"Switch","type":"n8n-nodes-base.switch","position":[416,496],"parameters":{"rules":{"values":[{"outputKey":"create","conditions":{"options":{"version":3,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"145d9953-de54-49da-a77e-4b36ac6746c4","operator":{"type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"create"}]},"renameOutput":true},{"outputKey":"update","conditions":{"options":{"version":3,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"e8db78af-3bc3-4902-83e5-1654aab87e60","operator":{"name":"filter.operator.equals","type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"update"}]},"renameOutput":true},{"outputKey":"skip","conditions":{"options":{"version":3,"leftValue":"","caseSensitive":true,"typeValidation":"strict"},"combinator":"and","conditions":[{"id":"2cab4207-3a61-421a-a62a-b2c5a3601691","operator":{"name":"filter.operator.equals","type":"string","operation":"equals"},"leftValue":"={{ $json.action }}","rightValue":"skip"}]},"renameOutput":true}]},"options":{}},"typeVersion":3.4},{"id":"98859d16-bf8b-46ad-b2a1-739c1e7e304d","name":"Code: save current values","type":"n8n-nodes-base.code","position":[-848,512],"parameters":{"jsCode":"// FIX: usa ?? '' (non || '') per non perdere valori numerici come atHour=0 o atMinute=0\nreturn items.map(item => {\n  return {\n    json: {\n      ...item.json,\n      current_schedule:    item.json.schedule    ?? '',\n      current_freq:        item.json.freq        ?? '',\n      current_atHour:      String(item.json.atHour   != null ? item.json.atHour   : ''),\n      current_atMinute:    String(item.json.atMinute != null ? item.json.atMinute : ''),\n      current_byDay:       item.json.byDay       ?? '',\n      current_byMonthDay:  String(item.json.byMonthDay != null ? item.json.byMonthDay : ''),\n      current_interval:    String(item.json.interval  != null ? item.json.interval  : ''),\n      current_WorkflowID:  item.json.WorkflowID  ?? ''\n    }\n  };\n});\n"},"typeVersion":2},{"id":"3d5e5933-7d47-4a01-816e-d6197025821f","name":"Code manual merge","type":"n8n-nodes-base.code","position":[-208,512],"parameters":{"jsCode":"/**\n * Match manuale: combina i dati correnti con quelli salvati dal foglio.\n * FIX: Calendar_EventID ora propagato correttamente (serve per cancellare eventi vecchi).\n */\n\nconst currentWorkflows = $('Code: save current values').all();\nconst sheetRows = items;\n\nconsole.log(`Current workflows: ${currentWorkflows.length}`);\nconsole.log(`Sheet rows: ${sheetRows.length}`);\n\nconst savedMap = {};\nfor (const row of sheetRows) {\n  const wfId = row.json.WorkflowID;\n  if (wfId) savedMap[wfId] = row.json;\n}\n\nconsole.log(`Found ${Object.keys(savedMap).length} unique workflows in sheet`);\n\nconst output = currentWorkflows.map(current => {\n  const wfId = current.json.WorkflowID;\n  const saved = savedMap[wfId] || {};\n  const hasSavedData = Object.keys(saved).length > 0;\n\n  return {\n    json: {\n      ...current.json,\n\n      // Dati salvati su Sheets (usa null-coalesce per non perdere valori numerici tipo 0)\n      schedule:         saved.schedule          || '',\n      freq:             saved.freq              || '',\n      atHour:           saved.atHour   != null  ? saved.atHour   : '',\n      atMinute:         saved.atMinute != null  ? saved.atMinute : '',\n      byDay:            saved.byDay             || '',\n      byMonthDay:       saved.byMonthDay != null ? saved.byMonthDay : '',\n      interval:         saved.interval != null  ? saved.interval : '',\n      'On Calendar':    saved['On Calendar']    || '',\n      // ✅ FIX CRITICO: Calendar_EventID era mancante → UPDATE non cancellava mai i vecchi eventi\n      'Calendar_EventID': saved['Calendar_EventID'] || '',\n\n      _matchDebug: {\n        workflowId:       wfId,\n        foundInSheet:     hasSavedData,\n        totalInSheet:     Object.keys(savedMap).length,\n        savedSchedule:    saved.schedule           || 'EMPTY',\n        currentSchedule:  current.json.current_schedule || 'EMPTY',\n        calendarEventID:  saved['Calendar_EventID'] || 'EMPTY',\n      }\n    }\n  };\n});\n\nconsole.log(`Returning ${output.length} merged items`);\nreturn output;\n"},"typeVersion":2},{"id":"ef89bad9-1015-4fc3-8bcc-6ae0755255e3","name":"Code: post-create","type":"n8n-nodes-base.code","position":[1696,512],"parameters":{"jsCode":"/**\n * Post-create: aggrega gli event ID di Google Calendar per workflowId.\n *\n * Input: N items da 'Create an event' (risposta GCal - uno per evento creato)\n * Cross-ref: $('Code: RRULE').all() per recuperare i dati workflow originali\n * Output: 1 item per workflowId -> Sheets:OnCalendar=YES\n */\n\nconst allRruleItems = $('Code: RRULE').all();\nconst byWorkflow = {};\n\nfor (let i = 0; i < items.length; i++) {\n  const created = items[i].json;\n  const rruleItem = allRruleItems[i];\n  if (!rruleItem) continue;\n\n  const d = rruleItem.json;\n  const wfId = d.WorkflowID || d.current_WorkflowID || '';\n  if (!wfId) continue;\n\n  if (!byWorkflow[wfId]) {\n    byWorkflow[wfId] = { data: d, eventIds: [] };\n  }\n  const eventId = created.id || '';\n  if (eventId) byWorkflow[wfId].eventIds.push(eventId);\n}\n\nreturn Object.entries(byWorkflow).map(([wfId, { data: d, eventIds }]) => ({\n  json: {\n    WorkflowID: wfId,\n    Workflow: d.Workflow || '',\n    Tags: d.Tags || '',\n    triggerType: d.triggerType || '',\n    schedule: d.current_schedule || d.schedule || '',\n    freq: d.current_freq || d.freq || '',\n    atHour: String(d.current_atHour ?? d.atHour ?? ''),\n    atMinute: String(d.current_atMinute ?? d.atMinute ?? ''),\n    byDay: String(d.current_byDay ?? d.byDay ?? ''),\n    byMonthDay: String(d.current_byMonthDay ?? d.byMonthDay ?? ''),\n    interval: String(d.current_interval ?? d.interval ?? ''),\n    'On Calendar': 'YES',\n    Calendar_EventID: eventIds.join(' '),\n    calendarLastSync: new Date().toISOString().split('T')[0]\n  }\n}));\n"},"typeVersion":2},{"id":"d57f09f3-25bb-4b9a-8e98-9374cb7be397","name":"Code: split eventIds for delete","type":"n8n-nodes-base.code","position":[768,1040],"parameters":{"jsCode":"/**\n * Split eventIds: estrae un item per Calendar_EventID da eliminare.\n *\n * Input: items da Switch[update] (hanno Calendar_EventID spazio-separato)\n * Output: 1 item per eventId { calendarEventIdToDelete: 'gCal_event_id' }\n *\n * Se Calendar_EventID e' vuoto -> nessun output (niente da eliminare).\n * Il percorso UPDATE crea comunque nuovi eventi via Remove Duplicates.\n */\n\nconst output = [];\nfor (const item of items) {\n  const raw = String(item.json.Calendar_EventID || '').trim();\n  const ids = raw.split(/\\s+/).filter(id => id.length > 0);\n  for (const eid of ids) {\n    output.push({ json: { calendarEventIdToDelete: eid } });\n  }\n}\nreturn output;\n"},"typeVersion":2},{"id":"f22523dc-c631-4daa-8bd1-40a3de59d8b3","name":"Delete old event","type":"n8n-nodes-base.googleCalendar","position":[1056,1040],"parameters":{"eventId":"={{ $json.calendarEventIdToDelete }}","options":{"sendUpdates":"none"},"calendar":{"__rl":true,"mode":"list","value":"YOUR_GOOGLE_CALENDAR_ID","cachedResultName":"YOUR_GOOGLE_CALENDAR_ID"},"operation":"delete"},"credentials":{"googleCalendarOAuth2Api":{"name":"Google Calendar OAuth2"}},"typeVersion":1.3,"continueOnFail":true},{"id":"ea296d83-c150-4241-81a9-228c5b3f2db5","name":"__title","type":"n8n-nodes-base.stickyNote","position":[-1824,48],"parameters":{"color":7,"width":4104,"height":80,"content":"## 🕒 n8n – Workflow Scheduling Extraction\nScans all active n8n workflows every **30 min** · syncs schedules as recurring events on Google Calendar `YOUR_GOOGLE_ACCOUNT@gmail.com` · state store: Google Sheets `n8n Scheduling` tab"},"typeVersion":1},{"id":"f4b3437c-a404-460a-aa80-abfbfbe3e6c3","name":"__sec_fetch","type":"n8n-nodes-base.stickyNote","position":[-1824,144],"parameters":{"color":7,"width":584,"height":544,"content":"### A · Fetch\n\nRetrieves all n8n workflows via the n8n REST API. Both active and inactive workflows are fetched; filtering to scheduleTrigger-only happens in **Code: parsing**."},"typeVersion":1},{"id":"e5a7b6f2-3ea5-45f1-98d0-1947f109854e","name":"__sec_parse","type":"n8n-nodes-base.stickyNote","position":[-1216,144],"parameters":{"color":7,"width":908,"height":544,"content":"### B · Parse & Lookup\n\nNormalises each workflow's trigger configuration into a human-readable schedule string. Then reads the current Sheets state (schedule, On Calendar, Calendar_EventID) for every WorkflowID so it can be compared downstream."},"typeVersion":1},{"id":"9f5e07d5-b3f7-4535-bcb0-18282622262a","name":"__sec_detect","type":"n8n-nodes-base.stickyNote","position":[-272,144],"parameters":{"color":7,"width":880,"height":544,"content":"### C · Change Detection\n\nMerges live workflow data with Sheets state. Compares `current_schedule` (live) vs `schedule` (Sheets) and assigns an **action**: `create` (new) · `update` (changed) · `skip` (unchanged or unsupported type). Cron and minutely schedules are always skipped."},"typeVersion":1},{"id":"4d76bdd7-e4b0-4322-a8b9-e41596259524","name":"__sec_create","type":"n8n-nodes-base.stickyNote","position":[640,144],"parameters":{"color":7,"width":1640,"height":528,"content":"### D · Create / Update Path\n\nBuilds a Google Calendar recurring event payload (RRULE) and creates the event. On **update** this branch runs in parallel with the Delete branch (E). After creation, writes Calendar_EventID + On Calendar=YES back to Sheets."},"typeVersion":1},{"id":"297bdd17-8445-4781-99ed-814945170bdb","name":"__sec_delete","type":"n8n-nodes-base.stickyNote","position":[640,704],"parameters":{"color":7,"width":700,"height":496,"content":"### E · Delete Old Event\n\nRuns in parallel with branch D on **update**. Deletes the previous Calendar event(s) identified by Calendar_EventID from Sheets. `continueOnFail = true` — HTTP 410 (already gone) does not halt execution."},"typeVersion":1},{"id":"3f1980e5-cc38-4362-9a4f-4f7295193e77","name":"__sec_webhook","type":"n8n-nodes-base.stickyNote","position":[-1824,736],"parameters":{"color":7,"width":940,"height":470,"content":"### F · Webhook Sub-flow  *(disconnected – manual trigger)*\n\nStandalone maintenance endpoint. Accepts an HTTP POST with event IDs, fetches the matching Calendar events, and hard-deletes them. Not connected to the main 30-min pipeline."},"typeVersion":1},{"id":"eec1eecc-2a8f-4cfa-9ea9-e49372c00922","name":"__node_schedule_trigger","type":"n8n-nodes-base.stickyNote","position":[-1808,256],"parameters":{"color":7,"width":260,"height":380,"content":"**Schedule Trigger**\n\nFires every 30 min (cron `*/30 * * * *`).\nAlso supports manual execution from the n8n UI.\nEntry point for the entire sync pipeline."},"typeVersion":1},{"id":"07b02e91-7f2b-496a-b2db-ec4d36833299","name":"__node_get_many_workflows","type":"n8n-nodes-base.stickyNote","position":[-1536,256],"parameters":{"color":7,"width":260,"height":412,"content":"**Get many workflows**\n\nCalls `GET /api/v1/workflows` on the local n8n instance.\nReturns all workflows (active + inactive).\nNo filter applied here — filtering is done in Code: parsing."},"typeVersion":1},{"id":"d6c8e7a1-7073-4074-8c61-6e3b3fff0e79","name":"__node_code_parsing","type":"n8n-nodes-base.stickyNote","position":[-1216,256],"parameters":{"color":7,"width":260,"height":412,"content":"**Code: parsing**\n\n- Filters to workflows that have a `scheduleTrigger` node\n- Excludes system / internal workflows by name\n- Normalises trigger config → human schedule string\n- Extracts: WorkflowID, name, tags, triggerType, schedule, freq, atHour, atMinute, byDay, byMonthDay, interval"},"typeVersion":1},{"id":"abe5abb3-7c89-4f77-b7a9-23d51ef51daf","name":"__node_code_save","type":"n8n-nodes-base.stickyNote","position":[-912,256],"parameters":{"color":7,"width":260,"height":412,"content":"**Code: save current values**\n\nSnapshots all parsed fields into `current_*` prefixed copies\n(e.g. `current_schedule`, `current_freq`, …).\nPreserves the live state before it is overwritten by the Sheets\nmerge so change detection can compare fresh vs. persisted data."},"typeVersion":1},{"id":"e7c98db5-8183-4a2d-aa4d-a95f0f6be0d4","name":"__node_sheets_lookup","type":"n8n-nodes-base.stickyNote","position":[-592,256],"parameters":{"color":7,"width":260,"height":412,"content":"**Sheets: Lookup ExistOnCalendar**\n\nReads every row in the \"n8n Scheduling\" tab of the spreadsheet.\nReturns per-workflow: `schedule`, `On Calendar`, `Calendar_EventID`, `calendarLastSync`.\nUses a Google Service Account (does not expire — no OAuth re-auth needed)."},"typeVersion":1},{"id":"f3b6e523-8268-4cd6-9a86-fab7a0b3b663","name":"__node_code_merge","type":"n8n-nodes-base.stickyNote","position":[-256,256],"parameters":{"color":7,"width":260,"height":412,"content":"**Code manual merge**\n\nJoins live workflow items with Sheets rows on WorkflowID.\nPropagates from Sheets: `schedule`, `On Calendar`, `Calendar_EventID`.\n`current_*` fields are kept intact from Code: save current values.\nCritical: `Calendar_EventID` must flow through here for the delete branch to work."},"typeVersion":1},{"id":"b2edab53-4877-4dae-9f95-a7a144be4b41","name":"__node_code_detect","type":"n8n-nodes-base.stickyNote","position":[48,256],"parameters":{"color":7,"width":260,"height":396,"content":"**Code: detect changes**\n\nCompares `current_schedule` (live) vs `schedule` (Sheets).\nUses only the human string — not granular fields — to avoid false positives from stale rows.\n- Empty saved schedule or no Calendar entry → **create**\n- Strings differ → **update** (unless new schedule is unsupported → skip)\n- Strings equal → **skip**\n- cron / minutely / unsupported type → always **skip**"},"typeVersion":1},{"id":"0135024e-1f68-4378-a436-fbe7e4c755bc","name":"__node_switch","type":"n8n-nodes-base.stickyNote","position":[336,256],"parameters":{"color":7,"width":260,"height":412,"content":"**Switch**\n\nRoutes items by the `action` field:\n- `out0` → **create**  (new workflow, no Calendar event yet)\n- `out1` → **update**  (schedule changed; two parallel branches: D + E)\n- `out2` → **skip**    (no change; terminal — no downstream nodes)"},"typeVersion":1},{"id":"b0fc2d09-58c9-4093-9587-c04b19c302c1","name":"__node_remove_dupes","type":"n8n-nodes-base.stickyNote","position":[656,256],"parameters":{"color":7,"width":260,"height":380,"content":"**Remove Duplicates**\n\nDeduplicates by WorkflowID before event creation.\nDefensive node: prevents accidental double-creation if a workflow\nappears multiple times in the current batch (e.g. after a manual re-run)."},"typeVersion":1},{"id":"30577841-cce6-48e4-b9c8-9064856eeac5","name":"__node_code_rrule","type":"n8n-nodes-base.stickyNote","position":[960,256],"parameters":{"color":7,"width":260,"height":380,"content":"**Code: RRULE**\n\nConverts `current_schedule` string → Google Calendar event payload with RRULE recurrence.\nSupported types: daily · weekly (with or without specific days) · monthly · hourly.\nHourly → 1 daily event at 00:MM (not 24 events — avoids API rate limiting).\nUnsupported: cron, minutely → item silently dropped (returns empty array)."},"typeVersion":1},{"id":"afb8e9c5-a0be-4c7c-9649-77d97d68a22f","name":"__node_create_event","type":"n8n-nodes-base.stickyNote","position":[1280,256],"parameters":{"color":7,"width":260,"height":396,"content":"**Create an event**\n\nGoogle Calendar node. Creates a recurring event on `YOUR_GOOGLE_ACCOUNT@gmail.com`.\nUses `repeatFrequency` + `repeatUntil = null` for infinite recurrence.\nReturns the full GCal event object including the `id` used downstream.\nOAuth credential: `Google Calendar OAuth2` (expires periodically)."},"typeVersion":1},{"id":"8009e9f3-59ef-47aa-af81-00344237954b","name":"__node_code_post_create","type":"n8n-nodes-base.stickyNote","position":[1616,256],"parameters":{"color":7,"width":260,"height":412,"content":"**Code: post-create**\n\nAggregates created event IDs by WorkflowID (handles batch creates).\nAdds `On Calendar = YES`, `Calendar_EventID`, `calendarLastSync` (ISO timestamp).\nPrepares the exact row structure expected by Sheets: OnCalendar=YES."},"typeVersion":1},{"id":"0da373d3-2a4d-4fed-9d54-4541e73c0899","name":"__node_sheets_yes","type":"n8n-nodes-base.stickyNote","position":[1952,256],"parameters":{"color":7,"width":260,"height":412,"content":"**Sheets: OnCalendar=YES**\n\n`appendOrUpdate` operation matching on the **WorkflowID** column.\nWrites all schedule fields + `On Calendar=YES` + `Calendar_EventID` + `calendarLastSync`.\nService Account credential (never expires).\nThis is the canonical state store — downstream runs read from here."},"typeVersion":1},{"id":"d693564a-7a09-4880-bf53-e760331b118d","name":"__node_code_split","type":"n8n-nodes-base.stickyNote","position":[688,816],"parameters":{"color":7,"width":260,"height":366,"content":"**Code: split eventIds for delete**\n\nSplits the `Calendar_EventID` field (comma-separated string) into one item per ID.\nRequired because a workflow can accumulate multiple event IDs across runs.\nFeeds individual IDs to Delete old event."},"typeVersion":1},{"id":"57e6e365-7fd8-40b5-b948-a20a635dd9c0","name":"__node_delete_old","type":"n8n-nodes-base.stickyNote","position":[992,816],"parameters":{"color":7,"width":260,"height":382,"content":"**Delete old event**\n\nDeletes the previous Calendar event(s) when a schedule update occurs.\n`continueOnFail = true` — HTTP 410 \"Resource has been deleted\" is tolerated and does not halt execution.\nTerminal node — no downstream connections."},"typeVersion":1},{"id":"32cdfa17-1447-4dc3-9e38-0e42dd21fadb","name":"__node_webhook","type":"n8n-nodes-base.stickyNote","position":[-1808,848],"parameters":{"color":7,"width":260,"height":350,"content":"**Webhook**\n\nHTTP POST trigger for manual Calendar cleanup.\nNot connected to the main 30-min pipeline.\nAccepts event IDs in the request body."},"typeVersion":1},{"id":"09035062-7137-45c7-a4b4-d3ffca205ba2","name":"__node_get_events2","type":"n8n-nodes-base.stickyNote","position":[-1504,848],"parameters":{"color":7,"width":260,"height":350,"content":"**Get many events2**\n\nRetrieves Calendar events by ID or search query.\nFeeds the event list into Delete an event."},"typeVersion":1},{"id":"0b136e75-9d51-4280-a375-08b2ac28dbae","name":"__node_delete_event","type":"n8n-nodes-base.stickyNote","position":[-1184,848],"parameters":{"color":7,"width":260,"height":350,"content":"**Delete an event**\n\nHard-deletes Calendar events by ID.\nUsed only in the manual webhook-triggered maintenance flow."},"typeVersion":1},{"id":"a1b2c3d4-e5f6-7890-abcd-ef1234567890","name":"__overview","type":"n8n-nodes-base.stickyNote","position":[-2544,48],"parameters":{"width":680,"height":1288,"content":"## n8n Workflow Scheduling Extraction\n\nReads all n8n workflows every 30 min via REST API, extracts their schedules, compares against a Google Sheets state store, and syncs Google Calendar with recurring events.\n\n📄 Full docs: https://paoloronco.notion.site/n8n-Workflow-Scheduling-Extraction-Setup-Docs-330f0ba27c3280ef99b2c5e8e7dfd497\n\n---\n\n### What it does\n- **Fetch** all workflows via `GET /api/v1/workflows`\n- **Parse** scheduleTrigger / cron nodes → human-readable string (e.g. \"Every day 09:30\")\n- **Lookup** saved state from Sheets: `schedule`, `On Calendar`, `Calendar_EventID`\n- **Detect changes**: `create` · `update` · `skip`\n  - create: new workflow, no Calendar event yet\n  - update: schedule changed → delete old event + create new one\n  - skip: unchanged, or unsupported type (cron / minutely)\n- **Create**: build RRULE payload → create GCal event → write EventID to Sheets\n- **Update**: parallel branches — delete old event + create path\n- This workflow itself is always skipped (name filter)\n\n---\n\n### Required credentials\n- **n8n API key** — Settings → API → Add key → credential `n8n account`\n- **Google Calendar OAuth2** — credential `Oauth GCalendar`\n  ⚠️ Expires periodically — reconnect from Settings → Credentials\n- **Google Service Account** — credential `GCP_SA`\n  Never expires. Used exclusively for Google Sheets\n\n---\n\n### Setup\n1. n8n Settings → API → create API key → credential `n8n account`\n2. GCP Console → create Service Account → download JSON key\n   → import in n8n as Google Service Account (`GCP_SA`)\n3. Share the Google Sheets with the SA email (Editor role)\n   Sheet: `YOUR_SPREADSHEET_ID` · Tab: `n8n Scheduling`\n4. GCP → OAuth 2.0 client ID → credential Google Calendar OAuth2 (`Oauth GCalendar`)\n   → click \"Connect\" in n8n to authorize\n5. Workflow Settings → Error Workflow = `YOUR_ERROR_WORKFLOW_ID`\n6. Activate the workflow — first run populates Sheets and Calendar\n\n---\n\n### Known limits\n- **Hourly**: 1 daily event at 00:MM, not 24 (avoids GCal rate limit)\n- **Cron / minutely**: always skipped, never appear on Calendar\n- **Webhook sub-flow** (section F): disconnected, manual maintenance only\n- **OAuth GCalendar**: expiry blocks sections D/E — monitor regularly\n"},"typeVersion":1}],"active":true,"settings":{"callerPolicy":"workflowsFromSameOwner","executionOrder":"v1"},"connections":{"Switch":{"main":[[{"node":"Remove Duplicates","type":"main","index":0}],[{"node":"Remove Duplicates","type":"main","index":0},{"node":"Code: split eventIds for delete","type":"main","index":0}]]},"Webhook":{"main":[[{"node":"Get many events2","type":"main","index":0}]]},"Code: RRULE":{"main":[[{"node":"Create an event","type":"main","index":0}]]},"Code: parsing":{"main":[[{"node":"Code: save current values","type":"main","index":0}]]},"Create an event":{"main":[[{"node":"Code: post-create","type":"main","index":0}]]},"Get many events2":{"main":[[{"node":"Delete an event","type":"main","index":0}]]},"Schedule Trigger":{"main":[[{"node":"Get many workflows","type":"main","index":0}]]},"Code manual merge":{"main":[[{"node":"Code: detect changes","type":"main","index":0}]]},"Code: post-create":{"main":[[{"node":"Sheets:OnCalendar=YES","type":"main","index":0}]]},"Remove Duplicates":{"main":[[{"node":"Code: RRULE","type":"main","index":0}]]},"Get many workflows":{"main":[[{"node":"Code: parsing","type":"main","index":0}]]},"Code: detect changes":{"main":[[{"node":"Switch","type":"main","index":0}]]},"Code: save current values":{"main":[[{"node":"Sheets:Lookup-ExistOnCalendar","type":"main","index":0}]]},"Sheets:Lookup-ExistOnCalendar":{"main":[[{"node":"Code manual merge","type":"main","index":0}]]},"Code: split eventIds for delete":{"main":[[{"node":"Delete old event","type":"main","index":0}]]}}},"lastUpdatedBy":1,"workflowInfo":{"nodeCount":44,"nodeTypes":{"n8n-nodes-base.n8n":{"count":1},"n8n-nodes-base.code":{"count":7},"n8n-nodes-base.switch":{"count":1},"n8n-nodes-base.webhook":{"count":1},"n8n-nodes-base.stickyNote":{"count":26},"n8n-nodes-base.googleSheets":{"count":2},"n8n-nodes-base.googleCalendar":{"count":4},"n8n-nodes-base.scheduleTrigger":{"count":1},"n8n-nodes-base.removeDuplicates":{"count":1}}},"status":"published","readyToDemo":null,"user":{"name":"Paolo Ronco","username":"paoloronco","bio":"Cybersecurity Analyst passionate about automation, cloud, and infrastructure.\nSince discovering n8n, I automate everything I can: workflows, processes, integrations, and repetitive tasks.\nI love building, optimizing, and making systems more secure, faster, and scalable.","verified":true,"links":["https://paoloronco.it"],"avatar":"https://gravatar.com/avatar/f0b622283139bed01fcf8336ec78291bbd066c115b3b6b419b8b854ecbbb7097?r=pg&d=retro&size=200"},"nodes":[{"id":18,"icon":"file:googleSheets.svg","name":"n8n-nodes-base.googleSheets","codex":{"data":{"alias":["CSV","Sheet","Spreadsheet","GS"],"resources":{"generic":[{"url":"https://n8n.io/blog/love-at-first-sight-ricardos-n8n-journey/","icon":"❤️","label":"Love at first sight: Ricardo’s n8n journey"},{"url":"https://n8n.io/blog/why-business-process-automation-with-n8n-can-change-your-daily-life/","icon":"🧬","label":"Why business process automation with n8n can change your daily life"},{"url":"https://n8n.io/blog/automatically-adding-expense-receipts-to-google-sheets-with-telegram-mindee-twilio-and-n8n/","icon":"🧾","label":"Automatically Adding Expense Receipts to Google Sheets with Telegram, Mindee, Twilio, and n8n"},{"url":"https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/","icon":"🎫","label":"Supercharging your conference registration process with n8n"},{"url":"https://n8n.io/blog/creating-triggers-for-n8n-workflows-using-polling/","icon":"⏲","label":"Creating triggers for n8n workflows using polling"},{"url":"https://n8n.io/blog/no-code-ecommerce-workflow-automations/","icon":"store","label":"6 e-commerce workflows to power up your Shopify s"},{"url":"https://n8n.io/blog/migrating-community-metrics-to-orbit-using-n8n/","icon":"📈","label":"Migrating Community Metrics to Orbit using n8n"},{"url":"https://n8n.io/blog/automate-google-apps-for-productivity/","icon":"💡","label":"15 Google apps you can combine and automate to increase productivity"},{"url":"https://n8n.io/blog/your-business-doesnt-need-you-to-operate/","icon":" 🖥️","label":"Hey founders! Your business doesn't need you to operate"},{"url":"https://n8n.io/blog/how-honest-burgers-use-automation-to-save-100k-per-year/","icon":"🍔","label":"How Honest Burgers Use Automation to Save $100k per year"},{"url":"https://n8n.io/blog/how-a-digital-strategist-uses-n8n-for-online-marketing/","icon":"💻","label":"How a digital strategist uses n8n for online marketing"},{"url":"https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/","icon":"🧠","label":"Why this Product Manager loves workflow automation with n8n"},{"url":"https://n8n.io/blog/sending-automated-congratulations-with-google-sheets-twilio-and-n8n/","icon":"🙌","label":"Sending Automated Congratulations with Google Sheets, Twilio, and n8n "},{"url":"https://n8n.io/blog/how-a-membership-development-manager-automates-his-work-and-investments/","icon":"📈","label":"How a Membership Development Manager automates his work and investments"},{"url":"https://n8n.io/blog/aws-workflow-automation/","label":"7 no-code workflow automations for Amazon Web Services"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googlesheets/"}],"credentialDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/credentials/google/oauth-single-service/"}]},"categories":["Data & Storage","Productivity"],"nodeVersion":"1.0","codexVersion":"1.0"}},"group":"[\"input\",\"output\"]","defaults":{"name":"Google Sheets"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI2MCIgaGVpZ2h0PSI2MCI+PGcgZmlsbD0ibm9uZSIgZmlsbC1ydWxlPSJldmVub2RkIiBzdHJva2UtbGluZWNhcD0icm91bmQiIHN0cm9rZS1saW5lam9pbj0icm91bmQiPjxwYXRoIGZpbGw9IiMyOEI0NDYiIGQ9Ik0zNS42OSAxIDUyIDE3LjIyNXYzOS4wODdhMy42NyAzLjY3IDAgMCAxLTEuMDg0IDIuNjFBMy43IDMuNyAwIDAgMSA0OC4yOTMgNjBIMTIuNzA3YTMuNyAzLjcgMCAwIDEtMi42MjMtMS4wNzhBMy42NyAzLjY3IDAgMCAxIDkgNTYuMzEyVjQuNjg4YTMuNjcgMy42NyAwIDAgMSAxLjA4NC0yLjYxQTMuNyAzLjcgMCAwIDEgMTIuNzA3IDF6Ii8+PHBhdGggZmlsbD0iIzZBQ0U3QyIgZD0iTTM1LjY5IDEgNTIgMTcuMjI1SDM5LjM5N2MtMi4wNTQgMC0zLjcwNy0xLjgyOS0zLjcwNy0zLjg3MnoiLz48cGF0aCBmaWxsPSIjMjE5QjM4IiBkPSJNMzkuMjExIDE3LjIyNSA1MiAyMi40OHYtNS4yNTV6Ii8+PHBhdGggZmlsbD0iI0ZGRiIgZD0iTTIwLjEyIDMxLjk3NWMwLS44MTcuNjYyLTEuNDc1IDEuNDgzLTEuNDc1aDE3Ljc5NGMuODIxIDAgMS40ODIuNjU4IDEuNDgyIDEuNDc1djE1LjQ4N2MwIC44MTgtLjY2MSAxLjQ3NS0xLjQ4MiAxLjQ3NUgyMS42MDNhMS40NzYgMS40NzYgMCAwIDEtMS40ODItMS40NzRWMzEuOTc0em0yLjIyNSAxLjQ3NWg2LjY3MnYyLjIxMmgtNi42NzJ6bTAgNS4xNjJoNi42NzJ2Mi4yMTNoLTYuNjcyem0wIDUuMTYzaDYuNjcydjIuMjEyaC02LjY3MnptOS42MzgtMTAuMzI1aDYuNjcydjIuMjEyaC02LjY3MnptMCA1LjE2Mmg2LjY3MnYyLjIxM2gtNi42NzJ6bTAgNS4xNjNoNi42NzJ2Mi4yMTJoLTYuNjcyeiIvPjxwYXRoIGZpbGw9IiMyOEI0NDYiIGQ9Ik0zNC42OSAwIDUxIDE2LjIyNXYzOS4wODdhMy42NyAzLjY3IDAgMCAxLTEuMDg0IDIuNjFBMy43IDMuNyAwIDAgMSA0Ny4yOTMgNTlIMTEuNzA3YTMuNyAzLjcgMCAwIDEtMi42MjMtMS4wNzhBMy42NyAzLjY3IDAgMCAxIDggNTUuMzEyVjMuNjg4YTMuNjcgMy42NyAwIDAgMSAxLjA4NC0yLjYxQTMuNyAzLjcgMCAwIDEgMTEuNzA3IDB6Ii8+PHBhdGggZmlsbD0iIzZBQ0U3QyIgZD0iTTM0LjY5IDAgNTEgMTYuMjI1SDM4LjM5N2MtMi4wNTQgMC0zLjcwNy0xLjgyOS0zLjcwNy0zLjg3MnoiLz48cGF0aCBmaWxsPSIjMjE5QjM4IiBkPSJNMzguMjExIDE2LjIyNSA1MSAyMS40OHYtNS4yNTV6Ii8+PHBhdGggZmlsbD0iI0ZGRiIgZD0iTTE5LjEyIDMwLjk3NWMwLS44MTcuNjYyLTEuNDc1IDEuNDgzLTEuNDc1aDE3Ljc5NGMuODIxIDAgMS40ODIuNjU4IDEuNDgyIDEuNDc1djE1LjQ4N2MwIC44MTgtLjY2MSAxLjQ3NS0xLjQ4MiAxLjQ3NUgyMC42MDNhMS40NzYgMS40NzYgMCAwIDEtMS40ODItMS40NzRWMzAuOTc0em0yLjIyNSAxLjQ3NWg2LjY3MnYyLjIxMmgtNi42NzJ6bTAgNS4xNjJoNi42NzJ2Mi4yMTNoLTYuNjcyem0wIDUuMTYzaDYuNjcydjIuMjEyaC02LjY3MnptOS42MzgtMTAuMzI1aDYuNjcydjIuMjEyaC02LjY3MnptMCA1LjE2Mmg2LjY3MnYyLjIxM2gtNi42NzJ6bTAgNS4xNjNoNi42NzJ2Mi4yMTJoLTYuNjcyeiIvPjwvZz48L3N2Zz4="},"displayName":"Google Sheets","typeVersion":5,"nodeCategories":[{"id":3,"name":"Data & Storage"},{"id":4,"name":"Productivity"}]},{"id":47,"icon":"file:webhook.svg","name":"n8n-nodes-base.webhook","codex":{"data":{"alias":["HTTP","API","Build","WH"],"resources":{"generic":[{"url":"https://n8n.io/blog/learn-how-to-automatically-cross-post-your-content-with-n8n/","icon":"✍️","label":"Learn how to automatically cross-post your content with n8n"},{"url":"https://n8n.io/blog/running-n8n-on-ships-an-interview-with-maranics/","icon":"🛳","label":"Running n8n on ships: An interview with Maranics"},{"url":"https://n8n.io/blog/how-to-build-a-low-code-self-hosted-url-shortener/","icon":"🔗","label":"How to build a low-code, self-hosted URL shortener in 3 steps"},{"url":"https://n8n.io/blog/what-are-apis-how-to-use-them-with-no-code/","icon":" 🪢","label":"What are APIs and how to use them with no code"},{"url":"https://n8n.io/blog/5-tasks-you-can-automate-with-notion-api/","icon":"⚡️","label":"5 tasks you can automate with the new Notion API "},{"url":"https://n8n.io/blog/how-a-digital-strategist-uses-n8n-for-online-marketing/","icon":"💻","label":"How a digital strategist uses n8n for online marketing"},{"url":"https://n8n.io/blog/the-ultimate-guide-to-automate-your-video-collaboration-with-whereby-mattermost-and-n8n/","icon":"📹","label":"The ultimate guide to automate your video collaboration with Whereby, Mattermost, and n8n"},{"url":"https://n8n.io/blog/how-to-automatically-give-kudos-to-contributors-with-github-slack-and-n8n/","icon":"👏","label":"How to automatically give kudos to contributors with GitHub, Slack, and n8n"},{"url":"https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/","icon":"🤖","label":"5 workflow automations for Mattermost that we love at n8n"},{"url":"https://n8n.io/blog/why-this-product-manager-loves-workflow-automation-with-n8n/","icon":"🧠","label":"Why this Product Manager loves workflow automation with n8n"},{"url":"https://n8n.io/blog/creating-custom-incident-response-workflows-with-n8n/","label":"How to automate every step of an incident response workflow"},{"url":"https://n8n.io/blog/learn-to-build-powerful-api-endpoints-using-webhooks/","icon":"🧰","label":"Learn to Build Powerful API Endpoints Using Webhooks"},{"url":"https://n8n.io/blog/learn-how-to-use-webhooks-with-mattermost-slash-commands/","icon":"🦄","label":"Learn how to use webhooks with Mattermost slash commands"},{"url":"https://n8n.io/blog/how-goomer-automated-their-operations-with-over-200-n8n-workflows/","icon":"🛵","label":"How Goomer automated their operations with over 200 n8n workflows"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.webhook/"}]},"categories":["Development","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers"]}}},"group":"[\"trigger\"]","defaults":{"name":"Webhook"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI0OCIgaGVpZ2h0PSI0OCI+PHBhdGggZmlsbD0iIzM3NDc0ZiIgZD0iTTM1IDM3Yy0yLjIgMC00LTEuOC00LTRzMS44LTQgNC00IDQgMS44IDQgNC0xLjggNC00IDQiLz48cGF0aCBmaWxsPSIjMzc0NzRmIiBkPSJNMzUgNDNjLTMgMC01LjktMS40LTcuOC0zLjdsMy4xLTIuNWMxLjEgMS40IDIuOSAyLjMgNC43IDIuMyAzLjMgMCA2LTIuNyA2LTZzLTIuNy02LTYtNmMtMSAwLTIgLjMtMi45LjdsLTEuNyAxTDIzLjMgMTZsMy41LTEuOSA1LjMgOS40YzEtLjMgMi0uNSAzLS41IDUuNSAwIDEwIDQuNSAxMCAxMFM0MC41IDQzIDM1IDQzIi8+PHBhdGggZmlsbD0iIzM3NDc0ZiIgZD0iTTE0IDQzQzguNSA0MyA0IDM4LjUgNCAzM2MwLTQuNiAzLjEtOC41IDcuNS05LjdsMSAzLjlDOS45IDI3LjkgOCAzMC4zIDggMzNjMCAzLjMgMi43IDYgNiA2czYtMi43IDYtNnYtMmgxNXY0SDIzLjhjLS45IDQuNi01IDgtOS44IDgiLz48cGF0aCBmaWxsPSIjZTkxZTYzIiBkPSJNMTQgMzdjLTIuMiAwLTQtMS44LTQtNHMxLjgtNCA0LTQgNCAxLjggNCA0LTEuOCA0LTQgNCIvPjxwYXRoIGZpbGw9IiMzNzQ3NGYiIGQ9Ik0yNSAxOWMtMi4yIDAtNC0xLjgtNC00czEuOC00IDQtNCA0IDEuOCA0IDQtMS44IDQtNCA0Ii8+PHBhdGggZmlsbD0iI2U5MWU2MyIgZD0ibTE1LjcgMzQtMy40LTIgNS45LTkuN2MtMi0xLjktMy4yLTQuNS0zLjItNy4zIDAtNS41IDQuNS0xMCAxMC0xMHMxMCA0LjUgMTAgMTBjMCAuOS0uMSAxLjctLjMgMi41bC0zLjktMWMuMS0uNS4yLTEgLjItMS41IDAtMy4zLTIuNy02LTYtNnMtNiAyLjctNiA2YzAgMi4xIDEuMSA0IDIuOSA1LjFsMS43IDF6Ii8+PC9zdmc+"},"displayName":"Webhook","typeVersion":2,"nodeCategories":[{"id":5,"name":"Development"},{"id":9,"name":"Core Nodes"}]},{"id":112,"icon":"fa:map-signs","name":"n8n-nodes-base.switch","codex":{"data":{"alias":["Router","If","Path","Filter","Condition","Logic","Branch","Case"],"resources":{"generic":[{"url":"https://n8n.io/blog/2021-the-year-to-automate-the-new-you-with-n8n/","icon":"☀️","label":"2021: The Year to Automate the New You with n8n"},{"url":"https://n8n.io/blog/how-to-get-started-with-crm-automation-and-no-code-workflow-ideas/","icon":"👥","label":"How to get started with CRM automation (with 3 no-code workflow ideas"},{"url":"https://n8n.io/blog/build-your-own-virtual-assistant-with-n8n-a-step-by-step-guide/","icon":"👦","label":"Build your own virtual assistant with n8n: A step by step guide"},{"url":"https://n8n.io/blog/automation-for-maintainers-of-open-source-projects/","icon":"🏷️","label":"How to automatically manage contributions to open-source projects"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.switch/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Flow"]}}},"group":"[\"transform\"]","defaults":{"name":"Switch","color":"#506000"},"iconData":{"icon":"map-signs","type":"icon"},"displayName":"Switch","typeVersion":3,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":317,"icon":"file:googleCalendar.svg","name":"n8n-nodes-base.googleCalendar","codex":{"data":{"resources":{"generic":[{"url":"https://n8n.io/blog/how-to-host-virtual-coffee-breaks-with-n8n/","icon":"☕️","label":"How to host virtual coffee breaks with n8n"},{"url":"https://n8n.io/blog/supercharging-your-conference-registration-process-with-n8n/","icon":"🎫","label":"Supercharging your conference registration process with n8n"},{"url":"https://n8n.io/blog/automate-google-apps-for-productivity/","icon":"💡","label":"15 Google apps you can combine and automate to increase productivity"},{"url":"https://n8n.io/blog/your-business-doesnt-need-you-to-operate/","icon":" 🖥️","label":"Hey founders! Your business doesn't need you to operate"},{"url":"https://n8n.io/blog/5-workflow-automations-for-mattermost-that-we-love-at-n8n/","icon":"🤖","label":"5 workflow automation for Mattermost that we love at n8n"},{"url":"https://n8n.io/blog/tracking-time-spent-in-meetings-with-google-calendar-twilio-and-n8n/","icon":"🗓","label":"Tracking Time Spent in Meetings With Google Calendar, Twilio, and n8n"}],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/app-nodes/n8n-nodes-base.googlecalendar/"}],"credentialDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/credentials/google/oauth-single-service/"}]},"categories":["Productivity"],"nodeVersion":"1.0","codexVersion":"1.0"}},"group":"[\"input\"]","defaults":{"name":"Google Calendar"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHhtbG5zOnhsaW5rPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5L3hsaW5rIiBmaWxsPSIjZmZmIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIHN0cm9rZT0iIzAwMCIgc3Ryb2tlLWxpbmVjYXA9InJvdW5kIiBzdHJva2UtbGluZWpvaW49InJvdW5kIiB2aWV3Qm94PSIwIDAgODEgODIiPjx1c2UgeGxpbms6aHJlZj0iI2EiIHg9Ii41IiB5PSIuNSIvPjxzeW1ib2wgaWQ9ImEiIG92ZXJmbG93PSJ2aXNpYmxlIj48ZyBmaWxsLXJ1bGU9Im5vbnplcm8iIHN0cm9rZT0ibm9uZSI+PHBhdGggZD0iTTYxLjA1MiAxOC45NDdIMTguOTQ3djQyLjEwNWg0Mi4xMDV6Ii8+PHBhdGggZmlsbD0iI2VhNDMzNSIgZD0iTTYxLjA1MyA4MCA4MCA2MS4wNTNINjEuMDUzeiIvPjxwYXRoIGZpbGw9IiNmYmJjMDQiIGQ9Ik04MCAxOC45NDdINjEuMDUzdjQyLjEwNUg4MHoiLz48cGF0aCBmaWxsPSIjMzRhODUzIiBkPSJNNjEuMDUyIDYxLjA1M0gxOC45NDdWODBoNDIuMTA1eiIvPjxwYXRoIGZpbGw9IiMxODgwMzgiIGQ9Ik0wIDYxLjA1M3YxMi42MzJBNi4zMTQgNi4zMTQgMCAwIDAgNi4zMTYgODBoMTIuNjMyVjYxLjA1M3oiLz48cGF0aCBmaWxsPSIjMTk2N2QyIiBkPSJNODAgMTguOTQ3VjYuMzE2QTYuMzE0IDYuMzE0IDAgMCAwIDczLjY4NSAwSDYxLjA1M3YxOC45NDd6Ii8+PHBhdGggZmlsbD0iIzQyODVmNCIgZD0iTTYxLjA1MyAwSDYuMzE2QTYuMzE0IDYuMzE0IDAgMCAwIDAgNi4zMTZ2NTQuNzM3aDE4Ljk0N1YxOC45NDdoNDIuMTA1VjB6TTI3LjU4NCA1MS42MTFjLTEuNTc0LTEuMDYzLTIuNjYzLTIuNjE2LTMuMjU4LTQuNjY4bDMuNjUzLTEuNTA1cS40OTggMS44OTQgMS43MzcgMi45MzdjMS4yMzkgMS4wNDMgMS44MjEgMS4wMzcgMi45ODkgMS4wMzdxMS43OTIgMCAzLjA3OS0xLjA4OWMxLjI4Ny0xLjA4OSAxLjI5LTEuNjUzIDEuMjktMi43NzRhMy40NCAzLjQ0IDAgMCAwLTEuMzU4LTIuODExYy0uOTA1LS43MjctMi4wNDItMS4wODktMy40LTEuMDg5aC0yLjExMXYtMy42MTZIMzIuMXExLjc1MiAwIDIuOTUzLS45NDdjMS4yMDEtLjk0NyAxLjItMS40OTUgMS4yLTIuNTk1cTAtMS40NjctMS4wNzQtMi4zNDJjLTEuMDc0LS44NzUtMS42MjEtLjg3OS0yLjcyMS0uODc5cS0xLjYxLS4wMDItMi41NTguODU4Yy0uOTQ4Ljg2LTEuMTA2IDEuMzAxLTEuMzc5IDIuMTExbC0zLjYxNi0xLjUwNWMuNDc5LTEuMzU4IDEuMzU4LTIuNTU4IDIuNjQ3LTMuNTk1czIuOTM3LTEuNTU4IDQuOTM3LTEuNTU4cTIuMjItLjAwMiAzLjk4OS44NThjMS43NjkuODYgMi4xMDUgMS4zNjggMi43NzQgMi4zNzlzMSAyLjE1MyAxIDMuNDE2cTAgMS45MzItLjkzMiAzLjI3NGMtLjkzMiAxLjM0Mi0xLjM4NCAxLjU3OS0yLjI4OSAyLjA1OHYuMjE2YTYuOTUgNi45NSAwIDAgMSAyLjkzNyAyLjI4OXExLjE0NiAxLjUzOCAxLjE0NyAzLjY4NGMuMDAxIDIuMTQ2LS4zNjMgMi43MTEtMS4wODkgMy44MzJzLTEuNzMyIDIuMDA1LTMuMDA1IDIuNjQ3Yy0xLjI3OS42NDItMi43MTYuOTY4LTQuMzExLjk2OC0xLjg0Ny4wMDUtMy41NTMtLjUyNi01LjEyNi0xLjU4OXptMjIuNDM3LTE4LjEyNi00LjAxIDIuOS0yLjAwNS0zLjA0MiA3LjE5NS01LjE4OWgyLjc1OHYyNC40NzloLTMuOTM3VjMzLjQ4NHoiLz48L2c+PC9zeW1ib2w+PC9zdmc+"},"displayName":"Google Calendar","typeVersion":1,"nodeCategories":[{"id":4,"name":"Productivity"}]},{"id":565,"icon":"fa:sticky-note","name":"n8n-nodes-base.stickyNote","codex":{"data":{"alias":["Comments","Notes","Sticky"],"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers"]}}},"group":"[\"input\"]","defaults":{"name":"Sticky Note","color":"#FFD233"},"iconData":{"icon":"sticky-note","type":"icon"},"displayName":"Sticky Note","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":826,"icon":"file:n8n.svg","name":"n8n-nodes-base.n8n","codex":{"data":{"alias":["Workflow","Execution"],"resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.n8n/"}],"credentialDocumentation":[{"url":"https://docs.n8n.io/api/authentication/"}]},"categories":["Development","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers","Other Trigger Nodes"]}}},"group":"[\"transform\"]","defaults":{"name":"n8n"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCAyMzAgMTIwIj48cGF0aCBmaWxsPSIjRUE0QjcxIiBmaWxsLXJ1bGU9ImV2ZW5vZGQiIGQ9Ik0yMDQgNDhjLTExLjE4MyAwLTIwLjU4LTcuNjQ5LTIzLjI0NC0xOGgtMjcuNTA4YTEyIDEyIDAgMCAwLTExLjgzNiAxMC4wMjdsLS45ODcgNS45MTlBMjMuOTQgMjMuOTQgMCAwIDEgMTMyLjYyNiA2MGEyMy45NCAyMy45NCAwIDAgMSA3Ljc5OSAxNC4wNTRsLjk4NyA1LjkxOUExMiAxMiAwIDAgMCAxNTMuMjQ4IDkwaDMuNTA4QzE1OS40MiA3OS42NDkgMTY4LjgxNyA3MiAxODAgNzJjMTMuMjU1IDAgMjQgMTAuNzQ1IDI0IDI0cy0xMC43NDUgMjQtMjQgMjRjLTExLjE4MyAwLTIwLjU4LTcuNjQ5LTIzLjI0NC0xOGgtMy41MDhjLTExLjczMiAwLTIxLjc0NC04LjQ4Mi0yMy42NzMtMjAuMDU0bC0uOTg3LTUuOTE5QTEyIDEyIDAgMCAwIDExNi43NTIgNjZoLTkuNTA4QzEwNC41OCA3Ni4zNTEgOTUuMTgzIDg0IDg0IDg0cy0yMC41OC03LjY0OS0yMy4yNDQtMThINDcuMjQ0QzQ0LjU4IDc2LjM1MSAzNS4xODMgODQgMjQgODQgMTAuNzQ1IDg0IDAgNzMuMjU1IDAgNjBzMTAuNzQ1LTI0IDI0LTI0YzExLjE4MyAwIDIwLjU4IDcuNjQ5IDIzLjI0NCAxOGgxMy41MTJDNjMuNDIgNDMuNjQ5IDcyLjgxNyAzNiA4NCAzNnMyMC41OCA3LjY0OSAyMy4yNDQgMThoOS41MDhhMTIgMTIgMCAwIDAgMTEuODM2LTEwLjAyN2wuOTg3LTUuOTE5QzEzMS41MDQgMjYuNDgyIDE0MS41MTYgMTggMTUzLjI0OCAxOGgyNy41MDhDMTgzLjQyIDcuNjQ5IDE5Mi44MTcgMCAyMDQgMGMxMy4yNTUgMCAyNCAxMC43NDUgMjQgMjRzLTEwLjc0NSAyNC0yNCAyNG0wLTEyYzYuNjI3IDAgMTItNS4zNzMgMTItMTJzLTUuMzczLTEyLTEyLTEyLTEyIDUuMzczLTEyIDEyIDUuMzczIDEyIDEyIDEyTTI0IDcyYzYuNjI3IDAgMTItNS4zNzMgMTItMTJzLTUuMzczLTEyLTEyLTEyLTEyIDUuMzczLTEyIDEyIDUuMzczIDEyIDEyIDEybTcyLTEyYzAgNi42MjctNS4zNzMgMTItMTIgMTJzLTEyLTUuMzczLTEyLTEyIDUuMzczLTEyIDEyLTEyIDEyIDUuMzczIDEyIDEybTk2IDM2YzAgNi42MjctNS4zNzMgMTItMTIgMTJzLTEyLTUuMzczLTEyLTEyIDUuMzczLTEyIDEyLTEyIDEyIDUuMzczIDEyIDEyIiBjbGlwLXJ1bGU9ImV2ZW5vZGQiLz48L3N2Zz4="},"displayName":"n8n","typeVersion":1,"nodeCategories":[{"id":5,"name":"Development"},{"id":9,"name":"Core Nodes"}]},{"id":834,"icon":"file:code.svg","name":"n8n-nodes-base.code","codex":{"data":{"alias":["cpde","Javascript","JS","Python","Script","Custom Code","Function"],"details":"The Code node allows you to execute JavaScript in your workflow.","resources":{"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.code/"}]},"categories":["Development","Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Helpers","Data Transformation"]}}},"group":"[\"transform\"]","defaults":{"name":"Code"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNTEyIiBoZWlnaHQ9IjUxMiIgdmlld0JveD0iMCAwIDUxMiA1MTIiIGZpbGw9Im5vbmUiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyI+CjxnIGNsaXAtcGF0aD0idXJsKCNjbGlwMF8xMTcxXzQ0MSkiPgo8cGF0aCBkPSJNMTcwLjI4MyA0OEgxOTYuNUMyMDMuMTI3IDQ4IDIwOC41IDQyLjYyNzQgMjA4LjUgMzZWMTJDMjA4LjUgNS4zNzI1OCAyMDMuMTI3IDAgMTk2LjUgMEgxNzAuMjgzQzEyNi4xIDAgOTAuMjgzIDM1LjgxNzIgOTAuMjgzIDgwVjE3NkM5MC4yODMgMjA2LjkyOCA2NS4yMTA5IDIzMiAzNC4yODMgMjMySDIzQzE2LjM3MjYgMjMyIDExIDIzNy4zNzIgMTEgMjQ0VjI2OEMxMSAyNzQuNjI3IDE2LjM3MjQgMjgwIDIyLjk5OTYgMjgwTDM0LjI4MyAyODBDNjUuMjEwOSAyODAgOTAuMjgzIDMwNS4wNzIgOTAuMjgzIDMzNlY0NDBDOTAuMjgzIDQ3OS43NjQgMTIyLjUxOCA1MTIgMTYyLjI4MyA1MTJIMTk2LjVDMjAzLjEyNyA1MTIgMjA4LjUgNTA2LjYyNyAyMDguNSA1MDBWNDc2QzIwOC41IDQ2OS4zNzMgMjAzLjEyNyA0NjQgMTk2LjUgNDY0SDE2Mi4yODNDMTQ5LjAyOCA0NjQgMTM4LjI4MyA0NTMuMjU1IDEzOC4yODMgNDQwVjMzNkMxMzguMjgzIDMwOS4wMjIgMTI4LjAxMSAyODQuNDQzIDExMS4xNjQgMjY1Ljk2MUMxMDYuMTA5IDI2MC40MTYgMTA2LjEwOSAyNTEuNTg0IDExMS4xNjQgMjQ2LjAzOUMxMjguMDExIDIyNy41NTcgMTM4LjI4MyAyMDIuOTc4IDEzOC4yODMgMTc2VjgwQzEzOC4yODMgNjIuMzI2OSAxNTIuNjEgNDggMTcwLjI4MyA0OFoiIGZpbGw9IiNGRjk5MjIiLz4KPHBhdGggZD0iTTMwNSAzNkMzMDUgNDIuNjI3NCAzMTAuMzczIDQ4IDMxNyA0OEgzNDIuOTc5QzM2MC42NTIgNDggMzc0Ljk3OCA2Mi4zMjY5IDM3NC45NzggODBWMTc2QzM3NC45NzggMjAyLjk3OCAzODUuMjUxIDIyNy41NTcgNDAyLjA5OCAyNDYuMDM5QzQwNy4xNTMgMjUxLjU4NCA0MDcuMTUzIDI2MC40MTYgNDAyLjA5OCAyNjUuOTYxQzM4NS4yNTEgMjg0LjQ0MyAzNzQuOTc4IDMwOS4wMjIgMzc0Ljk3OCAzMzZWNDMyQzM3NC45NzggNDQ5LjY3MyAzNjAuNjUyIDQ2NCAzNDIuOTc5IDQ2NEgzMTdDMzEwLjM3MyA0NjQgMzA1IDQ2OS4zNzMgMzA1IDQ3NlY1MDBDMzA1IDUwNi42MjcgMzEwLjM3MyA1MTIgMzE3IDUxMkgzNDIuOTc5QzM4Ny4xNjEgNTEyIDQyMi45NzggNDc2LjE4MyA0MjIuOTc4IDQzMlYzMzZDNDIyLjk3OCAzMDUuMDcyIDQ0OC4wNTEgMjgwIDQ3OC45NzkgMjgwSDQ5MEM0OTYuNjI3IDI4MCA1MDIgMjc0LjYyOCA1MDIgMjY4VjI0NEM1MDIgMjM3LjM3MyA0OTYuNjI4IDIzMiA0OTAgMjMyTDQ3OC45NzkgMjMyQzQ0OC4wNTEgMjMyIDQyMi45NzggMjA2LjkyOCA0MjIuOTc4IDE3NlY4MEM0MjIuOTc4IDM1LjgxNzIgMzg3LjE2MSAwIDM0Mi45NzkgMEgzMTdDMzEwLjM3MyAwIDMwNSA1LjM3MjU4IDMwNSAxMlYzNloiIGZpbGw9IiNGRjk5MjIiLz4KPC9nPgo8ZGVmcz4KPGNsaXBQYXRoIGlkPSJjbGlwMF8xMTcxXzQ0MSI+CjxyZWN0IHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJ3aGl0ZSIvPgo8L2NsaXBQYXRoPgo8L2RlZnM+Cjwvc3ZnPgo="},"displayName":"Code","typeVersion":2,"nodeCategories":[{"id":5,"name":"Development"},{"id":9,"name":"Core Nodes"}]},{"id":839,"icon":"fa:clock","name":"n8n-nodes-base.scheduleTrigger","codex":{"data":{"alias":["Time","Scheduler","Polling","Cron","Interval"],"resources":{"generic":[],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.scheduletrigger/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0"}},"group":"[\"trigger\",\"schedule\"]","defaults":{"name":"Schedule Trigger","color":"#31C49F"},"iconData":{"icon":"clock","type":"icon"},"displayName":"Schedule Trigger","typeVersion":1,"nodeCategories":[{"id":9,"name":"Core Nodes"}]},{"id":1238,"icon":"file:removeDuplicates.svg","name":"n8n-nodes-base.removeDuplicates","codex":{"data":{"alias":["Dedupe","Deduplicate","Duplicates","Remove","Unique","Transform","Array","List","Item"],"details":"","resources":{"generic":[],"primaryDocumentation":[{"url":"https://docs.n8n.io/integrations/builtin/core-nodes/n8n-nodes-base.removeduplicates/"}]},"categories":["Core Nodes"],"nodeVersion":"1.0","codexVersion":"1.0","subcategories":{"Core Nodes":["Data Transformation"]}}},"group":"[\"transform\"]","defaults":{"name":"Remove Duplicates"},"iconData":{"type":"file","fileBuffer":"data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI1MTIiIGhlaWdodD0iNTEyIiBmaWxsPSJub25lIj48ZyBmaWxsPSIjNTRCOEM5IiBjbGlwLXBhdGg9InVybCgjYSkiPjxwYXRoIGQ9Ik0xMzQuMDk3IDExMWgzOC44Mjl2MzIuNTA4SDEzOC4xNnYzNC42MzVoLTMyLjUwOHYtMzguNjk5YzAtMTUuNzA5IDEyLjczNS0yOC40NDQgMjguNDQ1LTI4LjQ0NG03Ny42NTggMzIuNTA4VjExMWg3Ny42NTd2MzIuNTA4em0xMTYuNDg2IDBWMTExaDc3LjY1OHYzMi41MDh6bTExNi40ODcgMFYxMTFoMzguODI5YzE1LjcxIDAgMjguNDQ1IDEyLjczNSAyOC40NDUgMjguNDQ0djM4LjY5OWgtMzIuNTA4di0zNC42MzV6bTM0Ljc2NiA3My4yMzhoMzIuNTA4djM4LjY5OGMwIDE1LjcxLTEyLjczNSAyOC40NDUtMjguNDQ1IDI4LjQ0NWgtMzguODI5di0zMi41MDhoMzQuNzY2ek0wIDI0NC41MzdDMCAyMjkuMzI5IDEyLjczNSAyMTcgMjguNDQ0IDIxN2gzNDkuNDYxYzE1LjcwOSAwIDI4LjQ0NCAxMi4zMjkgMjguNDQ0IDI3LjUzN3YxMjkuODE1YzAgMTUuMjA4LTEyLjczNSAyNy41MzctMjguNDQ0IDI3LjUzN0gyOC40NDVDMTIuNzM0IDQwMS44ODkgMCAzODkuNTYgMCAzNzQuMzUyeiIvPjwvZz48ZGVmcz48Y2xpcFBhdGggaWQ9ImEiPjxwYXRoIGZpbGw9IiNmZmYiIGQ9Ik0wIDBoNTEydjUxMkgweiIvPjwvY2xpcFBhdGg+PC9kZWZzPjwvc3ZnPg=="},"displayName":"Remove Duplicates","typeVersion":2,"nodeCategories":[{"id":9,"name":"Core Nodes"}]}],"categories":[{"id":16,"name":"DevOps"}],"image":[]}}