AutomationFlowsSlack & Telegram › Send Daily SEO Digest From Google Search Console to Discord with Heartbeat…

Send Daily SEO Digest From Google Search Console to Discord with Heartbeat…

Original n8n title: Send Daily SEO Digest From Google Search Console to Discord with Heartbeat Alerts

ByWongsakon H. @checkilo on n8n.io

This workflow runs daily to pull Google Search Console performance data, log it to Google Sheets, send a daily SEO digest to Discord with day-over-day deltas, and ping an external heartbeat endpoint; if any step errors, it immediately pings the heartbeat service’s /fail…

Cron / scheduled trigger★★★★☆ complexity16 nodesHTTP RequestGoogle SheetsDiscordError Trigger
Slack & Telegram Trigger: Cron / scheduled Nodes: 16 Complexity: ★★★★☆ Added:

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

This workflow follows the Discord → HTTP Request 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
{
  "name": "Daily SEO digest + silent-failure alert (Google Search Console)",
  "nodes": [
    {
      "id": "ac68ca1c-101a-46fb-969c-46fbdbeccea2",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -352,
        208
      ],
      "parameters": {
        "width": 480,
        "height": 864,
        "content": "## Daily SEO digest + silent-failure alert (Google Search Console)\n\n### How it works\n\nThis workflow runs every morning to pull Google Search Console performance data, aggregate it, and append the daily metrics to a Google Sheet. It then reads recent sheet rows, compares the latest metrics with yesterday\u2019s values, and posts a digest to Discord. After a successful digest it sends a heartbeat ping, while a separate error path sends a failure ping if the workflow breaks silently.\n\n### Setup steps\n\n- Configure the daily schedule trigger for the desired local timezone and run time.\n- Set up Google Search Console API access in the HTTP Request node, including OAuth credentials and the correct verified site/property URL.\n- Configure the Google Sheets nodes with the target spreadsheet, sheet/tab, and matching columns for the aggregated SEO metrics.\n- Add the Discord credential or webhook/channel settings for the digest message.\n- Replace the Checkilo heartbeat and failure ping URLs with the correct monitor endpoints.\n\n### Customization\n\nAdjust the aggregation code, comparison thresholds, Discord message format, and the date range used for Google Search Console data to match the SEO metrics you want to track."
      },
      "typeVersion": 1
    },
    {
      "id": "94ebf0a7-24af-4341-86c8-0cef024937a7",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        208,
        208
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 320,
        "content": "## Schedule and fetch data\n\nStarts the workflow each morning and requests the latest Google Search Console data from the API."
      },
      "typeVersion": 1
    },
    {
      "id": "58ba758d-85f3-40d3-acba-f9eea2c5faf5",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        640,
        208
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 320,
        "content": "## Aggregate and store metrics\n\nTransforms the raw Search Console response into daily summary metrics and appends them to the tracking Google Sheet."
      },
      "typeVersion": 1
    },
    {
      "id": "ce371f83-6bdf-4d28-b435-f04b30fe71c9",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1072,
        208
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 320,
        "content": "## Read and compare history\n\nReads stored sheet rows and calculates the change between the latest daily metrics and yesterday\u2019s values."
      },
      "typeVersion": 1
    },
    {
      "id": "6435f94b-f390-48d3-b833-460924c2d3fe",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1504,
        208
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Send digest heartbeat\n\nPosts the SEO digest to Discord, then sends a success heartbeat ping to confirm the daily workflow completed."
      },
      "typeVersion": 1
    },
    {
      "id": "ace364a8-08ef-42bb-99c7-f193406a6188",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        608,
        560
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 320,
        "content": "## Error failure alert\n\nSeparate lower-canvas error branch that triggers on workflow failures and sends a failure ping to the monitoring endpoint."
      },
      "typeVersion": 1
    },
    {
      "id": "node-schedule",
      "name": "When Every Day at 08:00",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        260,
        360
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "node-gsc",
      "name": "Post to GSC API",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        460,
        360
      ],
      "parameters": {
        "url": "https://www.googleapis.com/webmasters/v3/sites/REPLACE_WITH_URL_ENCODED_SITE/searchAnalytics/query",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"startDate\": \"{{ $today.minus({ days: 2 }).toFormat('yyyy-MM-dd') }}\",\n  \"endDate\": \"{{ $today.minus({ days: 2 }).toFormat('yyyy-MM-dd') }}\",\n  \"dimensions\": [\"query\"],\n  \"rowLimit\": 100\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleOAuth2Api"
      },
      "typeVersion": 4.2
    },
    {
      "id": "node-aggregate",
      "name": "Aggregate GSC Data",
      "type": "n8n-nodes-base.code",
      "position": [
        688,
        368
      ],
      "parameters": {
        "jsCode": "const rows = ($input.first().json.rows) || [];\nlet clicks = 0, impressions = 0, posWeighted = 0;\nfor (const r of rows) {\n  clicks += r.clicks;\n  impressions += r.impressions;\n  posWeighted += r.position * r.impressions;\n}\nconst avgPosition = impressions ? (posWeighted / impressions) : 0;\nconst topQueries = rows\n  .slice()\n  .sort((a, b) => (b.clicks - a.clicks) || (b.impressions - a.impressions))\n  .slice(0, 5)\n  .map(r => r.keys[0]);\nconst date = $today.minus({ days: 2 }).toFormat('yyyy-MM-dd');\nreturn [{ json: {\n  date,\n  clicks,\n  impressions,\n  avgPosition: Math.round(avgPosition * 10) / 10,\n  topQueries: topQueries.join(', ')\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "node-sheet",
      "name": "Append to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        896,
        368
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [],
          "mappingMode": "autoMapInputData",
          "matchingColumns": []
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "node-getrows",
      "name": "Read Google Sheets Data",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1120,
        368
      ],
      "parameters": {
        "options": {},
        "operation": "read",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "REPLACE_WITH_SHEET_ID"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "node-delta",
      "name": "Compare With Previous Day",
      "type": "n8n-nodes-base.code",
      "position": [
        1328,
        368
      ],
      "parameters": {
        "jsCode": "const today = $('Aggregate GSC Data').first().json;\nconst rows = $input.all().map(i => i.json);\n// rows are oldest-first; the row we just appended is last, yesterday is second-to-last\nconst prev = rows.length >= 2 ? rows[rows.length - 2] : null;\nconst pct = (curr, oldVal) => {\n  const o = Number(oldVal);\n  if (prev === null || !o) return null;\n  return Math.round(((curr - o) / o) * 1000) / 10;\n};\nreturn [{ json: {\n  ...today,\n  clicksChange: pct(today.clicks, prev && prev.clicks),\n  imprChange: pct(today.impressions, prev && prev.impressions)\n} }];"
      },
      "typeVersion": 2
    },
    {
      "id": "node-discord",
      "name": "Send Digest to Discord",
      "type": "n8n-nodes-base.discord",
      "position": [
        1552,
        368
      ],
      "parameters": {
        "content": "=\ud83d\udcca SEO \u2014 {{ $json.date }}\nClicks: {{ $json.clicks }}{{ $json.clicksChange === null ? '' : ' (' + ($json.clicksChange >= 0 ? '+' : '') + $json.clicksChange + '% vs yesterday)' }}\nImpressions: {{ $json.impressions }}{{ $json.imprChange === null ? '' : ' (' + ($json.imprChange >= 0 ? '+' : '') + $json.imprChange + '%)' }}\nAvg position: {{ $json.avgPosition }}\nTop queries: {{ $json.topQueries }}{{ ($json.clicksChange !== null && $json.clicksChange <= -20) ? '\\n\\n\u26a0\ufe0f Clicks dropped ' + Math.abs($json.clicksChange) + '% vs yesterday \u2014 worth a look' : '' }}",
        "options": {},
        "authentication": "webhook"
      },
      "typeVersion": 2
    },
    {
      "id": "node-heartbeat",
      "name": "Send Heartbeat Ping",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1760,
        368
      ],
      "parameters": {
        "url": "https://ping.checkilo.app/REPLACE_WITH_YOUR_PING_SLUG",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"total_clicks\": {{ $('Aggregate GSC Data').first().json.clicks }},\n  \"total_impressions\": {{ $('Aggregate GSC Data').first().json.impressions }},\n  \"avg_position\": {{ $('Aggregate GSC Data').first().json.avgPosition }},\n  \"top_query\": {{ JSON.stringify(($('Aggregate GSC Data').first().json.topQueries || '').split(',')[0].trim()) }}\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "node-error-trigger",
      "name": "When Error Occurs",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        660,
        720
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "node-fail-ping",
      "name": "Post Error Ping Fail",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        880,
        720
      ],
      "parameters": {
        "url": "https://ping.checkilo.app/REPLACE_WITH_YOUR_PING_SLUG/fail",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"error\": {{ JSON.stringify($json.execution?.error?.message || 'unknown') }},\n  \"failed_node\": {{ JSON.stringify($json.execution?.lastNodeExecuted || 'unknown') }}\n}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "connections": {
    "Post to GSC API": {
      "main": [
        [
          {
            "node": "Aggregate GSC Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Error Occurs": {
      "main": [
        [
          {
            "node": "Post Error Ping Fail",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate GSC Data": {
      "main": [
        [
          {
            "node": "Append to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Digest to Discord": {
      "main": [
        [
          {
            "node": "Send Heartbeat Ping",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append to Google Sheets": {
      "main": [
        [
          {
            "node": "Read Google Sheets Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Google Sheets Data": {
      "main": [
        [
          {
            "node": "Compare With Previous Day",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "When Every Day at 08:00": {
      "main": [
        [
          {
            "node": "Post to GSC API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Compare With Previous Day": {
      "main": [
        [
          {
            "node": "Send Digest to Discord",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}
Pro

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

About this workflow

This workflow runs daily to pull Google Search Console performance data, log it to Google Sheets, send a daily SEO digest to Discord with day-over-day deltas, and ping an external heartbeat endpoint; if any step errors, it immediately pings the heartbeat service’s /fail…

Source: https://n8n.io/workflows/16624/ — 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 automated n8n workflow monitors real-time cryptocurrency prices using CoinGecko API and sends smart alerts when price conditions are met. It supports multi-coin tracking, dynamic conditions, and

Google Sheets, HTTP Request, Email Send +2
Slack & Telegram

⚠️ Heads up: this is satire. The "Hell Yeah!" workflow is a parody of "automate your whole life with AI agents" grindset content. The API endpoints are fictional and the function nodes are illustrativ

HTTP Request, Salesforce, Telegram +4
Slack & Telegram

This workflow tracks a configurable crypto watchlist using the CoinGecko API, sends Telegram alerts when price, % change, or volume-spike conditions are met (with optional RSI filtering), optionally l

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

This workflow continuously monitors the Meta Ads Library for new creatives from a specific competitor pages, logs them into Google Sheets, and sends a concise Telegram notification with the number of

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

Enhance financial oversight with this automated n8n workflow. Triggered every 5 minutes, it fetches real-time bank transactions via an API, enriches and transforms the data, and applies smart logic to

HTTP Request, Email Send, Google Sheets +1