AutomationFlowsSlack & Telegram › Monitor Excel 365 Changes with Google Sheets and Telegram

Monitor Excel 365 Changes with Google Sheets and Telegram

Original n8n title: Track Excel 365 Changes and Approvals with Telegram and Google Sheets Logging

ByRamdoni @ramdoni on n8n.io

This workflow monitors an Excel 365 sheet every minute and detects new, updated, and deleted rows using a unique ID column. It compares the current dataset with the previous snapshot and identifies field-level differences.

Cron / scheduled trigger★★★★☆ complexity21 nodesMicrosoft ExcelGoogle SheetsTelegram
Slack & Telegram Trigger: Cron / scheduled Nodes: 21 Complexity: ★★★★☆ Added:

This workflow corresponds to n8n.io template #13716 — we link there as the canonical source.

This workflow follows the Google Sheets → Telegram 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 →

Download .json
{
  "id": "2sUwYje5ZEf8dXH9",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Track changes and approvals in Excel 365",
  "tags": [],
  "nodes": [
    {
      "id": "5b8a06be-2ef3-437c-8003-826d8e868c58",
      "name": "Get rows",
      "type": "n8n-nodes-base.microsoftExcel",
      "position": [
        112,
        128
      ],
      "parameters": {
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "{C46945EA-111F-884A-9CE2-7890BA63B938}",
          "cachedResultUrl": "https://onedrive.live.com/personal/f1cfeff06c99e405/_layouts/15/doc.aspx?resid=1736a44f-46ee-4594-ac0c-ca2529c411a8&cid=f1cfeff06c99e405&activeCell=Sheet2!A1:E23",
          "cachedResultName": "Table_sheet2"
        },
        "filters": {},
        "resource": "table",
        "workbook": {
          "__rl": true,
          "mode": "list",
          "value": "F1CFEFF06C99E405!s1736a44f46ee4594ac0cca2529c411a8",
          "cachedResultUrl": "https://onedrive.live.com/personal/f1cfeff06c99e405/_layouts/15/doc.aspx?resid=1736a44f-46ee-4594-ac0c-ca2529c411a8&cid=f1cfeff06c99e405",
          "cachedResultName": "Contoh Excel N8N"
        },
        "operation": "getRows",
        "returnAll": true,
        "worksheet": {
          "__rl": true,
          "mode": "list",
          "value": "{9ECBF732-2A1B-49F1-AC81-3EB96DB24F0C}",
          "cachedResultUrl": "https://onedrive.live.com/personal/f1cfeff06c99e405/_layouts/15/doc.aspx?resid=1736a44f-46ee-4594-ac0c-ca2529c411a8&cid=f1cfeff06c99e405&activeCell=Sheet2!A1",
          "cachedResultName": "Sheet2"
        }
      },
      "credentials": {
        "microsoftExcelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "c2076d05-31dd-49b0-8016-4c99ab7a2fed",
      "name": "Normalize Data",
      "type": "n8n-nodes-base.code",
      "position": [
        320,
        128
      ],
      "parameters": {
        "jsCode": "let items = $input.all();\nreturn items.map(item => {\n  return {\n    json: {\n      ...item.json,\n      ID: String(item.json.ID).trim()\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "d8329b72-1d5c-43d8-8b71-8512f306cb9a",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -144,
        48
      ],
      "parameters": {
        "color": 7,
        "width": 1024,
        "height": 272,
        "content": "## Step 1 "
      },
      "typeVersion": 1
    },
    {
      "id": "ab56b1b5-ddba-4ea3-8c52-139a87f18974",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -128,
        384
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 352,
        "content": "## Step 2 "
      },
      "typeVersion": 1
    },
    {
      "id": "8b277663-15b3-421e-bab9-effd20cfb42b",
      "name": "Append Log to Excel",
      "type": "n8n-nodes-base.microsoftExcel",
      "position": [
        384,
        832
      ],
      "parameters": {
        "options": {},
        "dataMode": "autoMap",
        "resource": "worksheet",
        "workbook": {
          "__rl": true,
          "mode": "list",
          "value": "F1CFEFF06C99E405!s1736a44f46ee4594ac0cca2529c411a8",
          "cachedResultUrl": "https://onedrive.live.com/personal/f1cfeff06c99e405/_layouts/15/doc.aspx?resid=1736a44f-46ee-4594-ac0c-ca2529c411a8&cid=f1cfeff06c99e405",
          "cachedResultName": "Contoh Excel N8N"
        },
        "operation": "append",
        "worksheet": {
          "__rl": true,
          "mode": "list",
          "value": "{82742EB8-C5C4-7049-B489-EC3070316882}",
          "cachedResultUrl": "https://onedrive.live.com/personal/f1cfeff06c99e405/_layouts/15/doc.aspx?resid=1736a44f-46ee-4594-ac0c-ca2529c411a8&cid=f1cfeff06c99e405&activeCell=Audit_Log!A1",
          "cachedResultName": "Audit_Log"
        }
      },
      "credentials": {
        "microsoftExcelOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "0f3f7587-3db5-4bf8-a1a9-6999b69b4f28",
      "name": "Append Log to Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        384,
        992
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [
            {
              "id": "No",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "No",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Timestamp",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Timestamp",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "ChangeType",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "ChangeType",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "RowID",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "RowID",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Field",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Field",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "OldValue",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "OldValue",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "NewValue",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "NewValue",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1H5P7YgBkyFgOSKZE4CMd8bu4L1b4ki7YSSaCpNMaL9E/edit#gid=0",
          "cachedResultName": "Audit_Log"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1H5P7YgBkyFgOSKZE4CMd8bu4L1b4ki7YSSaCpNMaL9E",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1H5P7YgBkyFgOSKZE4CMd8bu4L1b4ki7YSSaCpNMaL9E/edit?usp=drivesdk",
          "cachedResultName": "Contoh n8n"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "f5ed1189-72e8-4c7b-a2d7-27ff4c215b89",
      "name": "Environment Config",
      "type": "n8n-nodes-base.code",
      "position": [
        512,
        128
      ],
      "parameters": {
        "jsCode": "return [{\n  json: {\n    CONFIG: {\n      idField: \"ID\",\n      ignoreFields: [\"UpdatedAt\", \"LastModified\"],\n      monitorOnly: null, \n      firstRunSilent: true,\n      enableAuditLog: true\n    }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "ad209afb-740f-47ad-8e76-a08e0e1e1e73",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -128,
        800
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 368,
        "content": "## Step 4 \u2013 Audit Log"
      },
      "typeVersion": 1
    },
    {
      "id": "c0955ae1-fa4d-4e29-a0eb-1e52806b9e72",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1568,
        -592
      ],
      "parameters": {
        "width": 832,
        "height": 3552,
        "content": "# Track changes and approvals in Excel 365\n\n## \ud83d\udccc Overview\n\nThis workflow monitors an Excel 365 sheet every minute and detects new, updated, and deleted rows using a unique ID column. It compares the current dataset with the previous snapshot and identifies field-level differences.\n\nWhen changes are detected, the workflow filters rows that require approval (Status = \u201cWaiting Approval\u201d), sends structured notifications, and optionally logs every field-level change into an audit sheet (Excel or Google Sheets).\n\nThe configuration layer allows you to define the ID column, ignored fields, and audit logging behavior without modifying the comparison logic.\n\nThis template is suitable for approval tracking, operational monitoring, and lightweight compliance logging.\n\n\u2e3b\n\n## \ud83d\ude80 Features\n\n### \u23f1 Scheduled Monitoring\n- Runs automatically every 1 minute\n- Near real-time Excel monitoring\n- Prevents unnecessary execution when no changes are detected\n\n### \ud83d\udd0d Row-Level Change Detection\n\n### Detects:\n- \u2705 New rows\n- \u270f\ufe0f Updated rows\n- \u274c Deleted rows\n\nUses a unique ID field per row for accurate tracking.\n\n\u2e3b\n\n### \ud83e\udde0 Field-Level Comparison\n- Compares previous vs current values\n- Identifies exactly which fields changed\n- Outputs structured change data\n- Prevents false positives via data normalization\n\n\u2e3b\n\n## \u2699\ufe0f Environment Configuration Layer\n\nCentralized configuration node allows easy customization without modifying core logic.\n\n## Configurable options include:\n- idField\n- ignoreFields\n- monitorOnly\n- firstRunSilent\n- enableAuditLog\n\nNo hardcoded logic required.\n\n\u2e3b\n\n## \ud83d\uded1 Approval Validation Layer\n- Filters rows where Status = \"Waiting Approval\"\n- Sends notifications only for relevant approval cases\n- Prevents unnecessary alerts\n\n\u2e3b\n\n## \ud83d\udd14 Smart Notification System\n- Sends formatted change notifications\n- Includes:\n- Change Type (NEW / UPDATED / DELETED)\n- Row ID\n- Field-level old \u2192 new values\n\nFully customizable message formatting.\n\u2e3b\n## \ud83d\udcca Optional Audit Logging\n\nIf enabled in the Environment Config:\n- Converts each field-level change into structured audit rows\n- Appends logs to:\n- Excel 365 (Audit Sheet)\n- Google Sheets (External Log)\n\nAudit Log Structure\n\n| Timestamp | ChangeType | RowID | Field | OldValue | New Value |\n|-------------|--------------|--------|------|----------|------------|\n\nDesigned for compliance and tracking purposes.\n\n## \ud83d\udce6 Use Cases\n- Internal approval tracking\n- Financial data monitoring\n- Sales pipeline control\n- Procurement workflows\n- Excel-based compliance systems\n- SME automation systems\n\n## \ud83e\udde9 Requirements\n- Microsoft 365 (Excel Online \u2013 Business)\n- n8n (Cloud or Self-hosted)\n- Microsoft credentials configured in n8n\n- Telegram Bot\n- (Optional) Google Sheets credentials for audit logging\n\n## \ud83d\udd27 Configuration Guide\nAll system behavior is controlled from the Environment Config node.\n\nExample configuration structure:\n\n```\n{\n  CONFIG: {\n    idField: \"ID\",\n    ignoreFields: [\"UpdatedAt\", \"LastModified\"],\n    monitorOnly: null,\n    firstRunSilent: true,\n    enableAuditLog: true\n  }\n}\n```\n\nYou can customize:\n- Which column acts as unique ID\n- Which fields to ignore\n- Which fields to monitor exclusively\n- Whether to enable audit logging\n- Whether first run should be silent\n\n## \ud83d\udfe2 First Run Behavior\nOn first execution:\n- The workflow initializes internal snapshot storage\n- No mass notification is sent (if firstRunSilent = true)\n\nThis prevents false \u201cNEW row\u201d alerts during setup.\n## \ud83c\udfe2 Who Is This For?\n- Operations teams\n- Finance departments\n- SMEs using Excel as core system\n- Automation consultants\n- Businesses requiring lightweight audit tracking\n\n\u2e3b\n\n## \ud83d\udca1 Why This Workflow?\n\nUnlike simple Excel polling workflows, this solution:\n- Tracks changes at field level\n- Supports approval-based filtering\n- Includes structured audit logging\n- Avoids duplicate alerts\n- Is fully configurable\n- Designed for production usage\n\nThis is not just an Excel notifier \u2014 it is a structured Change Tracking & Approval Monitoring System built on n8n."
      },
      "typeVersion": 1
    },
    {
      "id": "6d5867c7-9ce0-493d-af68-d4b921f04c39",
      "name": "Notification Approval",
      "type": "n8n-nodes-base.telegram",
      "position": [
        864,
        544
      ],
      "parameters": {
        "text": "=Payment Request Need Approval\n\nRequester :  {{ $json.Requester }}\nKeterangan : {{ $json.Name }}\nNominal : {{ $json.Amount }}",
        "chatId": "123456789",
        "replyMarkup": "inlineKeyboard",
        "inlineKeyboard": {
          "rows": [
            {
              "row": {
                "buttons": [
                  {
                    "text": "Approve",
                    "additionalFields": {
                      "url": "https://n8n.entigi.co.id/webhook-test/2ed56b5e-5307-4b7c-bc17-c70da9814eba?status=1&ID={{ $json.ID }}"
                    }
                  },
                  {
                    "text": "Reject",
                    "additionalFields": {
                      "url": "=https://n8n.entigi.co.id/webhook-test/2ed56b5e-5307-4b7c-bc17-c70da9814eba?status=2&ID={{ $json.ID }}"
                    }
                  }
                ]
              }
            }
          ]
        },
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "bb0e40aa-f1cb-4d05-a858-fc3431e77f66",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        592,
        464
      ],
      "parameters": {
        "color": 7,
        "width": 496,
        "height": 272,
        "content": " ## Step 3 Approval\n"
      },
      "typeVersion": 1
    },
    {
      "id": "d02a76ce-a5ca-4a3d-bae4-eb1909316a4a",
      "name": "Run every 1 minute",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -96,
        128
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 1
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "36fa3e89-7789-4562-9c1a-abf13e222da6",
      "name": "Check if changes exist",
      "type": "n8n-nodes-base.if",
      "position": [
        -96,
        560
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "a79d003d-7f26-41d5-b722-aec295b87f31",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.hasChanges }}",
              "rightValue": false
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "97834431-1892-4201-9fef-3807fb020835",
      "name": "Check if audit logging is enabled",
      "type": "n8n-nodes-base.switch",
      "position": [
        -96,
        896
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "version": 2,
                  "leftValue": "",
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "id": "36f6286b-c585-495a-b282-c4bf3cad1c5f",
                    "operator": {
                      "type": "boolean",
                      "operation": "true",
                      "singleValue": true
                    },
                    "leftValue": "={{ $('Environment Config').item.json.CONFIG.enableAuditLog }}",
                    "rightValue": "true"
                  }
                ]
              }
            }
          ]
        },
        "options": {}
      },
      "typeVersion": 3.3
    },
    {
      "id": "949e7846-c8a3-46fd-9ed0-f459ec5c6398",
      "name": "Compare previous and current snapshot",
      "type": "n8n-nodes-base.code",
      "position": [
        720,
        128
      ],
      "parameters": {
        "jsCode": "// 1. Gunakan sintaks baru untuk Static Data\nconst staticData = $getWorkflowStaticData('global');\n\nif (!staticData.previousData) {\n  staticData.previousData = {};\n}\n\n// 2. Ambil data input menggunakan $input.all()\n//const inputItems = $input.all();\nconst inputItems = $('Get rows').all();\n\n\n// Convert current data to object keyed by ID\nconst currentData = {};\nfor (const item of inputItems) {\n  // Pastikan field 'ID' ada di data kamu\n  if (item.json.ID) {\n    currentData[item.json.ID] = item.json;\n  }\n}\n\nconst changes = [];\n\n// Detect New and Updated rows\nfor (const id in currentData) {\n  const currentRow = currentData[id];\n  const previousRow = staticData.previousData[id];\n\n  if (!previousRow) {  \n    const newFields = [];\n\n    for (const key in currentRow) {\n      newFields.push({\n        source: currentRow,\n        field: key,\n        old: null,\n        new: currentRow[key]\n      });\n    }\n    \n    changes.push({\n      type: \"NEW\",\n      id,\n      changes: newFields\n    });\n    continue;\n  }\n\n  const rowChanges = [];\n  let hasChanged = false;\n  const config = $input.first().json.CONFIG;\n  \n  for (const key in currentRow) {\n    \n    if (key === config.idField) continue;\n    if (config.ignoreFields.includes(key)) continue;\n    if (config.monitorOnly && !config.monitorOnly.includes(key)) continue;\n    \n    if (JSON.stringify(currentRow[key]) !== JSON.stringify(previousRow[key])) {\n      rowChanges.push({\n        source: currentRow,\n        field: key,\n        old: previousRow[key],\n        new: currentRow[key]\n      });\n      hasChanged = true;\n    }\n  }\n  \n  if (hasChanged) {\n    changes.push({\n      type: \"UPDATED\",\n      id,\n      changes: rowChanges\n    });\n  }\n  \n}\n\n// Detect Deleted rows\nfor (const id in staticData.previousData) {\n  if (!currentData[id]) {\n    changes.push({\n      type: \"DELETED\",\n      id\n    });\n  }\n}\n\n// 3. Simpan snapshot untuk run berikutnya\nstaticData.previousData = currentData;\n\n// Kembalikan hasil sebagai array item n8n\nreturn [{\n  json: {\n    hasChanges: changes.length > 0,\n    totalChanges: changes.length,\n    changes\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "c78ddfa5-c614-4f68-bb49-ce4f9d57b6bd",
      "name": "Transform changes for notification",
      "type": "n8n-nodes-base.code",
      "position": [
        176,
        432
      ],
      "parameters": {
        "jsCode": "let message = \"\ud83d\udcca Excel Update Detected\\n\\n\";\nlet sendMessage = false;\nfor (const change of $json.changes) {\n\n  if (change.type === \"NEW\") {\n    message += `\ud83c\udd95 New Row Added (ID: ${change.id})\\n`;\n    \n    if (Array.isArray(change.changes)) {\n      for (const fieldChange of change.changes) {\n        message += `- ${fieldChange.field}: ${fieldChange.new}\\n`;\n      }\n    }\n    sendMessage=true;\n    message += \"\\n\";\n  }\n\n  if (change.type === \"UPDATED\") {\n    message += `\u270f\ufe0f Row Updated (ID: ${change.id})\\n`;\n\n    if (Array.isArray(change.changes)) {\n      for (const fieldChange of change.changes) {\n        message += `- ${fieldChange.field}: ${fieldChange.old} \u2192 ${fieldChange.new}\\n`;\n      }\n    }\n\n    message += \"\\n\";\n    sendMessage=true;\n  }\n\n  if (change.type === \"DELETED\") {\n    message += `\u274c Row Deleted (ID: ${change.id})\\n\\n`;\n    sendMessage=true;\n  }\n}\n\nreturn [{\n  json: {\n    message,\n    sendMessage\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "c467f737-037f-4e4b-9a07-7b7e31244b52",
      "name": "Build audit log rows",
      "type": "n8n-nodes-base.code",
      "position": [
        144,
        896
      ],
      "parameters": {
        "jsCode": "const auditRows = [];\nfor (const change of $json.changes) {\n  if (change.type === \"DELETED\") {\n    auditRows.push({\n      Timestamp: new Date().toISOString(),\n      ChangeType: \"DELETED\",\n      RowID: change.id,\n      Field: null,\n      OldValue: null,\n      NewValue: null\n    });\n    continue;\n  }\n\n  for (const fieldChange of change.changes) {\n    auditRows.push({\n      Timestamp: new Date().toISOString(),\n      ChangeType: change.type,\n      RowID: change.id,\n      Field: fieldChange.field,\n      OldValue: fieldChange.old ?? null,\n      NewValue: fieldChange.new ?? null\n    });\n  }\n}\n\nreturn auditRows.map(r => ({ json: r }));"
      },
      "typeVersion": 2
    },
    {
      "id": "4ccfc3ae-f8de-4eb2-afd1-d46b52a6d2a2",
      "name": "Filter rows with waiting approval",
      "type": "n8n-nodes-base.code",
      "position": [
        672,
        544
      ],
      "parameters": {
        "jsCode": "// check apakah ada yang waiting approval\nlet change_message = [];\nfor (const change of $json.changes) {\n  for (const fieldChange of change.changes) {\n    if(fieldChange.field=='Status' && fieldChange.new == 'Waiting Approval'){\n      change_message.push(fieldChange.source); \n    }\n  }\n}\n\n// Kembalikan setiap item di dalam 'changes' sebagai item n8n yang terpisah\nreturn change_message.map(change => {\n  return {\n    // json: change\n    json : change\n  };\n});\n"
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "28a56211-bfc7-49bc-b431-a4bb0fab7e30",
      "name": "Send notification",
      "type": "n8n-nodes-base.telegram",
      "position": [
        384,
        432
      ],
      "parameters": {
        "text": "={{ $json.message }}",
        "chatId": "123456789",
        "forceReply": {},
        "replyMarkup": "forceReply",
        "additionalFields": {}
      },
      "credentials": {
        "telegramApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "c8c5eb0f-d58a-47c6-898f-fe462713a0be",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -720,
        -592
      ],
      "parameters": {
        "color": 3,
        "width": 576,
        "height": 384,
        "content": "## How it works\n1. Runs every minute using a schedule trigger\n2. Reads rows from Excel 365\n3. Normalizes and stores a snapshot\n4. Compares with the previous state\n5. Detects new, updated, and deleted rows\n6. Filters rows with \u201cWaiting Approval\u201d status\n7. Sends structured notifications\n8. Logs changes if audit logging is enabled\n"
      },
      "typeVersion": 1
    },
    {
      "id": "f7606576-0cec-4113-90e0-6c76c259d904",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -720,
        -176
      ],
      "parameters": {
        "color": 5,
        "width": 560,
        "height": 288,
        "content": "## Setup steps\n1. Configure Microsoft Excel credentials\n2. Ensure your sheet contains a unique ID column\n3. Update the Environment Config node\n4.(Optional) Configure Google Sheets credentials for audit logging\n5. Activate the workflow"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "19874e82-7560-42af-b0c1-9fa47f43cbeb",
  "connections": {
    "Get rows": {
      "main": [
        [
          {
            "node": "Normalize Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Data": {
      "main": [
        [
          {
            "node": "Environment Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send notification": {
      "main": [
        []
      ]
    },
    "Environment Config": {
      "main": [
        [
          {
            "node": "Compare previous and current snapshot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run every 1 minute": {
      "main": [
        [
          {
            "node": "Get rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build audit log rows": {
      "main": [
        [
          {
            "node": "Append Log to Excel",
            "type": "main",
            "index": 0
          },
          {
            "node": "Append Log to Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if changes exist": {
      "main": [
        [
          {
            "node": "Transform changes for notification",
            "type": "main",
            "index": 0
          },
          {
            "node": "Check if audit logging is enabled",
            "type": "main",
            "index": 0
          },
          {
            "node": "Filter rows with waiting approval",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if audit logging is enabled": {
      "main": [
        [
          {
            "node": "Build audit log rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter rows with waiting approval": {
      "main": [
        [
          {
            "node": "Notification Approval",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Transform changes for notification": {
      "main": [
        [
          {
            "node": "Send notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compare previous and current snapshot": {
      "main": [
        [
          {
            "node": "Check if changes exist",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

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.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow monitors an Excel 365 sheet every minute and detects new, updated, and deleted rows using a unique ID column. It compares the current dataset with the previous snapshot and identifies field-level differences.

Source: https://n8n.io/workflows/13716/ — original creator credit. Request a take-down →

More Slack & Telegram workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Slack & Telegram

This workflow continuously monitors the TikTok Ads Library for new creatives from specific advertisers or keyword searches, scrapes them via Apify, logs them into Google Sheets, and sends concise noti

Google Sheets, Slack, Telegram +1
Slack & Telegram

This workflow automates plant care reminders and records using Google Sheets, Telegram, and OpenWeather API.

Google Sheets, HTTP Request, Telegram
Slack & Telegram

Apollo Data Enrichment Using Company Id to automatically finds contacts for companies listed in your Google Sheet, enriches each person with emails and phone numbers via Apollo’s API, and writes verif

Google Sheets, HTTP Request, Error Trigger +1
Slack & Telegram

++Download the google sheet here++ and replace this with the googles sheet node: Google sheet , upload to google sheets and replace in the google sheets node. Scheduled trigger: Runs once a day at 8 A

Google Sheets, HTTP Request, Telegram
Slack & Telegram

YT AI News Playlist Creator/AI News Form Updater. Uses googleSheets, httpRequest, splitOut, stickyNote. Scheduled trigger; 23 nodes.

Google Sheets, HTTP Request, YouTube +1