Detección de conflictos de días festivos globales y reprogramación de reuniones
Este es unPersonal Productivityflujo de automatización del dominio deautomatización que contiene 23 nodos.Utiliza principalmente nodos como If, Set, Code, Slack, HttpRequest. Detectar conflictos de festivos usando Google Calendar y Slack, y sugerir reprogramación de reuniones
- •Bot Token de Slack o URL de Webhook
- •Pueden requerirse credenciales de autenticación para la API de destino
Nodos utilizados (23)
Categoría
{
"id": "K67zdPMDUtledQFV",
"meta": {
"instanceId": "189dde98270e9ce0f006f0e9deb96aa5e627396fc6279cac8902c9b06936984d"
},
"name": "Global Holiday Conflict Detector and Meeting Rescheduler",
"tags": [],
"nodes": [
{
"id": "b487208f-33b3-4527-8e18-bb3c2a8746d7",
"name": "Verificación Diaria",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-752,
224
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 9
}
]
}
},
"typeVersion": 1.2
},
{
"id": "a8f9264a-5dba-4801-9c1e-e12a59237b8b",
"name": "Configuración del Flujo de Trabajo",
"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": "Combinar y Filtrar Festivos de la Próxima Semana",
"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": "Obtener Eventos de Calendario de la Próxima Semana",
"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": "Detectar Conflictos con Festivos",
"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": "Verificar Si Se Encontraron Conflictos",
"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": "Generar Sugerencias de Reprogramación",
"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": "Formatear Resumen 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": "Publicar Resumen 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": "Iterar Sobre Elementos",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-304,
80
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "74320878-4302-440c-99ce-5aefefcb1ca2",
"name": "Obtener Días Festivos Públicos",
"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": "Nota: Verificación Diaria",
"type": "n8n-nodes-base.stickyNote",
"position": [
-816,
64
],
"parameters": {
"color": "white",
"height": 144,
"content": "**Purpose:** Triggers the workflow once every weekday morning.\n**Key:** Runs at 09:00 server time (adjust in node).\n**Tip:** Change to weekly if you only need Monday runs."
},
"typeVersion": 1
},
{
"id": "0541544f-2e8e-41a7-b899-76de44829cca",
"name": "Nota: Configuración del Flujo de Trabajo",
"type": "n8n-nodes-base.stickyNote",
"position": [
-544,
-208
],
"parameters": {
"color": "white",
"width": 256,
"height": 208,
"content": "**Purpose:** Central place to define variables.\n**Fields:** `currentYear`, `nextWeekStart`, `nextWeekEnd`, `countryCodes`, `slackChannel`, `calendarId`.\n**Tip:** Edit country list and calendar/channel IDs here only."
},
"typeVersion": 1
},
{
"id": "02931af5-a167-422c-ae32-53412ed0f4f4",
"name": "Nota: Combinar y Filtrar Festivos de la Próxima Semana",
"type": "n8n-nodes-base.stickyNote",
"position": [
80,
48
],
"parameters": {
"color": "white",
"content": "**Purpose:** Merges API results and filters to next week only.\n**Output:** `holidays[]` with `date`, `name`, `countryCode`."
},
"typeVersion": 1
},
{
"id": "79ac359f-12a3-43ba-bf75-b987f37f482e",
"name": "Nota: Obtener Eventos de Calendario de la Próxima Semana",
"type": "n8n-nodes-base.stickyNote",
"position": [
-368,
480
],
"parameters": {
"color": "white",
"content": "**Purpose:** Reads all events in next week’s window from Google Calendar.\n**Config:** Uses `calendarId` and `timeMin/Max` from the Set node.\n**Note:** Re-connect your own Google credential in n8n."
},
"typeVersion": 1
},
{
"id": "165237d2-da5e-408f-bb46-77be9bf4c38c",
"name": "Nota: Detectar Conflictos con Festivos",
"type": "n8n-nodes-base.stickyNote",
"position": [
384,
48
],
"parameters": {
"color": "white",
"content": "**Purpose:** Compares event dates with holiday dates to find conflicts.\n**Output:** Single item `{ conflicts[], totalConflicts }` so downstream runs once."
},
"typeVersion": 1
},
{
"id": "3b55ac06-aa21-44de-a6e0-37051e02cfa1",
"name": "Nota: Verificar Si Se Encontraron Conflictos",
"type": "n8n-nodes-base.stickyNote",
"position": [
688,
48
],
"parameters": {
"color": "white",
"content": "**Purpose:** Guards the branch; continues only when conflicts exist.\n**Condition:** Array not empty."
},
"typeVersion": 1
},
{
"id": "fc01dc46-773b-4881-b646-2bcf21c7bc1f",
"name": "Nota: Generar Sugerencias de Reprogramación",
"type": "n8n-nodes-base.stickyNote",
"position": [
992,
48
],
"parameters": {
"color": "white",
"content": "**Purpose:** Suggests next business day that is not a holiday/weekend.\n**Logic:** Looks ahead up to 30 days, preserving original start time."
},
"typeVersion": 1
},
{
"id": "cec6277f-583d-4ba9-8063-2e956395bff5",
"name": "Nota: Formatear Resumen Slack",
"type": "n8n-nodes-base.stickyNote",
"position": [
1296,
48
],
"parameters": {
"color": "white",
"content": "**Purpose:** Creates a readable Slack message.\n**Includes:** Event, date/time, affected countries, holiday, suggestion."
},
"typeVersion": 1
},
{
"id": "6d766a0d-bea6-4bbb-a824-127b53b9dc45",
"name": "Nota: Publicar Resumen Slack",
"type": "n8n-nodes-base.stickyNote",
"position": [
1600,
48
],
"parameters": {
"color": "white",
"content": "**Purpose:** Posts the digest to Slack.\n**Config:** Uses `slackChannel` from the Set node.\n**Note:** Re-connect your own Slack OAuth in n8n (left unconfigured)."
},
"typeVersion": 1
},
{
"id": "8461d7cc-e39d-4c86-9c6f-96a7aa55ba4f",
"name": "Nota: Iterar Sobre Elementos",
"type": "n8n-nodes-base.stickyNote",
"position": [
-208,
-160
],
"parameters": {
"color": "white",
"content": "**Purpose:** Iterates through each country code.\n**Flow:** Splits the array to call the holiday API per country, then loops back."
},
"typeVersion": 1
},
{
"id": "f857f854-7ed9-43c1-af05-7e596d8369dc",
"name": "Nota: Obtener Días Festivos Públicos",
"type": "n8n-nodes-base.stickyNote",
"position": [
96,
-192
],
"parameters": {
"color": "white",
"width": 288,
"height": 192,
"content": "**Purpose:** Calls Nager.Date API for public holidays.\n**URL:** `https://date.nager.at/api/v3/PublicHolidays/{year}/{country}`.\n**Security:** No API key needed. Keep credentials empty."
},
"typeVersion": 1
},
{
"id": "48d8def6-1e2e-4e03-b963-a44f9e2c0de1",
"name": "Resumen de Plantilla",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1632,
-448
],
"parameters": {
"color": "yellow",
"width": 688,
"height": 752,
"content": "## What this template does\nDetects global public-holiday conflicts in **next week’s** meetings and posts a Slack digest with suggested reschedule dates.\n\n## Who it’s for\nRemote and distributed teams that want to avoid scheduling meetings on regional holidays.\n\n## How it works\n1) Fetch public holidays for selected country codes. \n2) Pull next week’s events from Google Calendar. \n3) Detect date overlaps and generate a reschedule suggestion (next weekday that isn’t a holiday). \n4) Post a single Slack summary message.\n\n## How to set up\n- Configure **countryCodes**, **calendarId**, and **slackChannel** in the **Workflow Configuration** (Set) node. \n- Connect your own Google Calendar and Slack credentials in n8n (left as unauthenticated by design).\n\n## Requirements\n- n8n (self-hosted or Cloud) \n- Slack app with chat:write and channel access \n- Google Calendar with read access\n\n## Customize\n- Change the time window by editing `nextWeekStart/End` in **Workflow Configuration**. \n- Adjust the suggestion logic in **Generate Reschedule Suggestions**."
},
"typeVersion": 1
}
],
"active": false,
"pinData": {},
"settings": {
"executionOrder": "v1"
},
"versionId": "c01575e0-ea13-45a5-bb0a-c771b4107645",
"connections": {
"b487208f-33b3-4527-8e18-bb3c2a8746d7": {
"main": [
[
{
"node": "a8f9264a-5dba-4801-9c1e-e12a59237b8b",
"type": "main",
"index": 0
}
]
]
},
"d04fe2ce-a6f3-49d5-a384-39a3a201fa37": {
"main": [
[
{
"node": "6472f288-c7e2-49da-bd32-61b91d9528b3",
"type": "main",
"index": 0
}
],
[
{
"node": "74320878-4302-440c-99ce-5aefefcb1ca2",
"type": "main",
"index": 0
}
]
]
},
"4bb37c98-7400-4e4c-9519-4cafa5555067": {
"main": [
[
{
"node": "d01a7818-dc28-4490-ad5b-e5963cd22cbd",
"type": "main",
"index": 0
}
]
]
},
"74320878-4302-440c-99ce-5aefefcb1ca2": {
"main": [
[
{
"node": "d04fe2ce-a6f3-49d5-a384-39a3a201fa37",
"type": "main",
"index": 0
}
]
]
},
"a8f9264a-5dba-4801-9c1e-e12a59237b8b": {
"main": [
[
{
"node": "d04fe2ce-a6f3-49d5-a384-39a3a201fa37",
"type": "main",
"index": 0
},
{
"node": "524535d0-6d38-4da8-a5c9-e775f4ff4e50",
"type": "main",
"index": 0
}
]
]
},
"32c956cf-781f-4548-96a9-9d758a7f22a3": {
"main": [
[
{
"node": "fd84ad56-55c4-49c4-9e60-368977208c0c",
"type": "main",
"index": 0
}
]
]
},
"5ed5e1c2-726a-43bd-8f49-ed40f3eac9fa": {
"main": [
[
{
"node": "32c956cf-781f-4548-96a9-9d758a7f22a3",
"type": "main",
"index": 0
}
]
]
},
"fd84ad56-55c4-49c4-9e60-368977208c0c": {
"main": [
[
{
"node": "4bb37c98-7400-4e4c-9519-4cafa5555067",
"type": "main",
"index": 0
}
]
]
},
"6472f288-c7e2-49da-bd32-61b91d9528b3": {
"main": [
[
{
"node": "5ed5e1c2-726a-43bd-8f49-ed40f3eac9fa",
"type": "main",
"index": 0
}
]
]
}
}
}¿Cómo usar este flujo de trabajo?
Copie el código de configuración JSON de arriba, cree un nuevo flujo de trabajo en su instancia de n8n y seleccione "Importar desde JSON", pegue la configuración y luego modifique la configuración de credenciales según sea necesario.
¿En qué escenarios es adecuado este flujo de trabajo?
Avanzado - Productividad personal
¿Es de pago?
Este flujo de trabajo es completamente gratuito, puede importarlo y usarlo directamente. Sin embargo, tenga en cuenta que los servicios de terceros utilizados en el flujo de trabajo (como la API de OpenAI) pueden requerir un pago por su cuenta.
Flujos de trabajo relacionados recomendados
Takuya Ojima
@takuyaCompartir este flujo de trabajo