This workflow corresponds to n8n.io template #8305 — we link there as the canonical source.
This workflow follows the Google Calendar → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"id": "atCSLFdL7AyLdjJD",
"name": "Everhour Time Off",
"tags": [],
"nodes": [
{
"id": "fe08e8fe-779b-4428-8060-08da88a61118",
"name": "Schedule Trigger",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-1856,
384
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "8082b4f8-5ff0-4af3-ac3f-268cdd8bee97",
"name": "Build Assignment Groups",
"type": "n8n-nodes-base.code",
"position": [
-656,
576
],
"parameters": {
"jsCode": "// Input: per-day items from Edit Fields (each has externalKey, startDate, endDateExclusive)\n// Output: one item per assignmentId with the set of current keys and a search window\n\nconst groups = {}; // { [assignmentId]: { currentKeys:Set, minDate, maxDate } }\n\nfor (const it of items) {\n const j = it.json;\n const parts = (j.externalKey || '').split(':'); // [\"everhour\", \"<id>\", \"<date>\"]\n const assignmentId = parts[1];\n if (!assignmentId) continue;\n\n if (!groups[assignmentId]) {\n groups[assignmentId] = {\n assignmentId,\n currentKeys: new Set(),\n minDate: j.startDate,\n maxDate: j.endDateExclusive\n };\n }\n groups[assignmentId].currentKeys.add(j.externalKey);\n if (j.startDate < groups[assignmentId].minDate) groups[assignmentId].minDate = j.startDate;\n if (j.endDateExclusive > groups[assignmentId].maxDate) groups[assignmentId].maxDate = j.endDateExclusive;\n}\n\n// Emit one item per assignment, with a small buffer on the search window (optional)\nconst out = [];\nfor (const id of Object.keys(groups)) {\n const g = groups[id];\n out.push({\n json: {\n assignmentId: id,\n currentKeys: Array.from(g.currentKeys),\n searchTimeMin: g.minDate + 'T00:00:00',\n searchTimeMax: g.maxDate + 'T23:59:59'\n }\n });\n}\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "f210fb86-0d78-4140-adff-0573566b23ac",
"name": "Build Global Key Set",
"type": "n8n-nodes-base.code",
"position": [
-656,
768
],
"parameters": {
"jsCode": "// Build the set of keys we SHOULD have right now\nlet minDate = null, maxDate = null;\nconst keys = new Set();\n\nfor (const it of items) {\n const j = it.json;\n if (j.externalKey) keys.add(j.externalKey);\n if (j.startDate && (!minDate || j.startDate < minDate)) minDate = j.startDate;\n if (j.endDateExclusive && (!maxDate || j.endDateExclusive > maxDate)) maxDate = j.endDateExclusive;\n}\n\n// Reasonable window (optional). Broaden if you want.\nif (!minDate) minDate = new Date().toISOString().slice(0,10);\nif (!maxDate) {\n const d = new Date(); d.setFullYear(d.getFullYear() + 1);\n maxDate = d.toISOString().slice(0,10);\n}\n\nreturn [{\n json: {\n currentKeys: Array.from(keys),\n searchTimeMin: minDate + 'T00:00:00',\n searchTimeMax: maxDate + 'T23:59:59'\n }\n}];\n"
},
"typeVersion": 2
},
{
"id": "c76c87dc-876c-46f0-ac7e-a4a15ac3661c",
"name": "Everhour - Fetch Time Off",
"type": "n8n-nodes-base.httpRequest",
"notes": "Uses Header Auth credential (add `X-Api-Key`). No hardcoded keys.",
"position": [
-1568,
384
],
"parameters": {
"url": "https://api.everhour.com/resource-planner/assignments",
"options": {},
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth"
},
"notesInFlow": true,
"typeVersion": 4.2
},
{
"id": "92a16e7c-bb00-439f-b819-6b7557ff5017",
"name": "Filter Approved Time-Off",
"type": "n8n-nodes-base.if",
"position": [
-1344,
384
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "9e28789f-61a6-4fcb-b132-294825f6ea39",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.status }}",
"rightValue": "approved"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "c20edb41-1400-4f8d-a90e-c61bda0295c7",
"name": "Build Calendar Payload",
"type": "n8n-nodes-base.set",
"position": [
-896,
384
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "a7cb20ad-8a14-4252-9bb2-499db02852cd",
"name": "=searchQuery",
"type": "string",
"value": "={{ $json.externalKey }}"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "5fe3aab5-3136-4619-8857-96e53578adbd",
"name": "Prepare Time-Off Items",
"type": "n8n-nodes-base.code",
"position": [
-1120,
384
],
"parameters": {
"jsCode": "// All time-off (including half-day) \u2192 keep creating as ALL-DAY events,\n// but also expose `isAllDay` based on Everhour's original period.\n\nfunction* eachDateInclusive(startStr, endStr, includeWeekends) {\n const start = new Date(startStr + 'T00:00:00');\n const end = new Date(endStr + 'T00:00:00');\n for (let d = new Date(start); d <= end; d.setDate(d.getDate() + 1)) {\n const dow = d.getDay(); // 0=Sun .. 6=Sat\n if (!includeWeekends && (dow === 0 || dow === 6)) continue;\n yield new Date(d);\n }\n}\nfunction fmtDate(d){ return d.toISOString().slice(0,10); }\n\nconst out = [];\nfor (const item of items) {\n const r = item.json;\n if ((r.status || '').toLowerCase() !== 'approved') continue;\n if (r.type !== 'time-off') continue;\n\n const userName = r.user?.name || 'Unknown';\n const userEmail = r.user?.email || '';\n const startDate = r.startDate;\n const endDate = r.endDate || r.startDate;\n const includeWeekends = !!r.includeWeekends;\n const period = r.timeOffPeriod || 'full-day'; // 'full-day' | 'half-of-day'\n const type = r.timeOffType?.name || 'Time Off';\n const note = r.note || '';\n const id = r.id;\n\n // Derive the original \"isAllDay\" from Everhour's period (without changing mapping)\n const isAllDayOriginal = (period !== 'half-of-day');\n\n // Expand per-day; skip weekends if flag=false\n for (const d of eachDateInclusive(startDate, endDate, includeWeekends)) {\n const day = fmtDate(d);\n\n // All-day mapping (end date exclusive)\n const endObj = new Date(d);\n endObj.setDate(endObj.getDate() + 1);\n const endExclusive = fmtDate(endObj);\n\n out.push({\n json: {\n // You are still creating ALL-DAY events\n isAllDay: isAllDayOriginal, // <-- added (reflects original Everhour period)\n startDate: day,\n endDateExclusive: endExclusive,\n\n employeeName: userName,\n employeeEmail: userEmail,\n type,\n note,\n status: r.status,\n\n // unique key per assignment-day\n externalKey: `everhour:${id}:${day}`,\n\n // keep for debugging/visibility\n originalPeriod: period\n }\n });\n }\n}\nreturn out;\n"
},
"typeVersion": 2
},
{
"id": "11e26f03-b016-445a-99b1-72b9e5b8c0c3",
"name": "Find Existing Event by External Key",
"type": "n8n-nodes-base.httpRequest",
"position": [
-672,
96
],
"parameters": {
"url": "={{ 'https://www.googleapis.com/calendar/v3/calendars/' + $items(\"Config\")[0].json.calendarId + '/events' }}",
"options": {},
"sendQuery": true,
"authentication": "predefinedCredentialType",
"queryParameters": {
"parameters": [
{
"name": "singleEvents",
"value": "true"
},
{
"name": "maxResults",
"value": "1"
},
{
"name": "=q",
"value": "={{ $json.externalKey }}"
}
]
},
"nodeCredentialType": "googleApi"
},
"typeVersion": 4.2
},
{
"id": "929dc366-f019-435e-88ed-9d1807ac70da",
"name": "Attach Found Event ID",
"type": "n8n-nodes-base.code",
"position": [
-448,
96
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// This Function is in \"Run Once for Each Item\" mode.\n// $json here is the HTTP search response for THIS PTO item.\n\nconst ev = Array.isArray($json.items) ? $json.items[0] : null;\n\n// IMPORTANT: use the current index, not 0\nconst i = $itemIndex;\nconst src = $items(\"Build Calendar Payload\")[i].json;\n\nreturn {\n json: {\n ...src, // keep your PTO fields for THIS item\n existingEventId: ev ? ev.id : null, // id if found\n found: !!ev // boolean for IF node\n }\n};\n"
},
"typeVersion": 2
},
{
"id": "b05c97db-3810-4847-8a24-b81a824894ab",
"name": "Check if Event Exists",
"type": "n8n-nodes-base.if",
"position": [
-224,
96
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "59967d64-2b86-4c86-a653-45adec8fba72",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.found }}",
"rightValue": "true"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "1c3a725d-775e-452b-9567-b00f2a3181b2",
"name": "Update All-Day Event",
"type": "n8n-nodes-base.googleCalendar",
"notes": "Calendar ID pulled from the Config node: `$items(\"Config\")[0].json.calendarId`.",
"position": [
0,
0
],
"parameters": {
"eventId": "={{ $json.existingEventId }}",
"calendar": {
"__rl": true,
"mode": "list",
"value": "={{ $items(\"Config\")[0].json.calendarId }}"
},
"operation": "update",
"updateFields": {
"end": "={{ $json.endDateExclusive }}",
"start": "={{ $json.startDate }}",
"allday": "yes",
"summary": "={{ $json.employeeName }} - {{ $json.type }}",
"description": "=Everhour PTO\nKey: {{$json.externalKey}}\nType: {{$json.type}}\nStatus: {{$json.status}}\nNote: {{$json.note}}\n"
}
},
"notesInFlow": true,
"typeVersion": 1.3
},
{
"id": "ba91bdf7-b56a-4f61-adc3-ef1a016ebde6",
"name": "Create All-Day Event",
"type": "n8n-nodes-base.googleCalendar",
"notes": "Calendar ID pulled from the Config node: `$items(\"Config\")[0].json.calendarId`.",
"position": [
0,
192
],
"parameters": {
"end": "={{ $json.endDateExclusive }}",
"start": "={{ $json.startDate }}",
"calendar": {
"__rl": true,
"mode": "list",
"value": "={{ $items(\"Config\")[0].json.calendarId }}"
},
"additionalFields": {
"allday": "yes",
"summary": "={{ $json.employeeName }} - {{ $json.type }}",
"description": "=Everhour PTO\nKey: {{$json.externalKey}}\nType: {{$json.type}}\nStatus: {{$json.status}}\nNote: {{$json.note}}\n"
}
},
"notesInFlow": true,
"typeVersion": 1.3
},
{
"id": "2d4aeb2e-742d-499c-9e94-8a8be2c5c9d6",
"name": "List Assignment Events",
"type": "n8n-nodes-base.httpRequest",
"notes": "Calendar ID pulled from the Config node: `$items(\"Config\")[0].json.calendarId`.",
"position": [
-432,
576
],
"parameters": {
"url": "={{ 'https://www.googleapis.com/calendar/v3/calendars/' + $items(\"Config\")[0].json.calendarId + '/events' }}",
"options": {},
"sendQuery": true,
"authentication": "predefinedCredentialType",
"queryParameters": {
"parameters": [
{
"name": "singleEvents",
"value": "true"
},
{
"name": "maxResults",
"value": "2500"
},
{
"name": "orderBy",
"value": "startTime"
},
{
"name": "q",
"value": "={{ 'everhour:' + $json.assignmentId + ':' }}"
}
]
},
"nodeCredentialType": "googleApi"
},
"notesInFlow": true,
"typeVersion": 4.2
},
{
"id": "cca57225-aef9-4426-9d21-816e8a882ab9",
"name": "Find Obsolete Events (per assignment)",
"type": "n8n-nodes-base.code",
"position": [
-208,
576
],
"parameters": {
"jsCode": "// Run Once for ALL Items\n// Pair each HTTP Request2 result (events for an assignment) with the matching group from Code2\n\nconst groups = $items(\"Build Assignment Groups\").map(it => it.json); // <-- node name must match exactly\nconst out = [];\n\nfor (let i = 0; i < items.length; i++) {\n const group = groups[i];\n if (!group) continue;\n\n const current = new Set(group.currentKeys);\n const apiItems = Array.isArray(items[i].json.items) ? items[i].json.items : [];\n\n for (const ev of apiItems) {\n const desc = ev.description || \"\";\n const m = desc.match(/Key:\\s*(everhour:[^\\s]+)/i); // \"Key: everhour:<id>:<date>\"\n if (!m) continue;\n const key = m[1];\n\n if (!current.has(key)) {\n out.push({\n json: { eventId: ev.id, key, assignmentId: group.assignmentId }\n });\n }\n }\n}\n\nreturn out; // one item per stale event to delete\n"
},
"typeVersion": 2
},
{
"id": "acf5aa87-0784-4633-a127-0cae6a069bf8",
"name": "Delete Calendar Event",
"type": "n8n-nodes-base.googleCalendar",
"notes": "Calendar ID pulled from the Config node: `$items(\"Config\")[0].json.calendarId`.",
"position": [
16,
672
],
"parameters": {
"eventId": "={{ $json.eventId }}",
"options": {
"sendUpdates": "none"
},
"calendar": {
"__rl": true,
"mode": "list",
"value": "={{ $items(\"Config\")[0].json.calendarId }}"
},
"operation": "delete"
},
"notesInFlow": true,
"retryOnFail": true,
"typeVersion": 1.3
},
{
"id": "6c4c4f1e-7772-41f7-9ef9-22936167b523",
"name": "List All Everhour Events",
"type": "n8n-nodes-base.httpRequest",
"notes": "Calendar ID pulled from the Config node: `$items(\"Config\")[0].json.calendarId`.",
"position": [
-432,
768
],
"parameters": {
"url": "={{ 'https://www.googleapis.com/calendar/v3/calendars/' + $items(\"Config\")[0].json.calendarId + '/events' }}",
"options": {},
"sendQuery": true,
"authentication": "predefinedCredentialType",
"queryParameters": {
"parameters": [
{
"name": "singleEvents",
"value": "true"
},
{
"name": "maxResults",
"value": "2500"
},
{
"name": "orderBy",
"value": "startTime"
},
{
"name": "q",
"value": "everhour:"
}
]
},
"nodeCredentialType": "googleApi"
},
"notesInFlow": true,
"typeVersion": 4.2
},
{
"id": "6651bc72-0b3d-4fe3-99cd-e1ff5a092da0",
"name": "Find Obsolete Events (global)",
"type": "n8n-nodes-base.code",
"position": [
-208,
768
],
"parameters": {
"jsCode": "// Run Once for ALL Items\n// Compare calendar events (items[0].json.items) vs currentKeys from \"Build global key set\"\n\nconst current = new Set($items(\"Build Global Key Set\")[0].json.currentKeys);\nconst evs = Array.isArray(items[0].json.items) ? items[0].json.items : [];\nconst out = [];\n\nfor (const ev of evs) {\n const desc = ev.description || \"\";\n const m = desc.match(/Key:\\s*(everhour:[^\\s]+)/i); // extracts \"everhour:<id>:<date>\"\n if (!m) continue;\n const key = m[1];\n if (!current.has(key)) {\n out.push({ json: { eventId: ev.id, key }});\n }\n}\nreturn out; // one item per stale event to delete\n"
},
"typeVersion": 2
},
{
"id": "ac73d08e-3e0b-416f-8ec9-89b11e4c3a25",
"name": "Config",
"type": "n8n-nodes-base.set",
"notes": "Set your Google Calendar ID here. Example: team-calendar@group.calendar.google.com",
"position": [
-1856,
160
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "0b59d6f6-4e28-4f6d-a4ad-0cb253b9a65f",
"name": "calendarId",
"type": "string",
"value": "user@example.com"
}
]
},
"includeOtherFields": true
},
"notesInFlow": true,
"typeVersion": 3.4,
"alwaysOutputData": true
},
{
"id": "9bde197a-7009-41d3-ac7e-6b86ccdd2863",
"name": "\ud83d\uddd2\ufe0f Template Description (Read Me)",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2960,
160
],
"parameters": {
"color": null,
"width": 1024,
"height": 736,
"content": "## Everhour time-off sync to Google Calendar\n\n### Who\u2019s it for\nTeams using **Everhour** for time tracking and **Google Calendar** for visibility.\n\n### What it does\n- Fetches **approved time-off** from Everhour\n- Creates/updates **all-day events** per day of absence\n- Deletes **stale events** if requests change or are canceled\n\n### How it works\n1. **Schedule Trigger** runs on an interval.\n2. Fetch Everhour assignments via API (Header Auth \u2014 set your Everhour API key in n8n credentials).\n3. Filter to **approved** time-off.\n4. Create/update events keyed by `everhour:<assignmentId>:<YYYY-MM-DD>`.\n5. Remove obsolete calendar events by comparing current keys with what\u2019s on the calendar.\n\n### Setup\n- Create Everhour **Header Auth** credential: `X-Api-Key: <YOUR_EVERHOUR_API_KEY>`.\n- Add your **Google Calendar** OAuth credential to Calendar nodes.\n- In the **Config** node, set `calendarId` (e.g., `team@group.calendar.google.com`).\n\n### Customize\n- Change schedule cadence.\n- Filter by users/time-off types.\n- Adjust event title/description template.\n"
},
"typeVersion": 1
},
{
"id": "3dbeed2f-b8db-493e-ac9f-32b393008668",
"name": "Note: Fetch Everhour",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1632,
240
],
"parameters": {
"color": 7,
"height": 336,
"content": "## **Fetch Everhour**\n\nFetch approved time-off from Everhour using Header Auth (no keys in node)."
},
"typeVersion": 1
},
{
"id": "a595b36f-b23f-4356-85ce-2da5a0758d03",
"name": "Note: Build Keys",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1168,
256
],
"parameters": {
"color": 7,
"width": 416,
"height": 336,
"content": "## **Build Keys**\n\nGenerate per-day `externalKey` like `everhour:<id>:<date>`."
},
"typeVersion": 1
},
{
"id": "b028f58c-bb85-4894-a274-a099a4ea92ec",
"name": "Note: Upsert Events",
"type": "n8n-nodes-base.stickyNote",
"position": [
-720,
-112
],
"parameters": {
"color": 7,
"width": 976,
"height": 512,
"content": "## **Upsert Events**\n\nSearch by key \u2192 update if found, otherwise create all-day event."
},
"typeVersion": 1
},
{
"id": "921f878d-1014-4018-995c-bac11224961a",
"name": "Note: Cleanup",
"type": "n8n-nodes-base.stickyNote",
"position": [
-704,
432
],
"parameters": {
"color": 7,
"width": 960,
"height": 528,
"content": "## **Cleanup**\n\nList events and delete those not present in current keys."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"executionOrder": "v1"
},
"versionId": "13a31a87-6b2e-4e1d-9f14-0f8a350b0d78",
"connections": {
"Schedule Trigger": {
"main": [
[
{
"node": "Everhour - Fetch Time Off",
"type": "main",
"index": 0
},
{
"node": "Config",
"type": "main",
"index": 0
}
]
]
},
"Build Global Key Set": {
"main": [
[
{
"node": "List All Everhour Events",
"type": "main",
"index": 0
}
]
]
},
"Attach Found Event ID": {
"main": [
[
{
"node": "Check if Event Exists",
"type": "main",
"index": 0
}
]
]
},
"Check if Event Exists": {
"main": [
[
{
"node": "Update All-Day Event",
"type": "main",
"index": 0
}
],
[
{
"node": "Create All-Day Event",
"type": "main",
"index": 0
}
]
]
},
"Build Calendar Payload": {
"main": [
[
{
"node": "Find Existing Event by External Key",
"type": "main",
"index": 0
},
{
"node": "Build Assignment Groups",
"type": "main",
"index": 0
},
{
"node": "Build Global Key Set",
"type": "main",
"index": 0
}
]
]
},
"List Assignment Events": {
"main": [
[
{
"node": "Find Obsolete Events (per assignment)",
"type": "main",
"index": 0
}
]
]
},
"Prepare Time-Off Items": {
"main": [
[
{
"node": "Build Calendar Payload",
"type": "main",
"index": 0
}
]
]
},
"Build Assignment Groups": {
"main": [
[
{
"node": "List Assignment Events",
"type": "main",
"index": 0
}
]
]
},
"Filter Approved Time-Off": {
"main": [
[
{
"node": "Prepare Time-Off Items",
"type": "main",
"index": 0
}
]
]
},
"List All Everhour Events": {
"main": [
[
{
"node": "Find Obsolete Events (global)",
"type": "main",
"index": 0
}
]
]
},
"Everhour - Fetch Time Off": {
"main": [
[
{
"node": "Filter Approved Time-Off",
"type": "main",
"index": 0
}
]
]
},
"Find Obsolete Events (global)": {
"main": [
[
{
"node": "Delete Calendar Event",
"type": "main",
"index": 0
}
]
]
},
"Find Existing Event by External Key": {
"main": [
[
{
"node": "Attach Found Event ID",
"type": "main",
"index": 0
}
]
]
},
"Find Obsolete Events (per assignment)": {
"main": [
[
{
"node": "Delete Calendar Event",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Teams that track absences in Everhour and want a shared Google Calendar view for quick planning. Ideal for managers, HR/OPS, and teammates who need instant visibility into approved time off. Pulls approved time-off from Everhour on a schedule Creates/updates all-day events per…
Source: https://n8n.io/workflows/8305/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
[TEMPLATE] Full Class -> Calendar Sync. Uses httpRequest, googleCalendar. Scheduled trigger; 24 nodes.
🕌 How it works
This workflow automates the process of finding local events and adding them directly to your Google Calendar. It eliminates the need for manual event tracking by automatically scraping event informati
Import Forex Factory Calendar events into Google Calendar. Delete past Forex Factory Calendar events from Google Calendar. Get reminders for important economic data releases — especially High Impact n
Simple Calendar Sync. Uses googleCalendar, httpRequest. Scheduled trigger; 4 nodes.