{
  "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 26: Check if there is a record or not?": {
      "main": [
        [],
        [
          {
            "node": "Step 27: Check if it is currently >= 8 AM?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 39: Check if there is a record or not?": {
      "main": [
        [
          {
            "node": "Step 40: Filter out events whose titles match the action of a customer visiting the company.",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Step 41: Check if there is a record or not?": {
      "main": [
        [
          {
            "node": "Step 42: Read the Odoo Calendar Event notifications that have been sent.",
            "type": "main",
            "index": 0
          }
        ],
        []
      ]
    },
    "Step 44: Check if there is a record or not?": {
      "main": [
        [],
        [
          {
            "node": "Step 45: Check if it is currently >= 8 AM?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 31: Call Slack API's getUploadURLExternal API": {
      "main": [
        [
          {
            "node": "Step 32: Choose branch 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 49: Call Slack API's getUploadURLExternal API": {
      "main": [
        [
          {
            "node": "Step 50: Choose branch 2",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 10: Filter out records that have not been sent.": {
      "main": [
        [
          {
            "node": "Step 11: Check if there is a record or not?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 25: Filter out records that have not been sent.": {
      "main": [
        [
          {
            "node": "Step 26: Check if there is a record or not?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 43: Filter out records that have not been sent.": {
      "main": [
        [
          {
            "node": "Step 44: Check if there is a record or not?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 13: Extract the email and member ID of the partners.": {
      "main": [
        [
          {
            "node": "Step 12: Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 1: Activation schedule: Every 1 hour, from 6 AM to 6 PM.": {
      "main": [
        [
          {
            "node": "Step 2: Set Variables",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 38: Get the event information from Google Calendar for today.": {
      "main": [
        [
          {
            "node": "Step 39: Check if there is a record or not?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 20: Get the event information from Google Calendar for today..": {
      "main": [
        [
          {
            "node": "Step 21: Check if there is a record or not?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 19: Fill in the Calendar Event information in the Google Sheet.": {
      "main": [
        [
          {
            "node": "Step 18: Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 37: Fill in the Calendar Event information in the Google Sheet.": {
      "main": [
        [
          {
            "node": "Step 36: Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 4: Get the event information from Google Calendar for tomorrow.": {
      "main": [
        [
          {
            "node": "Step 5: Check if there is a record or not?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 55: Fill in the Calendar Event information in the Google Sheet.": {
      "main": [
        [
          {
            "node": "Step 54: Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 8: Read the Odoo Calendar Event notifications that have been sent.": {
      "main": [
        [
          {
            "node": "Step 9: Read Google Sheets member slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 24: Read the Odoo Calendar Event notifications that have been sent.": {
      "main": [
        [
          {
            "node": "Step 25: Filter out records that have not been sent.",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 42: Read the Odoo Calendar Event notifications that have been sent.": {
      "main": [
        [
          {
            "node": "Step 43: Filter out records that have not been sent.",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 34: Call the completeUploadExternal Slack API to complete the file upload.": {
      "main": [
        [
          {
            "node": "Step 35: Return output of step 25",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 52: Call the completeUploadExternal Slack API to complete the file upload.": {
      "main": [
        [
          {
            "node": "Step 53: Return output of step 43",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 6: Filter out events whose titles match the action of a customer visiting the company.": {
      "main": [
        [
          {
            "node": "Step 7: Check if there is a record or not?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 22: Filter out events whose titles match the action of a customer visiting the company.": {
      "main": [
        [
          {
            "node": "Step 23: Check if there is a record or not?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Step 40: Filter out events whose titles match the action of a customer visiting the company.": {
      "main": [
        [
          {
            "node": "Step 41: Check if there is a record or not?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}