image.png

n8n-Workflow

{
  "name": "Google Ads Daily Digest",
  "nodes": [
    {
      "parameters": {
        "jsCode": "// 1. INPUT DATEN LADEN\nconst inventoryRaw = $('Get Inventory').all().map(item => item.json);\nconst logsRaw = $('Get Logs').all().map(item => item.json);\n\n// 2. EINSTELLUNGEN\nconst now = new Date();\nconst msPerHour = 60 * 60 * 1000;\nconst msPerMinute = 60 * 1000;\nconst lookbackWindow = new Date(now.getTime() - (24 * msPerHour)); \nconst bufferMinutes = 30;\n\nconst formatTime = (dateObj) => {\n  return dateObj.toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });\n};\n\n// ---------------------------------------------------------\n// SCHRITT 1: DIE WIRKLICH LETZTEN ZEITSTEMPEL AUS DEN LOGS HOLEN\n// ---------------------------------------------------------\n// Wir erstellen ein Map, um für jedes Skript den neuesten Log-Eintrag zu finden\nconst realLastRuns = {};\n\nfor (const log of logsRaw) {\n  const sName = log['Script Name'] || log['Script_Name'] || 'Unknown';\n  const aID = log['Account ID'] || log['Account_ID'] || 'N/A';\n  const logTime = new Date(log.Timestamp);\n  \n  if (isNaN(logTime.getTime())) continue;\n\n  const key = `${sName}-${aID}`;\n  \n  // Wenn wir noch keinen Eintrag haben oder dieser Log-Eintrag neuer ist\n  if (!realLastRuns[key] || logTime > realLastRuns[key]) {\n    realLastRuns[key] = logTime;\n  }\n}\n\n// ---------------------------------------------------------\n// SCHRITT 2: WATCHDOG (Prüfung mit abgeglichenen Daten)\n// ---------------------------------------------------------\nlet overdueScripts = [];\n\nfor (const script of inventoryRaw) {\n  const isActive = String(script.Active).toUpperCase() === 'TRUE';\n  if (!isActive) continue;\n\n  const sName = script.Script_Name;\n  const aID = script.Account_ID;\n  const key = `${sName}-${aID}`;\n\n  // WICHTIG: Wir nehmen den Zeitstempel aus dem Log, falls vorhanden. \n  // Ansonsten den aus dem Inventory.\n  let lastRunDate = new Date(script.Last_Run_Timestamp);\n  if (realLastRuns[key] && realLastRuns[key] > lastRunDate) {\n    lastRunDate = realLastRuns[key];\n  }\n\n  if (isNaN(lastRunDate.getTime())) continue;\n\n  const freqHours = parseFloat(script.Expected_Frequency_Hours) || 24;\n  const deadlineMs = lastRunDate.getTime() + (freqHours * msPerHour) + (bufferMinutes * msPerMinute);\n  const deadline = new Date(deadlineMs);\n\n  if (now > deadline) {\n    const diffMs = now - deadline;\n    overdueScripts.push({\n      name: sName,\n      account: aID,\n      hoursLate: Math.floor(diffMs / msPerHour),\n      minutesLate: Math.floor((diffMs % msPerHour) / msPerMinute)\n    });\n  }\n}\n\n// ---------------------------------------------------------\n// SCHRITT 3: LOGGING SUMMARY (Ergebnisse der letzten 24h)\n// ---------------------------------------------------------\nlet stats = { success: 0, error: 0, optimize: [], critical: [] };\n\nfor (const log of logsRaw) {\n  const logTime = new Date(log.Timestamp);\n  if (!isNaN(logTime.getTime()) && logTime >= lookbackWindow && logTime <= now) {\n    const status = (log.Status || '').toUpperCase().trim();\n    if (status === 'SUCCESS') stats.success++;\n    else if (status === 'ERROR') stats.error++;\n    else if (status === 'OPTIMIZE') stats.optimize.push(log);\n    else if (status === 'CRITICAL') stats.critical.push(log);\n  }\n}\n\n// ---------------------------------------------------------\n// SCHRITT 4: SLACK MESSAGE\n// ---------------------------------------------------------\nlet msg = `---\\n🌅 *Guten Morgen! Dein Google Ads Daily Digest*\\n_Zeitraum: Letzte 24 Stunden (bis ${formatTime(now)})_\\n\\n`;\n\nmsg += `💀 *Watchdog Alarm (Überfällige Skripte):*\\n`;\nif (overdueScripts.length === 0) {\n  msg += `✅ Alle aktiven Skripte laufen nach Plan.\\n`;\n} else {\n  overdueScripts.forEach(item => {\n    msg += `- [Warnung] *${item.name}* (${item.account}) überfällig seit ${item.hoursLate} Std. ${item.minutesLate} Min.\\n`;\n  });\n}\n\nmsg += `\\n📊 *Rückblick (Performance):*\\n`;\nmsg += `- ${stats.success}x SUCCESS\\n`;\nmsg += `- ${stats.error}x ERROR\\n`;\n\nif (stats.critical.length > 0 || stats.optimize.length > 0) {\n  msg += `\\n🚀 *Handlung empfohlen (Highlights):*\\n`;\n  const printHighlight = (h, prefix) => {\n    const sName = h['Script Name'] || h['Script_Name'] || 'Unbekannt';\n    const aID = h['Account ID'] || h['Account_ID'] || 'N/A';\n    const message = h.Message || 'Keine Nachricht';\n    msg += `${prefix} *${sName}* (${aID})\\n    👉 ${message}\\n`;\n  };\n  stats.critical.forEach(h => printHighlight(h, '🔴 *CRITICAL:*'));\n  stats.optimize.forEach(h => printHighlight(h, '💡'));\n}\n\nmsg += `\\n---`;\n\nreturn [{ json: { slackMessage: msg } }];"
      },
      "id": "5f1b6594-6cf8-4be7-af80-bddb9a60d108",
      "name": "Process Daily Digest",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        256,
        0
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 7
            }
          ]
        }
      },
      "id": "2ef0fcb2-fe38-47e4-bf87-b018b98c92c9",
      "name": "Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.1,
      "position": [
        -416,
        0
      ]
    },
    {
      "parameters": {
        "authentication": "oAuth2",
        "select": "user",
        "user": {
          "__rl": true,
          "value": "DEINE_SLACK_USER_ID_HIER",
          "mode": "list",
          "cachedResultName": "dein.slack.username"
        },
        "text": "={{ $json.slackMessage }}",
        "otherOptions": {
          "mrkdwn": true
        }
      },
      "id": "c096c585-6f14-45a0-8f8d-61f379348963",
      "name": "Slack",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.1,
      "position": [
        480,
        0
      ],
      "webhookId": "DEINE_SLACK_WEBHOOK_ID_HIER",
      "credentials": {
        "slackOAuth2Api": {
          "id": "DEINE_SLACK_CREDENTIAL_ID_HIER",
          "name": "Slack account"
        }
      },
      "notes": "Hier deine Credentials einfügen"
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "DEINE_SHEET_ID_HIER",
          "mode": "list",
          "cachedResultName": "Dein n8n Script Logging Sheet",
          "cachedResultUrl": "<https://docs.google.com/spreadsheets/d/DEINE_SHEET_ID_HIER/edit?usp=drivesdk>"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list",
          "cachedResultName": "Logging",
          "cachedResultUrl": "<https://docs.google.com/spreadsheets/d/DEINE_SHEET_ID_HIER/edit#gid=0>"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -192,
        96
      ],
      "id": "2ed97d7b-b94b-4799-a4e4-77c5279890bd",
      "name": "Get Logs",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "DEINE_GSHEETS_CREDENTIAL_ID_HIER",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {
        "documentId": {
          "__rl": true,
          "value": "DEINE_SHEET_ID_HIER",
          "mode": "list",
          "cachedResultName": "Dein n8n Script Logging Sheet",
          "cachedResultUrl": "<https://docs.google.com/spreadsheets/d/DEINE_SHEET_ID_HIER/edit?usp=drivesdk>"
        },
        "sheetName": {
          "__rl": true,
          "value": 522954319,
          "mode": "list",
          "cachedResultName": "Inventory",
          "cachedResultUrl": "<https://docs.google.com/spreadsheets/d/DEINE_SHEET_ID_HIER/edit#gid=522954319>"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        -192,
        -96
      ],
      "id": "c539dfce-44aa-458a-aad9-850d878ae3e5",
      "name": "Get Inventory",
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "DEINE_GSHEETS_CREDENTIAL_ID_HIER",
          "name": "Google Sheets account"
        }
      }
    },
    {
      "parameters": {},
      "type": "n8n-nodes-base.merge",
      "typeVersion": 3.2,
      "position": [
        32,
        0
      ],
      "id": "8db820ce-ba99-4c1a-8fa5-5c861b7e15de",
      "name": "Merge"
    }
  ],
  "pinData": {},
  "connections": {
    "Schedule Trigger": {
      "main": [
        [
          {
            "node": "Get Inventory",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get Logs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Daily Digest": {
      "main": [
        [
          {
            "node": "Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Logs": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Get Inventory": {
      "main": [
        [
          {
            "node": "Merge",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge": {
      "main": [
        [
          {
            "node": "Process Daily Digest",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "availableInMCP": false
  },
  "versionId": "PLATZHALTER_VERSION_ID",
  "meta": {
    "templateCredsSetupCompleted": true,
    "instanceId": "DEINE_N8N_INSTANCE_ID_HIER"
  },
  "id": "PLATZHALTER_WORKFLOW_ID",
  "tags": []
}

Beispiel Tabellen:

Get Inventory

image.png

Get Logs:

image.png