This workflow corresponds to n8n.io template #13111 — we link there as the canonical source.
This workflow follows the Google Calendar → Google Sheets 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": "deYaFqsqAPYeo2Nc",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Customer Visit Notification",
"tags": [
{
"id": "eumrxkLAF6iNOufx",
"name": "Noti",
"createdAt": "2025-12-26T10:56:32.851Z",
"updatedAt": "2025-12-26T10:56:32.851Z"
},
{
"id": "P0InItjxOPutjm62",
"name": "Slack",
"createdAt": "2025-09-22T07:52:01.502Z",
"updatedAt": "2025-09-22T07:52:01.502Z"
},
{
"id": "HyLSX6V7QZs5MrZy",
"name": "Odoo",
"createdAt": "2025-03-24T01:54:27.030Z",
"updatedAt": "2025-03-24T01:54:27.030Z"
}
],
"nodes": [
{
"id": "26d95d9c-8b73-4503-9f0d-c8f54960089c",
"name": "Step 1: Activation schedule: Every 1 hour, from 6 AM to 6 PM.",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-2720,
2016
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "=0 6-18/1 * * *"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "cabcc91d-be29-46b1-a802-7e150f2a598b",
"name": "Step 4: Get the event information from Google Calendar for tomorrow.",
"type": "n8n-nodes-base.googleCalendar",
"position": [
-2512,
2272
],
"parameters": {
"limit": "={{ 100 }}",
"options": {},
"timeMax": "={{ $node['Step 3: Get datetime'].json.tomorrow }} 23:55:00",
"timeMin": "={{ $node['Step 3: Get datetime'].json.tomorrow }} 00:05:00",
"calendar": {
"__rl": true,
"mode": "list",
"value": ""
},
"operation": "getAll"
},
"credentials": {
"googleCalendarOAuth2Api": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 1.3,
"alwaysOutputData": true
},
{
"id": "a6fd4c7e-56ca-4670-9d28-986c279ea5a7",
"name": "Step 2: Set Variables",
"type": "n8n-nodes-base.set",
"position": [
-2512,
2016
],
"parameters": {
"mode": "raw",
"options": {},
"jsonOutput": "={\n \"redmine6_Url\": \"\",\n \"Hour\": {{ $json.Hour }},\n \"limit\": 100,\n \"channel_id\": \"\",\n \"calendar_event_web_search_read\": \"\"\n}"
},
"typeVersion": 3.4
},
{
"id": "aa89d67f-6e98-4bd3-bcfb-0a4928282927",
"name": "Step 3: Get datetime",
"type": "n8n-nodes-base.code",
"position": [
-2720,
2272
],
"parameters": {
"jsCode": "const toVNTime = (d = new Date()) => {\n const date = new Date(d.getTime() + 7 * 60 * 60 * 1000); // UTC+7\n return date;\n};\n\nconst formatDate = (date) => {\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, \"0\");\n const d = String(date.getDate()).padStart(2, \"0\");\n return `${y}-${m}-${d}`;\n};\n\nconst formatDayMonth = (date) => {\n const d = String(date.getDate()).padStart(2, \"0\");\n const m = String(date.getMonth() + 1).padStart(2, \"0\");\n return `${d}/${m}`;\n};\n\nconst getDayOfWeek = (date) => {\n const days = [\"Monday\", \"Tuesday\", \"Wednesday\", \"Thursday\", \"Friday\", \"Saturday\", \"Sunday\"];\n return days[date.getDay()];\n};\n\nconst titleSheet = (date) => {\n const y = date.getFullYear();\n const m = String(date.getMonth() + 1).padStart(2, \"0\");\n return `${y}-${m}`;\n};\n\nconst now = toVNTime();\n\nconst yesterday = new Date(now);\nyesterday.setDate(yesterday.getDate() - 1);\n\nconst twoDaysAgo = new Date(now);\ntwoDaysAgo.setDate(twoDaysAgo.getDate() - 2);\n\nconst tomorrow = new Date(now);\ntomorrow.setDate(tomorrow.getDate() + 1);\n\n// ===== Last week (Monday \u2192 Sunday) =====\nconst today = new Date(now);\nconst dayIndex = (today.getDay() + 6) % 7; // Monday = 0\nconst todayStr = formatDate(now);\nconst [year, month, day] = (todayStr.split(\"-\"));\nconst startOfLastWeek = new Date(today);\nstartOfLastWeek.setDate(today.getDate() - dayIndex - 7);\n\nconst endOfLastWeek = new Date(startOfLastWeek);\nendOfLastWeek.setDate(startOfLastWeek.getDate() + 6);\n\n// ===== Current week (DD/MM) =====\nconst startOfWeek = new Date(today);\nstartOfWeek.setDate(today.getDate() - dayIndex);\n\nconst endOfWeek = new Date(startOfWeek);\nendOfWeek.setDate(startOfWeek.getDate() + 6);\n\n\nreturn [{\n today: formatDate(now),\n yesterday: formatDate(yesterday),\n twoDaysAgo: formatDate(twoDaysAgo),\n tomorrow: formatDate(tomorrow),\n\n dayOfWeekYesterday: getDayOfWeek(yesterday),\n monthYesterday: titleSheet(yesterday),\n\n startOfLastWeek: formatDate(startOfLastWeek),\n endOfLastWeek: formatDate(endOfLastWeek),\n\n startOfLastWeekDM: formatDayMonth(startOfLastWeek),\n endOfLastWeekDM: formatDayMonth(endOfLastWeek),\n\n reportYearFolder: `${year}-Report`,\n reportDateFolder: `${year}${month}${day}-Report`,\n}];\n"
},
"typeVersion": 2
},
{
"id": "a1f2dd0a-687b-4362-9f53-f2cf9541330d",
"name": "Step 5: Check if there is a record or not?",
"type": "n8n-nodes-base.if",
"position": [
-2288,
2272
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f1ab87cc-5385-4e2b-86c6-95a5a9d615ab",
"operator": {
"type": "object",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $node['Step 4: Get the event information from Google Calendar for tomorrow.'].json }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.3
},
{
"id": "8f315c0f-4b8f-4755-9ac6-44d52147e3a3",
"name": "Step 6: Filter out events whose titles match the action of a customer visiting the company.",
"type": "n8n-nodes-base.filter",
"maxTries": 2,
"position": [
-2016,
1968
],
"parameters": {
"options": {
"ignoreCase": true
},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "or",
"conditions": [
{
"id": "d3f13ec7-2eae-4c41-9714-7ece062e9859",
"operator": {
"type": "string",
"operation": "regex"
},
"leftValue": "={{ $json.summary }}",
"rightValue": "=(to come|to arrive|visit)"
}
]
}
},
"retryOnFail": true,
"typeVersion": 2.3
},
{
"id": "cb9a9a37-8d9e-4b44-a53c-e7cd81eea088",
"name": "Step 7: Check if there is a record or not?",
"type": "n8n-nodes-base.if",
"position": [
-1808,
1968
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f1ab87cc-5385-4e2b-86c6-95a5a9d615ab",
"operator": {
"type": "object",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $node['Step 6: Filter out events whose titles match the action of a customer visiting the company.'].json }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.3
},
{
"id": "304c1318-2f4a-4367-98a0-7830b3246625",
"name": "Step 8: Read the Odoo Calendar Event notifications that have been sent.",
"type": "n8n-nodes-base.googleSheets",
"maxTries": 2,
"position": [
-2016,
2176
],
"parameters": {
"sheetName": {
"__rl": true,
"mode": "list",
"value": "",
"cachedResultUrl": "",
"cachedResultName": ""
},
"documentId": {
"__rl": true,
"mode": "url",
"value": "="
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.7,
"alwaysOutputData": true
},
{
"id": "4f430f6b-406f-45cb-9ddb-9988d6000c5c",
"name": "Step 9: Read Google Sheets member slack",
"type": "n8n-nodes-base.googleSheets",
"maxTries": 2,
"position": [
-1808,
2176
],
"parameters": {
"sheetName": {
"__rl": true,
"mode": "list",
"value": "",
"cachedResultUrl": "",
"cachedResultName": ""
},
"documentId": {
"__rl": true,
"mode": "url",
"value": ""
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.5,
"alwaysOutputData": true
},
{
"id": "945af957-40d0-4027-b12f-52a8e5b617db",
"name": "Step 10: Filter out records that have not been sent.",
"type": "n8n-nodes-base.code",
"position": [
-1568,
1968
],
"parameters": {
"jsCode": "const odoo_Calendar_Event_Sheet = $items(\"Step 8: Read the Odoo Calendar Event notifications that have been sent.\");\nconst records = $items(\"Step 6: Filter out events whose titles match the action of a customer visiting the company.\");\n\nconst sentEventIds = new Set(\n odoo_Calendar_Event_Sheet\n .filter(item => item.json[\"The day before\"] === \"Sent\")\n .map(item => item.json.ID)\n);\n\nfunction formatDateTime(dateTimeStr) {\n if (!dateTimeStr) return null;\n\n return dateTimeStr\n .replace(\"T\", \" \")\n .replace(/\\+\\d{2}:\\d{2}$/, \"\");\n}\n\nconst result = records\n .map(item => {\n const name = item.json.summary.replace(/\\[.*?\\]\\s*/g, '');\n\n return {\n odoo_calendar_event_id: item.json.id,\n odoo_calendar_event_name: name,\n odoo_calendar_event_start: formatDateTime(item.json.start.dateTime),\n odoo_calendar_event_stop: formatDateTime(item.json.end.dateTime),\n attendees: item.json.attendees,\n };\n })\n .filter(item => !sentEventIds.has(item.odoo_calendar_event_id));\n\nreturn result;"
},
"executeOnce": true,
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "b9acac39-d43f-4c1b-89db-3d3991d745dd",
"name": "Step 11: Check if there is a record or not?",
"type": "n8n-nodes-base.if",
"position": [
-1360,
1968
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f1ab87cc-5385-4e2b-86c6-95a5a9d615ab",
"operator": {
"type": "object",
"operation": "empty",
"singleValue": true
},
"leftValue": "={{ $node['Step 10: Filter out records that have not been sent.'].json }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.3
},
{
"id": "432caf07-304c-45be-b072-9d575b9021a6",
"name": "Step 12: Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-1568,
2176
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "115e8306-2e66-40a6-9f52-2280ff466903",
"name": "Step 13: Extract the email and member ID of the partners.",
"type": "n8n-nodes-base.code",
"position": [
-1360,
2176
],
"parameters": {
"jsCode": "const attendee_ids = $node[\"Step 12: Loop Over Items\"].json.attendees;\nconst mergedResults = $items(\"Step 9: Read Google Sheets member slack\");\nconst odoo_calendar_event_name = $('Step 12: Loop Over Items').first().json.odoo_calendar_event_name;\nconst odoo_calendar_event_start = $('Step 12: Loop Over Items').first().json.odoo_calendar_event_start;\nconst odoo_calendar_event_stop = $('Step 12: Loop Over Items').first().json.odoo_calendar_event_stop;\n\nconst finalResult = [];\n\nfor (const item of attendee_ids) {\n const inputEmail = String(item.email || \"\").toLowerCase().trim();\n if (!inputEmail) continue;\n\n const inputLocal = inputEmail.split(\"@\")[0];\n\n let bestMatch = null;\n let bestScore = -1;\n\n for (const sheetItem of mergedResults) {\n const data = sheetItem.json || {};\n if (!data.Email) continue;\n\n const itemEmail = String(data.Email).toLowerCase().trim();\n const itemLocal = itemEmail.split(\"@\")[0];\n\n let score = 0;\n\n if (itemEmail === inputEmail) score = 100;\n else if (itemLocal === inputLocal) score = 80;\n else if (itemEmail.includes(inputLocal)) score = 50;\n\n if (score > bestScore) {\n bestScore = score;\n bestMatch = data;\n }\n }\n\n if (bestMatch && bestScore >= 50) {\n finalResult.push({\n email: inputEmail,\n memberID: bestMatch.memberID,\n odoo_calendar_event_name,\n odoo_calendar_event_start,\n odoo_calendar_event_stop\n });\n }\n}\n\nreturn finalResult;\n"
},
"executeOnce": true,
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "8a2d65d5-780a-46ea-9e75-be847210fb12",
"name": "Step 14: Loop Over Partners.",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-1136,
1968
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "f561fd3d-d1c8-4b8d-a883-125f90071b35",
"name": "Step 15: Wait 1s",
"type": "n8n-nodes-base.wait",
"position": [
-928,
2176
],
"parameters": {
"amount": 1
},
"executeOnce": true,
"typeVersion": 1.1
},
{
"id": "0e1b2c15-95eb-4850-ade5-dfd9b1fde2b5",
"name": "Step 16: Send a message to your Partners",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 2,
"position": [
-1136,
2176
],
"parameters": {
"url": "=https://slack.com/api/chat.postMessage",
"method": "POST",
"options": {},
"sendBody": true,
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{
"name": "=channel",
"value": "={{ $node['Step 14: Loop Over Partners.'].json.memberID}}"
},
{
"name": "=text",
"value": "=:mega: *Notification regarding a customer's upcoming visit to the company.!*\n\n*Title*: *{{ $node['Step 14: Loop Over Partners.'].json.odoo_calendar_event_name }}*\n*From*: {{ $node['Step 14: Loop Over Partners.'].json.odoo_calendar_event_start }}\n*To*: {{ $node['Step 14: Loop Over Partners.'].json.odoo_calendar_event_stop }}\n\n~~~ Add your content"
}
]
},
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpQueryAuth": {
"name": "<your credential>"
},
"httpBearerAuth": {
"name": "<your credential>"
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.2,
"alwaysOutputData": false
},
{
"id": "90e61f88-4d5c-4204-8de3-cef379f4dca5",
"name": "Step 17: Return output of step8",
"type": "n8n-nodes-base.code",
"position": [
-928,
1968
],
"parameters": {
"jsCode": "const all = $items(\"Step 10: Filter out records that have not been sent.\");\nreturn all;\n"
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "a4b1e046-e1ac-4356-9dc0-135747e03451",
"name": "Step 18: Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-704,
1968
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "8713519b-7300-4917-8fe2-f746ef6e8465",
"name": "Step 19: Fill in the Calendar Event information in the Google Sheet.",
"type": "n8n-nodes-base.googleSheets",
"position": [
-512,
1968
],
"parameters": {
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "url",
"value": ""
},
"documentId": {
"__rl": true,
"mode": "url",
"value": "="
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "e8e9f878-9d87-49c6-9663-4413830534ca",
"name": "Step 20: Get the event information from Google Calendar for today..",
"type": "n8n-nodes-base.googleCalendar",
"position": [
-704,
2176
],
"parameters": {
"limit": "={{ 100 }}",
"options": {},
"timeMax": "={{ $node['Step 3: Get datetime'].json.today }} 23:55:00",
"timeMin": "={{ $node['Step 3: Get datetime'].json.today }} 00:05:00",
"calendar": {
"__rl": true,
"mode": "list",
"value": ""
},
"operation": "getAll"
},
"credentials": {
"googleCalendarOAuth2Api": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 1.3,
"alwaysOutputData": true
},
{
"id": "af39e469-032b-4b85-b84a-2721fdfe0aab",
"name": "Step 21: Check if there is a record or not?",
"type": "n8n-nodes-base.if",
"position": [
-512,
2176
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f1ab87cc-5385-4e2b-86c6-95a5a9d615ab",
"operator": {
"type": "object",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $node['Step 20: Get the event information from Google Calendar for today..'].json }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.3
},
{
"id": "a8d90c8a-bd26-49da-a966-5b3c369a4352",
"name": "Step 22: Filter out events whose titles match the action of a customer visiting the company.",
"type": "n8n-nodes-base.filter",
"maxTries": 2,
"position": [
-272,
1968
],
"parameters": {
"options": {
"ignoreCase": true
},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "or",
"conditions": [
{
"id": "d3f13ec7-2eae-4c41-9714-7ece062e9859",
"operator": {
"type": "string",
"operation": "regex"
},
"leftValue": "={{ $json.summary }}",
"rightValue": "=(to come|to arrive|visit)"
}
]
}
},
"retryOnFail": true,
"typeVersion": 2.3
},
{
"id": "37a49308-2c53-4a3c-872f-3ee9c8475e84",
"name": "Step 23: Check if there is a record or not?",
"type": "n8n-nodes-base.if",
"position": [
-64,
1968
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f1ab87cc-5385-4e2b-86c6-95a5a9d615ab",
"operator": {
"type": "object",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $node['Step 22: Filter out events whose titles match the action of a customer visiting the company.'].json }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.3
},
{
"id": "dd7deb8d-316a-4506-9aae-147a05935ad4",
"name": "Step 24: Read the Odoo Calendar Event notifications that have been sent.",
"type": "n8n-nodes-base.googleSheets",
"maxTries": 2,
"position": [
-272,
2176
],
"parameters": {
"sheetName": {
"__rl": true,
"mode": "list",
"value": "",
"cachedResultUrl": "",
"cachedResultName": ""
},
"documentId": {
"__rl": true,
"mode": "url",
"value": "="
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.7,
"alwaysOutputData": true
},
{
"id": "64b6ea77-3634-4eb5-bdaa-ff6921891d06",
"name": "Step 25: Filter out records that have not been sent.",
"type": "n8n-nodes-base.code",
"position": [
-64,
2176
],
"parameters": {
"jsCode": "const odoo_Calendar_Event_Sheet = $items(\"Step 24: Read the Odoo Calendar Event notifications that have been sent.\");\n\nconst records = $items(\"Step 22: Filter out events whose titles match the action of a customer visiting the company.\");\n\nconst sentEventIds = new Set(\n odoo_Calendar_Event_Sheet\n .filter(item => item.json[\"Today\"] === \"Sent\")\n .map(item => item.json.ID)\n);\n\nfunction formatDateTime(dateTimeStr) {\n if (!dateTimeStr) return null;\n\n return dateTimeStr\n .replace(\"T\", \" \")\n .replace(/\\+\\d{2}:\\d{2}$/, \"\");\n}\n\nconst result = records\n .map(item => {\n const name = item.json.summary.replace(/\\[.*?\\]\\s*/g, '');\n\n return {\n odoo_calendar_event_id: item.json.id,\n odoo_calendar_event_name: name,\n odoo_calendar_event_start: formatDateTime(item.json.start.dateTime),\n odoo_calendar_event_stop: formatDateTime(item.json.end.dateTime),\n attendees: item.json.attendees,\n };\n })\n .filter(item => !sentEventIds.has(item.odoo_calendar_event_id));\n\nreturn result;\n"
},
"executeOnce": true,
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "2f413100-9f8e-4737-bc07-02142f828619",
"name": "Step 26: Check if there is a record or not?",
"type": "n8n-nodes-base.if",
"position": [
176,
1968
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f1ab87cc-5385-4e2b-86c6-95a5a9d615ab",
"operator": {
"type": "object",
"operation": "empty",
"singleValue": true
},
"leftValue": "={{ $node['Step 25: Filter out records that have not been sent.'].json }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.3
},
{
"id": "99e4392d-9dd5-4c7f-842c-3b5fafc1bb87",
"name": "Step 27: Check if it is currently >= 8 AM?",
"type": "n8n-nodes-base.if",
"position": [
384,
1968
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "e4908b50-6426-44af-93c3-4fa8906cecd9",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{ $node['Step 2: Set Variables'].json.Hour }}",
"rightValue": "={{ 08 }}"
}
]
},
"looseTypeValidation": true
},
"executeOnce": true,
"typeVersion": 2.3
},
{
"id": "45f88c5c-e407-45cf-9093-abbdfb77635f",
"name": "Step 28: Convert to text",
"type": "n8n-nodes-base.code",
"position": [
176,
2176
],
"parameters": {
"jsCode": "const data = $items(\"Step 25: Filter out records that have not been sent.\");\n\nconst tagString = data\n .map(item => {\n const j = item.json;\n return `*Event:* *${j.odoo_calendar_event_name}*\\n` +\n `*From:* ${j.odoo_calendar_event_start}\\n` +\n `*To:* ${j.odoo_calendar_event_stop}`;\n })\n .join('\\n\\n');\n\nconst finalMessage = `<!channel> :mega:\\n${tagString}`;\nconst safeMessage = finalMessage.replace(/\\n/g, '\\\\n');\nreturn [{ json: { message: safeMessage } }];\n\n"
},
"executeOnce": true,
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "f2ff389d-70e9-447a-8f85-74c995390e31",
"name": "Step 29: Download file image",
"type": "n8n-nodes-base.googleDrive",
"position": [
384,
2176
],
"parameters": {
"fileId": {
"__rl": true,
"mode": "url",
"value": "="
},
"options": {},
"operation": "download"
},
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 3
},
{
"id": "ad4a82ad-b2dc-4e20-a99c-d16ec55569e9",
"name": "Step 30: Convert KB \u2192 bytes",
"type": "n8n-nodes-base.code",
"position": [
608,
1968
],
"parameters": {
"jsCode": "const item = $input.first();\n\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\n\nitem.binary.data = {\n ...item.binary.data,\n fileSize: buffer.length,\n fileName: `input_name.png`,\n};\n\nreturn item;"
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "a6fde675-6248-4c33-bcad-c7c8c1a21105",
"name": "Step 32: Choose branch 2",
"type": "n8n-nodes-base.merge",
"position": [
608,
2176
],
"parameters": {
"mode": "chooseBranch",
"useDataOfInput": 2
},
"typeVersion": 3.2
},
{
"id": "ebae7fb4-abf9-4b69-9f3e-a97780cbcaa7",
"name": "Step 33: POST upload_url Slack",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 2,
"position": [
816,
2176
],
"parameters": {
"url": "={{ $node[\"Step 31: Call Slack API's getUploadURLExternal API\"].json.upload_url}}",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "binaryData",
"jsonHeaders": "={\n \"Content-Type\": \"application/octet-stream\"\n}",
"sendHeaders": true,
"specifyHeaders": "json",
"inputDataFieldName": "data"
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.2,
"alwaysOutputData": false
},
{
"id": "c724f434-ac95-4c13-9170-775b48a77fed",
"name": "Step 31: Call Slack API's getUploadURLExternal API",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 2,
"position": [
816,
1968
],
"parameters": {
"url": "=https://slack.com/api/files.getUploadURLExternal",
"method": "POST",
"options": {},
"jsonQuery": "={\n \"filename\": \"{{$binary.data.fileName}}\",\n \"length\": {{$binary.data.fileSize}}\n}",
"sendQuery": true,
"specifyQuery": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpQueryAuth": {
"name": "<your credential>"
},
"httpBearerAuth": {
"name": "<your credential>"
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.2,
"alwaysOutputData": false
},
{
"id": "67cfa35b-ee8a-4876-b7f3-b80fa5bc8a92",
"name": "Step 34: Call the completeUploadExternal Slack API to complete the file upload.",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 2,
"position": [
1040,
1968
],
"parameters": {
"url": "=https://slack.com/api/files.completeUploadExternal",
"method": "POST",
"options": {},
"jsonBody": "={\n \"files\": [\n {\n \"id\": \"{{ $node['Step 31: Call Slack API's getUploadURLExternal API'].json.file_id }}\",\n \"title\": \"input_title\"\n }\n ],\n \"channel_id\": \"{{ $node['Step 2: Set Variables'].json.channel_id}}\",\n \"initial_comment\": \"{{ $node['Step 28: Convert to text'].json.message }}\"\n}\n",
"sendBody": true,
"specifyBody": "=json",
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{}
]
},
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpQueryAuth": {
"name": "<your credential>"
},
"httpBearerAuth": {
"name": "<your credential>"
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.2,
"alwaysOutputData": false
},
{
"id": "1d9b1407-3c05-4d84-b3b1-8c6ac90baca6",
"name": "Step 35: Return output of step 25",
"type": "n8n-nodes-base.code",
"position": [
1232,
1968
],
"parameters": {
"jsCode": "const all = $items(\"Step 43: Filter out records that have not been sent.\");\nreturn all;\n"
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "c9e0405b-3bde-4952-a9f3-9c7928f6e46f",
"name": "Step 36: Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
1040,
2176
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "e408c49d-cfe8-4003-a685-cdca0620e005",
"name": "Step 37: Fill in the Calendar Event information in the Google Sheet.",
"type": "n8n-nodes-base.googleSheets",
"position": [
1232,
2176
],
"parameters": {
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "url",
"value": ""
},
"documentId": {
"__rl": true,
"mode": "url",
"value": "="
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "e7e3ccf3-3c8c-4183-b4e4-230ff019b921",
"name": "Step 38: Get the event information from Google Calendar for today.",
"type": "n8n-nodes-base.googleCalendar",
"position": [
-2016,
2608
],
"parameters": {
"limit": "={{ 100 }}",
"options": {},
"timeMax": "={{ $node['Step 3: Get datetime'].json.today }} 23:55:00",
"timeMin": "={{ $node['Step 3: Get datetime'].json.today }} 00:05:00",
"calendar": {
"__rl": true,
"mode": "id",
"value": ""
},
"operation": "getAll"
},
"credentials": {
"googleCalendarOAuth2Api": {
"name": "<your credential>"
}
},
"executeOnce": true,
"typeVersion": 1.3,
"alwaysOutputData": true
},
{
"id": "e5d03c50-5dff-4c0f-826f-2c5dd6e7d5ac",
"name": "Step 39: Check if there is a record or not?",
"type": "n8n-nodes-base.if",
"position": [
-1808,
2608
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f1ab87cc-5385-4e2b-86c6-95a5a9d615ab",
"operator": {
"type": "object",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $node['Step 38: Get the event information from Google Calendar for today.'].json }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.3
},
{
"id": "1ad802f9-6521-449c-a6a2-dc08c1edce76",
"name": "Step 40: Filter out events whose titles match the action of a customer visiting the company.",
"type": "n8n-nodes-base.filter",
"maxTries": 2,
"position": [
-2016,
2816
],
"parameters": {
"options": {
"ignoreCase": true
},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "or",
"conditions": [
{
"id": "d3f13ec7-2eae-4c41-9714-7ece062e9859",
"operator": {
"type": "string",
"operation": "regex"
},
"leftValue": "={{ $json.summary }}",
"rightValue": "=(to come|to arrive|visit)"
}
]
}
},
"retryOnFail": true,
"typeVersion": 2.3
},
{
"id": "ffb3dd65-22fc-4bcf-8a6e-64e3cc2648f2",
"name": "Step 41: Check if there is a record or not?",
"type": "n8n-nodes-base.if",
"position": [
-1808,
2816
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f1ab87cc-5385-4e2b-86c6-95a5a9d615ab",
"operator": {
"type": "object",
"operation": "notEmpty",
"singleValue": true
},
"leftValue": "={{ $node['Step 40: Filter out events whose titles match the action of a customer visiting the company.'].json }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.3
},
{
"id": "738d36f6-294a-4bca-82c4-9c12f4c5336e",
"name": "Step 42: Read the Odoo Calendar Event notifications that have been sent.",
"type": "n8n-nodes-base.googleSheets",
"maxTries": 2,
"position": [
-1568,
2608
],
"parameters": {
"sheetName": {
"__rl": true,
"mode": "list",
"value": "",
"cachedResultUrl": "",
"cachedResultName": ""
},
"documentId": {
"__rl": true,
"mode": "url",
"value": "="
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.7,
"alwaysOutputData": true
},
{
"id": "f107d42c-08fa-4eaa-9c6e-b5f38ad54cae",
"name": "Step 43: Filter out records that have not been sent.",
"type": "n8n-nodes-base.code",
"position": [
-1360,
2608
],
"parameters": {
"jsCode": "const odoo_Calendar_Event_Sheet = $items(\"Step 42: Read the Odoo Calendar Event notifications that have been sent.\");\n\nconst records = $items(\"Step 40: Filter out events whose titles match the action of a customer visiting the company.\");\n\nconst sentEventIds = new Set(\n odoo_Calendar_Event_Sheet\n .filter(item => item.json[\"Today\"] === \"Sent\")\n .map(item => item.json.ID)\n);\n\nfunction formatDateTime(dateTimeStr) {\n if (!dateTimeStr) return null;\n\n return dateTimeStr\n .replace(\"T\", \" \")\n .replace(/\\+\\d{2}:\\d{2}$/, \"\");\n}\n\nconst result = records\n .map(item => {\n const name = item.json.summary.replace(/\\[.*?\\]\\s*/g, '');\n\n return {\n odoo_calendar_event_id: item.json.id,\n odoo_calendar_event_name: name,\n odoo_calendar_event_start: formatDateTime(item.json.start.dateTime),\n odoo_calendar_event_stop: formatDateTime(item.json.end.dateTime),\n attendees: item.json.attendees,\n };\n })\n .filter(item => !sentEventIds.has(item.odoo_calendar_event_id));\n\nreturn result;\n"
},
"executeOnce": true,
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "a8824e2a-6a4b-4853-b7a5-8638d5537c18",
"name": "Step 44: Check if there is a record or not?",
"type": "n8n-nodes-base.if",
"position": [
-1568,
2816
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f1ab87cc-5385-4e2b-86c6-95a5a9d615ab",
"operator": {
"type": "object",
"operation": "empty",
"singleValue": true
},
"leftValue": "={{ $node['Step 43: Filter out records that have not been sent.'].json }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.3
},
{
"id": "635658e0-557e-481a-ae07-039c64e365f0",
"name": "Step 45: Check if it is currently >= 8 AM?",
"type": "n8n-nodes-base.if",
"position": [
-1360,
2816
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "e4908b50-6426-44af-93c3-4fa8906cecd9",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{ $node['Step 2: Set Variables'].json.Hour }}",
"rightValue": "={{ 08 }}"
}
]
},
"looseTypeValidation": true
},
"executeOnce": true,
"typeVersion": 2.3
},
{
"id": "81c2a562-7dc9-41f4-b338-f82abd75b1c7",
"name": "Step 46: Convert to text",
"type": "n8n-nodes-base.code",
"position": [
-1136,
2608
],
"parameters": {
"jsCode": "const data = $items(\"Step 43: Filter out records that have not been sent.\");\n\nconst tagString = data\n .map(item => {\n const j = item.json;\n return `*Event:* *${j.odoo_calendar_event_name}*\\n` +\n `*From:* ${j.odoo_calendar_event_start}\\n` +\n `*To:* ${j.odoo_calendar_event_stop}`;\n })\n .join('\\n\\n');\n\nconst finalMessage = `<!channel> :mega:\\n${tagString}`;\nconst safeMessage = finalMessage.replace(/\\n/g, '\\\\n');\nreturn [{ json: { message: safeMessage } }];\n\n"
},
"executeOnce": true,
"typeVersion": 2,
"alwaysOutputData": true
},
{
"id": "2ac1743e-9efe-43ca-94fe-82d512faa6e4",
"name": "Step 47: Download file image",
"type": "n8n-nodes-base.googleDrive",
"position": [
-928,
2608
],
"parameters": {
"fileId": {
"__rl": true,
"mode": "id",
"value": ""
},
"options": {},
"operation": "download"
},
"credentials": {
"googleDriveOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 3
},
{
"id": "03b54ae8-8f49-4cc9-b9ef-8741f9bd5e5f",
"name": "Step 48: Convert KB \u2192 bytes",
"type": "n8n-nodes-base.code",
"position": [
-704,
2608
],
"parameters": {
"jsCode": "const item = $input.first();\n\nconst buffer = await this.helpers.getBinaryDataBuffer(0, 'data');\nitem.binary.data = {\n ...item.binary.data,\n fileSize: buffer.length,\n fileName: `image.png`,\n};\n\nreturn item;"
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "594c0ad9-14f3-4575-8a44-d5bf81efc649",
"name": "Step 49: Call Slack API's getUploadURLExternal API",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 2,
"position": [
-1136,
2816
],
"parameters": {
"url": "=https://slack.com/api/files.getUploadURLExternal",
"method": "POST",
"options": {},
"jsonQuery": "={\n \"filename\": \"{{$binary.data.fileName}}\",\n \"length\": {{$binary.data.fileSize}}\n}",
"sendQuery": true,
"specifyQuery": "json",
"authentication": "genericCredentialType",
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpQueryAuth": {
"name": "<your credential>"
},
"httpBearerAuth": {
"name": "<your credential>"
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.2,
"alwaysOutputData": false
},
{
"id": "8663bcf0-78b4-4e4b-8bde-5dbeab4bc5f0",
"name": "Step 50: Choose branch 2",
"type": "n8n-nodes-base.merge",
"position": [
-928,
2816
],
"parameters": {
"mode": "chooseBranch",
"useDataOfInput": 2
},
"typeVersion": 3.2
},
{
"id": "43ad5a1a-658c-4bdf-9c25-06c913a07b11",
"name": "Step 51: POST upload_url Slack",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 2,
"position": [
-704,
2816
],
"parameters": {
"url": "={{ $node[\"Step 49: Call Slack API's getUploadURLExternal API\"].json.upload_url}}",
"method": "POST",
"options": {},
"sendBody": true,
"contentType": "binaryData",
"jsonHeaders": "={\n \"Content-Type\": \"application/octet-stream\"\n}",
"sendHeaders": true,
"specifyHeaders": "json",
"inputDataFieldName": "data"
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.2,
"alwaysOutputData": false
},
{
"id": "ce2afd81-a9eb-48f8-b7dc-836ea163907c",
"name": "Step 52: Call the completeUploadExternal Slack API to complete the file upload.",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"maxTries": 2,
"position": [
-512,
2608
],
"parameters": {
"url": "=https://slack.com/api/files.completeUploadExternal",
"method": "POST",
"options": {},
"jsonBody": "={\n \"files\": [\n {\n \"id\": \"{{ $node['Step 49: Call Slack API's getUploadURLExternal API'].json.file_id }}\",\n \"title\": \"input_title\"\n }\n ],\n \"channel_id\": \"{{ $node['Step 2: Set Variables'].json.channel_id}}\",\n \"initial_comment\": \"{{ $node['Step 46: Convert to text'].json.message }}\"\n}\n",
"sendBody": true,
"specifyBody": "=json",
"authentication": "genericCredentialType",
"bodyParameters": {
"parameters": [
{}
]
},
"genericAuthType": "httpBearerAuth"
},
"credentials": {
"httpQueryAuth": {
"name": "<your credential>"
},
"httpBearerAuth": {
"name": "<your credential>"
}
},
"executeOnce": true,
"retryOnFail": true,
"typeVersion": 4.2,
"alwaysOutputData": false
},
{
"id": "9894b3cd-bd04-4861-b4f5-1b71d583d876",
"name": "Step 53: Return output of step 43",
"type": "n8n-nodes-base.code",
"position": [
-288,
2608
],
"parameters": {
"jsCode": "const all = $items(\"Step 43: Filter out records that have not been sent.\");\nreturn all;\n"
},
"executeOnce": true,
"typeVersion": 2
},
{
"id": "031dd88f-e78a-4e54-9b8c-cc6f0f747099",
"name": "Step 54: Loop Over Items",
"type": "n8n-nodes-base.splitInBatches",
"position": [
-512,
2816
],
"parameters": {
"options": {}
},
"typeVersion": 3
},
{
"id": "b31df922-1bb0-4007-9290-5b252faf1b90",
"name": "Step 55: Fill in the Calendar Event information in the Google Sheet.",
"type": "n8n-nodes-base.googleSheets",
"position": [
-288,
2816
],
"parameters": {
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "url",
"value": ""
},
"documentId": {
"__rl": true,
"mode": "url",
"value": "="
}
},
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"typeVersion": 4.7
},
{
"id": "30270cab-2f81-410a-bc93-85969f166d44",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2112,
1776
],
"parameters": {
"color": 7,
"width": 1312,
"height": 640,
"content": "## 2. The system sends a notification to partners one day in advance."
},
"typeVersion": 1
},
{
"id": "7ea32518-a36e-4010-bdc5-bd11cc84fb68",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
-3584,
1776
],
"parameters": {
"width": 720,
"height": 1280,
"content": "# Customer Visit Notification\nThis workflow monitors Google Calendar for events indicating that a customer will visit the company today or the next day, retrieves the required details, and sends reminder notifications to the relevant stakeholders. It also posts a company-wide announcement to ensure proper preparation and a professional reception for the customer.\n\n## \ud83d\udcccWho is this for?\n- Reception / Administration team\n- Sales / Account owners in charge of customers\n- Management / Related team leaders\n- Security / IT / Logistics (for meeting room, equipment, and check-in preparation)\n\n## \ud83d\udcccThe problem\n- Customer visit information is usually shared manually and can be easily missed.\n- Related staff are not informed in time to prepare.\n- This causes last-minute preparation for reception, meeting rooms, documents, and support.\n- It affects the customer experience and the company\u2019s professional image.\n\n## \ud83d\udcccHow it works\n- When a customer meeting is scheduled, the system records the information (time, customer name, company, person in charge).\n- The system automatically sends notifications to related groups based on timeline:\n - Notify the whole office 1 hour before the visit.\n - Notify related members 24 hours in advance.\n- Notifications can be sent via Email / Slack / Internal chat.\n\n\n## \ud83d\udcccQuick setup\nRequired information:\n- N8n Version 2.4.6\n- Google Calendar OAuth2 API: Client ID, Client Secret\n- Google Sheets OAuth2 API: Client ID, Client Secret\n- Slack App: Bot User OAuth Token\n\n\nGoogle Sheets will be used to log all notified events.\n\n## \ud83d\udcccResults\n- Everyone is aware of the customer visit schedule.\n- Teams can proactively prepare meeting rooms, documents, and manpower.\n- Reduce mistakes and missed communication.\n- Improve customer experience and company professionalism.\n\n## \ud83d\udcccTake it further\n- Customer check-in using QR code / Visitor form\n- Send reminders to prepare documents\n- Store visit history\n- Monthly/quarterly reports for number of customer visits\n\n## \ud83d\udcccNeed help customizing?\nContact me for consulting and support:\n[Linkedin](https://www.linkedin.com/company/bac-ha-software/posts/?feedView=all) / [Website](https://bachasoftware.com/bhsoft-contacts)"
},
"typeVersion": 1
},
{
"id": "50bd906f-d87f-44a5-99d7-5042ccb17451",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2816,
1776
],
"parameters": {
"color": 7,
"width": 560,
"height": 720,
"content": "## 1. Set Variables and Get datetime"
},
"typeVersion": 1
},
{
"id": "50e9bb55-b56c-44cb-9e82-cedd827c4c47",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
-2112,
2480
],
"parameters": {
"color": 7,
"width": 2064,
"height": 592,
"content": "## 5. The system sends a company-wide announcement to a public channel today"
},
"typeVersion": 1
},
{
"id": "5cc130dd-ce52-40e4-8998-a4770a83e7ff",
"name": "Sticky Note3",
"type": "n8n-nodes-base.stickyNote",
"position": [
-768,
1776
],
"parameters": {
"color": 7,
"width": 432,
"height": 640,
"content": "## 3. Fill in the Calendar Event information in the Google Sheet."
},
"typeVersion": 1
},
{
"id": "0920be29-95ad-4ad8-b487-dba54b607b9a",
"name": "Sticky Note4",
"type": "n8n-nodes-base.stickyNote",
"position": [
-304,
1776
],
"parameters": {
"color": 7,
"width": 1712,
"height": 640,
"content": "## 4. The system sends a company-wide announcement to a public channel today and updates a note in the Google Sheet."
},
"typeVersion": 1
}
],
"active": false,
"settings": {
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "5450be35-7894-4cb6-acee-f9e35626b6c1",
"connections": {
"Step 15: Wait 1s": {
"main": [
[
{
"node": "Step 16: Send a message to your Partners",
"type": "main",
"index": 0
}
]
]
},
"Step 3: Get datetime": {
"main": [
[
{
"node": "Step 4: Get the event information from Google Calendar for tomorrow.",
"type": "main",
"index": 0
}
]
]
},
"Step 2: Set Variables": {
"main": [
[
{
"node": "Step 3: Get datetime",
"type": "main",
"index": 0
}
]
]
},
"Step 12: Loop Over Items": {
"main": [
[
{
"node": "Step 14: Loop Over Partners.",
"type": "main",
"index": 0
}
],
[
{
"node": "Step 13: Extract the email and member ID of the partners.",
"type": "main",
"index": 0
}
]
]
},
"Step 18: Loop Over Items": {
"main": [
[
{
"node": "Step 20: Get the event information from Google Calendar for today..",
"type": "main",
"index": 0
}
],
[
{
"node": "Step 19: Fill in the Calendar Event information in the Google Sheet.",
"type": "main",
"index": 0
}
]
]
},
"Step 28: Convert to text": {
"main": [
[
{
"node": "Step 29: Download file image",
"type": "main",
"index": 0
}
]
]
},
"Step 32: Choose branch 2": {
"main": [
[
{
"node": "Step 33: POST upload_url Slack",
"type": "main",
"index": 0
}
]
]
},
"Step 36: Loop Over Items": {
"main": [
[],
[
{
"node": "Step 37: Fill in the Calendar Event information in the Google Sheet.",
"type": "main",
"index": 0
}
]
]
},
"Step 46: Convert to text": {
"main": [
[
{
"node": "Step 47: Download file image",
"type": "main",
"index": 0
}
]
]
},
"Step 50: Choose branch 2": {
"main": [
[
{
"node": "Step 51: POST upload_url Slack",
"type": "main",
"index": 0
}
]
]
},
"Step 54: Loop Over Items": {
"main": [
[],
[
{
"node": "Step 55: Fill in the Calendar Event information in the Google Sheet.",
"type": "main",
"index": 0
}
]
]
},
"Step 14: Loop Over Partners.": {
"main": [
[
{
"node": "Step 17: Return output of step8",
"type": "main",
"index": 0
}
],
[
{
"node": "Step 15: Wait 1s",
"type": "main",
"index": 0
}
]
]
},
"Step 29: Download file image": {
"main": [
[
{
"node": "Step 30: Convert KB \u2192 bytes",
"type": "main",
"index": 0
}
]
]
},
"Step 47: Download file image": {
"main": [
[
{
"node": "Step 48: Convert KB \u2192 bytes",
"type": "main",
"index": 0
}
]
]
},
"Step 30: Convert KB \u2192 bytes": {
"main": [
[
{
"node": "Step 31: Call Slack API's getUploadURLExternal API",
"type": "main",
"index": 0
},
{
"node": "Step 32: Choose branch 2",
"type": "main",
"index": 1
}
]
]
},
"Step 48: Convert KB \u2192 bytes": {
"main": [
[
{
"node": "Step 49: Call Slack API's getUploadURLExternal API",
"type": "main",
"index": 0
},
{
"node": "Step 50: Choose branch 2",
"type": "main",
"index": 1
}
]
]
},
"Step 33: POST upload_url Slack": {
"main": [
[
{
"node": "Step 34: Call the completeUploadExternal Slack API to complete the file upload.",
"type": "main",
"index": 0
}
]
]
},
"Step 51: POST upload_url Slack": {
"main": [
[
{
"node": "Step 52: Call the completeUploadExternal Slack API to complete the file upload.",
"type": "main",
"index": 0
}
]
]
},
"Step 17: Return output of step8": {
"main": [
[
{
"node": "Step 18: Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Step 35: Return output of step 25": {
"main": [
[
{
"node": "Step 36: Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Step 53: Return output of step 43": {
"main": [
[
{
"node": "Step 54: Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Step 9: Read Google Sheets member slack": {
"main": [
[
{
"node": "Step 10: Filter out records that have not been sent.",
"type": "main",
"index": 0
}
]
]
},
"Step 16: Send a message to your Partners": {
"main": [
[
{
"node": "Step 14: Loop Over Partners.",
"type": "main",
"index": 0
}
]
]
},
"Step 27: Check if it is currently >= 8 AM?": {
"main": [
[
{
"node": "Step 28: Convert to text",
"type": "main",
"index": 0
}
],
[]
]
},
"Step 45: Check if it is currently >= 8 AM?": {
"main": [
[
{
"node": "Step 46: Convert to text",
"type": "main",
"index": 0
}
],
[]
]
},
"Step 5: Check if there is a record or not?": {
"main": [
[
{
"node": "Step 6: Filter out events whose titles match the action of a customer visiting the company.",
"type": "main",
"index": 0
}
],
[
{
"node": "Step 38: Get the event information from Google Calendar for today.",
"type": "main",
"index": 0
}
]
]
},
"Step 7: Check if there is a record or not?": {
"main": [
[
{
"node": "Step 8: Read the Odoo Calendar Event notifications that have been sent.",
"type": "main",
"index": 0
}
],
[]
]
},
"Step 11: Check if there is a record or not?": {
"main": [
[],
[
{
"node": "Step 12: Loop Over Items",
"type": "main",
"index": 0
}
]
]
},
"Step 21: Check if there is a record or not?": {
"main": [
[
{
"node": "Step 22: Filter out events whose titles match the action of a customer visiting the company.",
"type": "main",
"index": 0
}
],
[]
]
},
"Step 23: Check if there is a record or not?": {
"main": [
[
{
"node": "Step 24: Read the Odoo Calendar Event notifications that have been sent.",
"type": "main",
"index": 0
}
],
[]
]
},
"Step
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
googleCalendarOAuth2ApigoogleDriveOAuth2ApigoogleSheetsOAuth2ApihttpBearerAuthhttpQueryAuth
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow monitors Google Calendar for events indicating that a customer will visit the company today or the next day, retrieves the required details, and sends reminder notifications to the relevant stakeholders. It also posts a company-wide announcement to ensure proper…
Source: https://n8n.io/workflows/13111/ — 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.
LefBot — V1 Master Pipeline (11 Feb). Uses googleSheets, httpRequest, googleDrive. Scheduled trigger; 37 nodes.
LefBot V1 Master Pipeline 11Feb. Uses googleSheets, httpRequest, googleDrive. Scheduled trigger; 37 nodes.
LefBot — V1 Master Pipeline (11 Feb v2). Uses googleSheets, httpRequest, googleDrive. Scheduled trigger; 37 nodes.
This n8n workflow automatically finds apartments for rent in Germany, filters them by your city, rent budget, and number of rooms, and applies to them via email. Each application includes: A personali
Workflow Overview Zoom Attendance Evaluator with Follow-up is an n8n automation workflow that automatically evaluates Zoom meeting attendance and sends follow-up emails to no-shows and early leavers w