全球节假日冲突检测与会议重新安排
高级
这是一个Personal Productivity领域的自动化工作流,包含 23 个节点。主要使用 If, Set, Code, Slack, HttpRequest 等节点。 使用Google Calendar和Slack检测节假日冲突并建议会议重新安排
前置要求
- •Slack Bot Token 或 Webhook URL
- •可能需要目标 API 的认证凭证
分类
工作流预览
可视化展示节点连接关系,支持缩放和平移
导出工作流
复制以下 JSON 配置到 n8n 导入,即可使用此工作流
{
"id": "K67zdPMDUtledQFV",
"meta": {
"instanceId": "189dde98270e9ce0f006f0e9deb96aa5e627396fc6279cac8902c9b06936984d"
},
"name": "全球节假日冲突检测与会议重新安排",
"tags": [],
"nodes": [
{
"id": "b487208f-33b3-4527-8e18-bb3c2a8746d7",
"name": "每日检查",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-752,
224
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 9
}
]
}
},
"typeVersion": 1.2
},
{
"id": "a8f9264a-5dba-4801-9c1e-e12a59237b8b",
"name": "工作流配置",
"type": "n8n-nodes-base.set",
"position": [
-528,
224
],
"parameters": {
"options": {},
"assignments": {
"assignments": [
{
"id": "id-1",
"name": "currentYear",
"type": "number",
"value": "={{ new Date().getFullYear() }}"
},
{
"id": "id-2",
"name": "nextWeekStart",
"type": "string",
"value": "={{ new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] }}"
},
{
"id": "id-3",
"name": "nextWeekEnd",
"type": "string",
"value": "={{ new Date(Date.now() + 14 * 24 * 60 * 60 * 1000).toISOString().split('T')[0] }}"
},
{
"id": "id-4",
"name": "slackChannel",
"type": "string",
"value": "C09FB9QQQTX"
},
{
"id": "id-5",
"name": "calendarId",
"type": "string",
"value": "c_91f92ee12632d48cc78642add679d75aa8aecb09abea89eadbc97cb17d2de336@group.calendar.google.com"
},
{
"id": "b5660f95-e978-4a1a-9c33-225dfa4e9552",
"name": "countryCodes",
"type": "array",
"value": "[\"US\", \"GB\", \"DE\", \"IN\", \"CN\", \"KR\", \"HK\"]"
}
]
},
"includeOtherFields": true
},
"typeVersion": 3.4
},
{
"id": "6472f288-c7e2-49da-bd32-61b91d9528b3",
"name": "合并并过滤下周节假日",
"type": "n8n-nodes-base.code",
"position": [
144,
224
],
"parameters": {
"jsCode": "// Get the next week date range from Workflow Configuration\nconst nextWeekStart = new Date($('Workflow Configuration').first().json.nextWeekStart);\nconst nextWeekEnd = new Date($('Workflow Configuration').first().json.nextWeekEnd);\n\n// Combine all holiday data from the loop\nconst allHolidays = [];\n\n// Process all input items (which are the results from each loop iteration)\nfor (const item of $input.all()) {\n // The HTTP Request node in the loop outputs the holiday array in item.json\n if (Array.isArray(item.json)) {\n for (const holiday of item.json) {\n allHolidays.push(holiday);\n }\n }\n}\n\n// Filter holidays that fall within next week range\nconst nextWeekHolidays = allHolidays.filter(holiday => {\n const holidayDate = new Date(holiday.date);\n return holidayDate >= nextWeekStart && holidayDate <= nextWeekEnd;\n});\n\n// Map to desired output format\nconst formattedHolidays = nextWeekHolidays.map(holiday => ({\n date: holiday.date,\n name: holiday.name || holiday.localName,\n country: holiday.countryCode === 'US' ? 'United States' : \n holiday.countryCode === 'GB' ? 'United Kingdom' : \n holiday.countryCode === 'DE' ? 'Germany' : \n holiday.countryCode === 'IN' ? 'India' : \n holiday.countryCode === 'CN' ? 'China' : \n holiday.countryCode === 'KR' ? 'South Korea' : \n holiday.countryCode === 'HK' ? 'Hong Kong' : holiday.countryCode,\n countryCode: holiday.countryCode\n}));\n\n// Return all filtered holidays as a single item\nreturn [{\n json: {\n holidays: formattedHolidays\n }\n}];"
},
"typeVersion": 2
},
{
"id": "524535d0-6d38-4da8-a5c9-e775f4ff4e50",
"name": "获取下周日历事件",
"type": "n8n-nodes-base.googleCalendar",
"position": [
-304,
272
],
"parameters": {
"options": {},
"timeMax": "={{ $('Workflow Configuration').first().json.nextWeekEnd }}T23:59:59Z",
"timeMin": "={{ $('Workflow Configuration').first().json.nextWeekStart }}T00:00:00Z",
"calendar": {
"__rl": true,
"mode": "id",
"value": "={{ $('Workflow Configuration').first().json.calendarId }}"
},
"operation": "getAll",
"returnAll": true
},
"typeVersion": 1.3
},
{
"id": "5ed5e1c2-726a-43bd-8f49-ed40f3eac9fa",
"name": "检测节假日冲突",
"type": "n8n-nodes-base.code",
"position": [
480,
224
],
"parameters": {
"jsCode": "// Get the single item containing the list of all holidays from the first input\nconst holidayList = $input.all(0)[0].json.holidays || [];\n\n// Get all items containing calendar events from the second input\nconst calendarEventItems = $input.all(1);\n\n// Create a map of holiday dates for quick lookup\nconst holidayMap = new Map();\nholidayList.forEach(holiday => {\n const date = holiday.date;\n if (!holidayMap.has(date)) {\n holidayMap.set(date, []);\n }\n holidayMap.get(date).push({\n name: holiday.name,\n country: holiday.countryCode || holiday.country\n });\n});\n\n// Detect conflicts by iterating through each calendar event item\nconst conflicts = [];\ncalendarEventItems.forEach(eventItem => {\n const event = eventItem.json;\n const eventStart = event.start?.dateTime || event.start?.date;\n if (!eventStart) return; // Skip if no start time\n \n const eventDate = eventStart.split('T')[0];\n \n // Check if this date has any holidays\n if (holidayMap.has(eventDate)) {\n const holidaysOnDate = holidayMap.get(eventDate);\n const eventTime = eventStart.includes('T') ? eventStart.split('T')[1].substring(0, 5) : 'All day';\n const attendees = event.attendees ? event.attendees.map(a => a.email) : [];\n \n conflicts.push({\n eventName: event.summary || 'Untitled Event',\n eventDate: eventDate,\n eventTime: eventTime,\n holidayName: holidaysOnDate.map(h => h.name).join(', '),\n affectedCountries: [...new Set(holidaysOnDate.map(h => h.country))].join(', '),\n attendees: attendees,\n eventId: event.id\n });\n }\n});\n\n// Return all found conflicts in a SINGLE item to ensure subsequent nodes run only once\nreturn [{\n json: {\n conflicts: conflicts,\n totalConflicts: conflicts.length,\n checkDate: new Date().toISOString()\n }\n}];"
},
"typeVersion": 2
},
{
"id": "32c956cf-781f-4548-96a9-9d758a7f22a3",
"name": "检查是否发现冲突",
"type": "n8n-nodes-base.if",
"position": [
752,
224
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "id-1",
"operator": {
"type": "array",
"operation": "notEmpty"
},
"leftValue": "={{ $('Detect Holiday Conflicts').item.json.conflicts }}"
}
]
}
},
"typeVersion": 2.2
},
{
"id": "fd84ad56-55c4-49c4-9e60-368977208c0c",
"name": "生成重新安排建议",
"type": "n8n-nodes-base.code",
"position": [
1072,
224
],
"parameters": {
"jsCode": "const conflicts = $input.first().json.conflicts || [];\n\n// Get the holiday list from the 'Merge and Filter' node which ran before the conflict detection\nconst holidays = $('Merge and Filter Next Week Holidays').first().json.holidays.map(h => h.date);\n\nfunction isHoliday(dateStr) {\n return holidays.includes(dateStr);\n}\n\nfunction isWeekend(date) {\n const day = date.getDay();\n return day === 0 || day === 6; // Sunday or Saturday\n}\n\nfunction findNextAvailableDate(startDate) {\n let currentDate = new Date(startDate);\n currentDate.setDate(currentDate.getDate() + 1);\n \n for (let i = 0; i < 30; i++) {\n const dateStr = currentDate.toISOString().split('T')[0];\n if (!isWeekend(currentDate) && !isHoliday(dateStr)) {\n const originalTime = startDate.toTimeString().split(' ')[0].substring(0, 5);\n return { date: dateStr, time: originalTime };\n }\n currentDate.setDate(currentDate.getDate() + 1);\n }\n \n return {\n date: currentDate.toISOString().split('T')[0],\n time: startDate.toTimeString().split(' ')[0].substring(0, 5)\n };\n}\n\nconst enhancedConflicts = conflicts.map(conflict => {\n const eventDateTime = conflict.eventTime === 'All day' ? new Date(conflict.eventDate) : new Date(`${conflict.eventDate}T${conflict.eventTime}`);\n const suggestion = findNextAvailableDate(eventDateTime);\n \n return {\n ...conflict,\n suggestedDate: suggestion.date,\n suggestedTime: suggestion.time,\n originalDate: conflict.eventDate,\n originalTime: conflict.eventTime\n };\n});\n\nreturn [{\n json: {\n conflicts: enhancedConflicts,\n totalConflicts: enhancedConflicts.length\n }\n}];"
},
"typeVersion": 2
},
{
"id": "4bb37c98-7400-4e4c-9519-4cafa5555067",
"name": "格式化 Slack 摘要",
"type": "n8n-nodes-base.code",
"position": [
1376,
224
],
"parameters": {
"jsCode": "// Get the conflicts data from the previous node\nconst conflictsData = $input.first().json;\nconst conflicts = conflictsData.conflicts || [];\n\n// Use a single backslash \\n for newlines\nlet slackMessage = ':warning: *Holiday Conflict Alert* :warning:\\n\\n';\n\nif (conflicts.length === 0) {\n slackMessage += 'No conflicts detected for next week. All clear! :white_check_mark:';\n} else {\n slackMessage += `Found *${conflicts.length}* meeting(s) scheduled during public holidays next week:\\n\\n`;\n \n conflicts.forEach((conflict, index) => {\n slackMessage += `*${index + 1}. ${conflict.eventName}*\\n`;\n slackMessage += `:calendar: *Date:* ${conflict.originalDate}\\n`;\n slackMessage += `:clock3: *Time:* ${conflict.originalTime}\\n`;\n slackMessage += `:earth_americas: *Affected Countries:* ${conflict.affectedCountries}\\n`;\n slackMessage += `:pushpin: *Holiday:* ${conflict.holidayName}\\n`;\n \n // Add suggestion if it exists\n if (conflict.suggestedDate) {\n slackMessage += `:bulb: *Suggestion:* Reschedule to ${conflict.suggestedDate} at ${conflict.suggestedTime}\\n`;\n }\n \n slackMessage += '\\n---\\n\\n';\n });\n \n slackMessage += ':point_right: Please review and reschedule these meetings to accommodate team members in affected regions.';\n}\n\n// Return the formatted message\nreturn [{\n json: {\n slackMessage: slackMessage\n }\n}];"
},
"typeVersion": 2
},
{
"id": "d01a7818-dc28-4490-ad5b-e5963cd22cbd",
"name": "发布 Slack 摘要",
"type": "n8n-nodes-base.slack",
"position": [
1680,
224
],
"webhookId": "5337846c-6b31-45a2-80ac-6960f6350ab1",
"parameters": {
"text": "={{ $json.slackMessage }}",
"select": "channel",
"channelId": {
"__rl": true,
"mode": "id",
"value": "={{ $('Workflow Configuration').first().json.slackChannel }}"
},
"otherOptions": {},
"authentication": "oAuth2"
},
"typeVersion": 2.3
},
{
"id": "d04fe2ce-a6f3-49d5-a384-39a3a201fa37",
"name": "遍历项目",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-304,
80
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "74320878-4302-440c-99ce-5aefefcb1ca2",
"name": "获取公共节假日",
"type": "n8n-nodes-base.httpRequest",
"position": [
-80,
32
],
"parameters": {
"url": "=https://date.nager.at/api/v3/PublicHolidays/{{ $('Workflow Configuration').first().json.currentYear }}/{{ $json.countryCodes }}",
"options": {}
},
"typeVersion": 4.2
},
{
"id": "392bf796-1880-4a89-ad6b-7838a7a19351",
"name": "注意:每日检查",
"type": "n8n-nodes-base.stickyNote",
"position": [
-816,
64
],
"parameters": {
"color": "white",
"height": 144,
"content": "**目的:** 在每个工作日的早晨触发一次工作流。"
},
"typeVersion": 1
},
{
"id": "0541544f-2e8e-41a7-b899-76de44829cca",
"name": "注意:工作流配置",
"type": "n8n-nodes-base.stickyNote",
"position": [
-544,
-208
],
"parameters": {
"color": "white",
"width": 256,
"height": 208,
"content": "**目的:** 定义变量的中心位置。"
},
"typeVersion": 1
},
{
"id": "02931af5-a167-422c-ae32-53412ed0f4f4",
"name": "注意:合并并过滤下周节假日",
"type": "n8n-nodes-base.stickyNote",
"position": [
80,
48
],
"parameters": {
"color": "white",
"content": "**目的:** 合并 API 结果并仅过滤到下周。"
},
"typeVersion": 1
},
{
"id": "79ac359f-12a3-43ba-bf75-b987f37f482e",
"name": "注意:获取下周日历事件",
"type": "n8n-nodes-base.stickyNote",
"position": [
-368,
480
],
"parameters": {
"color": "white",
"content": "**目的:** 从 Google Calendar 读取下周窗口内的所有事件。"
},
"typeVersion": 1
},
{
"id": "165237d2-da5e-408f-bb46-77be9bf4c38c",
"name": "注意:检测节假日冲突",
"type": "n8n-nodes-base.stickyNote",
"position": [
384,
48
],
"parameters": {
"color": "white",
"content": "**目的:** 比较事件日期与节假日日期以查找冲突。"
},
"typeVersion": 1
},
{
"id": "3b55ac06-aa21-44de-a6e0-37051e02cfa1",
"name": "注意:检查是否发现冲突",
"type": "n8n-nodes-base.stickyNote",
"position": [
688,
48
],
"parameters": {
"color": "white",
"content": "**目的:** 保护分支;仅当存在冲突时继续。"
},
"typeVersion": 1
},
{
"id": "fc01dc46-773b-4881-b646-2bcf21c7bc1f",
"name": "注意:生成重新安排建议",
"type": "n8n-nodes-base.stickyNote",
"position": [
992,
48
],
"parameters": {
"color": "white",
"content": "**目的:** 建议下一个不是节假日/周末的工作日。"
},
"typeVersion": 1
},
{
"id": "cec6277f-583d-4ba9-8063-2e956395bff5",
"name": "注意:格式化 Slack 摘要",
"type": "n8n-nodes-base.stickyNote",
"position": [
1296,
48
],
"parameters": {
"color": "white",
"content": "**目的:** 创建可读的 Slack 消息。"
},
"typeVersion": 1
},
{
"id": "6d766a0d-bea6-4bbb-a824-127b53b9dc45",
"name": "注意:发布 Slack 摘要",
"type": "n8n-nodes-base.stickyNote",
"position": [
1600,
48
],
"parameters": {
"color": "white",
"content": "**目的:** 将摘要发布到 Slack。"
},
"typeVersion": 1
},
{
"id": "8461d7cc-e39d-4c86-9c6f-96a7aa55ba4f",
"name": "注意:遍历项目",
"type": "n8n-nodes-base.stickyNote",
"position": [
-208,
-160
],
"parameters": {
"color": "white",
"content": "**目的:** 遍历每个国家代码。"
},
"typeVersion": 1
},
{
"id": "f857f854-7ed9-43c1-af05-7e596d8369dc",
"name": "注意:获取公共节假日",
"type": "n8n-nodes-base.stickyNote",
"position": [
96,
-192
],
"parameters": {
"color": "white",
"width": 288,
"height": 192,
"content": "**目的:** 调用 Nager.Date API 获取公共节假日。"
},
"typeVersion": 1
},
{
"id": "48d8def6-1e2e-4e03-b963-a44f9e2c0de1",
"name": "模板概览",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1632,
-448
],
"parameters": {
"color": "yellow",
"width": 688,
"height": 752,
"content": "## 此模板的功能"
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "c01575e0-ea13-45a5-bb0a-c771b4107645",
"connections": {
"Daily Check": {
"main": [
[
{
"node": "Workflow Configuration",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Items": {
"main": [
[
{
"node": "Merge and Filter Next Week Holidays",
"type": "main",
"index": 0
}
],
[
{
"node": "Fetch Public Holidays",
"type": "main",
"index": 0
}
]
]
},
"Format Slack Digest": {
"main": [
[
{
"node": "Post Slack Digest",
"type": "main",
"index": 0
}
]
]
},
"Fetch Public Holidays": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Workflow Configuration": {
"main": [
[
{
"node": "Loop Over Items",
"type": "main",
"index": 0
},
{
"node": "Get Next Week Calendar Events",
"type": "main",
"index": 0
}
]
]
},
"Check If Conflicts Found": {
"main": [
[
{
"node": "Generate Reschedule Suggestions",
"type": "main",
"index": 0
}
]
]
},
"Detect Holiday Conflicts": {
"main": [
[
{
"node": "Check If Conflicts Found",
"type": "main",
"index": 0
}
]
]
},
"Generate Reschedule Suggestions": {
"main": [
[
{
"node": "Format Slack Digest",
"type": "main",
"index": 0
}
]
]
},
"Merge and Filter Next Week Holidays": {
"main": [
[
{
"node": "Detect Holiday Conflicts",
"type": "main",
"index": 0
}
]
]
}
}
}常见问题
如何使用这个工作流?
复制上方的 JSON 配置代码,在您的 n8n 实例中创建新工作流并选择「从 JSON 导入」,粘贴配置后根据需要修改凭证设置即可。
这个工作流适合什么场景?
高级 - 个人效率
需要付费吗?
本工作流完全免费,您可以直接导入使用。但请注意,工作流中使用的第三方服务(如 OpenAI API)可能需要您自行付费。
相关工作流推荐
使用AI求职信自动搜索职位
使用 Google Jobs、RemoteOK 和 GPT-3.5 自动化带AI求职信的职位搜索
If
Set
Code
+7
17 节点Shelly-Ann Davy
个人效率
LinkedIn职位搜索
LinkedIn职位搜索:自动匹配简历(GPT/Gemini)+求职信生成器+Telegram提醒
If
Set
Code
+13
33 节点Hojjat Jashnniloofar
个人效率
每日WhatsApp摘要与群组级别控制
WhatsApp群组摘要工作流
If
Set
Code
+13
39 节点Luís Philipe Trindade
个人效率
基于AI的会议研究与每日议程(Google日历、Attio CRM和Slack)
基于AI的会议研究与每日议程:使用Google日历、Attio CRM和Slack
If
Set
Code
+15
30 节点Harry Siggins
AI 摘要总结
自动化会议准备
使用 GPT-5 和 Gemini 研究从日历到 Slack 通过 Attio CRM 自动准备会议
If
Set
Code
+16
39 节点Harry Siggins
AI 摘要总结
竞争价格监控与警报(Bright Data、Sheets 和 Slack)
使用 Bright Data、Sheets 和 Slack 进行竞争价格监控与警报
If
Set
Code
+9
29 节点Daniel Shashko
市场调研