8
n8n 中文网amn8n.com

Everhour休假时间同步

高级

这是一个HR领域的自动化工作流,包含 23 个节点。主要使用 If, Set, Code, HttpRequest, GoogleCalendar 等节点。 将Everhour休假时间自动同步到Google日历全天事件

前置要求
  • 可能需要目标 API 的认证凭证
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
  "id": "atCSLFdL7AyLdjJD",
  "meta": {
    "instanceId": "ed54b493f1b983b9b64b327a27338be1695af08f90b0f5fff3b6d18c570c4fc2"
  },
  "name": "Everhour 休假时间",
  "tags": [],
  "nodes": [
    {
      "id": "fe08e8fe-779b-4428-8060-08da88a61118",
      "name": "计划触发器",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1856,
        384
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "8082b4f8-5ff0-4af3-ac3f-268cdd8bee97",
      "name": "构建分配组",
      "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": "构建全局键集",
      "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 - 获取休假时间",
      "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": "筛选已批准的休假",
      "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": "构建日历载荷",
      "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": "准备休假项目",
      "type": "n8n-nodes-base.code",
      "position": [
        -1120,
        384
      ],
      "parameters": {
        "jsCode": "// All time-off (including half-day) → 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": "通过外部键查找现有事件",
      "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": "附加找到的事件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": "检查事件是否存在",
      "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": "更新全天事件",
      "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": "创建全天事件",
      "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": "列出分配事件",
      "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": "查找过时事件(按分配)",
      "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": "删除日历事件",
      "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": "列出所有 Everhour 事件",
      "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": "查找过时事件(全局)",
      "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": "配置",
      "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": "REPLACE_WITH_YOUR_CALENDAR_ID@group.calendar.google.com"
            }
          ]
        },
        "includeOtherFields": true
      },
      "notesInFlow": true,
      "typeVersion": 3.4,
      "alwaysOutputData": true
    },
    {
      "id": "9bde197a-7009-41d3-ac7e-6b86ccdd2863",
      "name": "🗒️ 模板描述(自述文件)",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -2960,
        160
      ],
      "parameters": {
        "color": null,
        "width": 1024,
        "height": 736,
        "content": "## Everhour 休假同步到 Google Calendar"
      },
      "typeVersion": 1
    },
    {
      "id": "3dbeed2f-b8db-493e-ac9f-32b393008668",
      "name": "注意:获取 Everhour",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1632,
        240
      ],
      "parameters": {
        "color": 7,
        "height": 336,
        "content": "## **获取 Everhour**"
      },
      "typeVersion": 1
    },
    {
      "id": "a595b36f-b23f-4356-85ce-2da5a0758d03",
      "name": "注意:构建键",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1168,
        256
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 336,
        "content": "## **构建键**"
      },
      "typeVersion": 1
    },
    {
      "id": "b028f58c-bb85-4894-a274-a099a4ea92ec",
      "name": "注意:更新或插入事件",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -720,
        -112
      ],
      "parameters": {
        "color": 7,
        "width": 976,
        "height": 512,
        "content": "## **更新或插入事件**"
      },
      "typeVersion": 1
    },
    {
      "id": "921f878d-1014-4018-995c-bac11224961a",
      "name": "注意:清理",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -704,
        432
      ],
      "parameters": {
        "color": 7,
        "width": 960,
        "height": 528,
        "content": "## **清理**"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "pinData": {},
  "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
          }
        ]
      ]
    }
  }
}
常见问题

如何使用这个工作流?

复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。

这个工作流适合什么场景?

高级 - 人力资源

需要付费吗?

本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。

工作流信息
难度等级
高级
节点数量23
分类1
节点类型7
难度说明

适合高级用户,包含 16+ 个节点的复杂工作流

作者
Hossein Karami

Hossein Karami

@hosseinkarami

Senior Web Developer & AI Enthusiast

外部链接
在 n8n.io 查看

分享此工作流