AutomationFlowsSlack & Telegram › Monitor SEO Keyword Rankings with Serp API & Send Drop Alerts via Slack

Monitor SEO Keyword Rankings with Serp API & Send Drop Alerts via Slack

ByOneclick AI Squad @oneclick-ai on n8n.io

This automated workflow monitors your website's keyword rankings daily and sends instant alerts to your team when significant ranking drops occur. It fetches current ranking positions, compares them with historical data, and triggers notifications through Slack and email when…

Cron / scheduled trigger★★★★☆ complexity12 nodesGoogle SheetsHTTP RequestSlackEmail Send
Slack & Telegram Trigger: Cron / scheduled Nodes: 12 Complexity: ★★★★☆ Added:

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

This workflow follows the Emailsend → Google Sheets recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "id": "8Pj6PSyWQCOFASKY",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "SEO Keyword Rank Monitor with Drop Alerts for Your Team",
  "tags": [],
  "nodes": [
    {
      "id": "85f989d7-89a1-4f71-9156-fbedc02f3b1a",
      "name": "Daily SEO Check Trigger",
      "type": "n8n-nodes-base.cron",
      "notes": "\ud83d\udd50 DAILY SCHEDULER\n\nWhy this node?\n\u2022 Automatically runs SEO checks every day at 8 AM\n\u2022 Ensures consistent daily monitoring without manual intervention\n\u2022 Uses cron expression '0 8 * * *' for daily 8 AM execution\n\u2022 Essential for tracking keyword ranking changes over time\n\nWhat it does:\n\u2022 Triggers the entire SEO monitoring workflow\n\u2022 Runs Monday through Sunday automatically\n\u2022 Can be modified to run multiple times per day if needed",
      "position": [
        -1860,
        -140
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "7146f49a-fc4c-4d65-8d10-3f43c9d8f6b6",
      "name": "Get Keywords Database",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "\ud83d\udcca KEYWORD DATABASE\n\nWhy this node?\n\u2022 Stores all keywords to monitor in one central location\n\u2022 Easy to add/remove keywords without changing workflow\n\u2022 Maintains historical ranking data for comparison\n\u2022 Allows team collaboration on keyword list management\n\nWhat it does:\n\u2022 Reads keywords, target URLs, and previous rankings\n\u2022 Provides data structure: keyword, url, previous_rank, current_rank\n\u2022 Acts as the source of truth for all SEO monitoring\n\u2022 Enables bulk processing of multiple keywords",
      "position": [
        -1640,
        -140
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "=54refghuy654e3w"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "23456yujhyt543erfgb"
        },
        "authentication": "serviceAccount",
        "combineFilters": "AND"
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4
    },
    {
      "id": "823ab7be-aa9f-45c8-884e-cb3e54a0764c",
      "name": "Filter Active Keywords Only",
      "type": "n8n-nodes-base.filter",
      "notes": "\ud83d\udd0d KEYWORD FILTER\n\nWhy this node?\n\u2022 Saves API costs by only checking active keywords\n\u2022 Prevents processing empty or disabled keyword entries\n\u2022 Allows temporary disabling of specific keyword monitoring\n\u2022 Improves workflow efficiency and performance\n\nWhat it does:\n\u2022 Filters out empty keyword fields\n\u2022 Only processes keywords marked as 'monitor_active = true'\n\u2022 Reduces unnecessary API calls to SERP services\n\u2022 Ensures clean data flow to ranking APIs",
      "position": [
        -1420,
        -140
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "condition-1",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              },
              "leftValue": "={{ $json.keyword }}",
              "rightValue": ""
            },
            {
              "id": "condition-2",
              "operator": {
                "type": "string",
                "operation": "equals"
              },
              "leftValue": "={{ $json.monitor_active }}",
              "rightValue": "true"
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "c3157af6-3541-44f3-bde6-f05788aede5a",
      "name": "Fetch Google Rankings via SERP API",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "\ud83c\udf10 GOOGLE RANKING FETCHER\n\nWhy this node?\n\u2022 Gets real-time Google search rankings for keywords\n\u2022 Uses SERP API to avoid Google blocking/rate limiting\n\u2022 Fetches top 100 results to catch ranking changes\n\u2022 Provides accurate, location-specific ranking data\n\nWhat it does:\n\u2022 Searches Google for each keyword\n\u2022 Returns organic search results with positions\n\u2022 Captures ranking data for specified location (US)\n\u2022 Handles Google's anti-bot measures automatically\n\u2022 Costs ~$0.001 per search (check SERP API pricing)",
      "position": [
        -1200,
        -140
      ],
      "parameters": {
        "url": "https://serpapi.com/search",
        "options": {},
        "sendQuery": true,
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "queryParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ $json.keyword }}"
            },
            {
              "name": "location",
              "value": "United States"
            },
            {
              "name": "hl",
              "value": "en"
            },
            {
              "name": "gl",
              "value": "us"
            },
            {
              "name": "google_domain",
              "value": "google.com"
            },
            {
              "name": "api_key",
              "value": "YOUR_SERPAPI_KEY"
            },
            {
              "name": "num",
              "value": "100"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "ca3e0cb8-9772-416b-8dd0-4f4beb80b87a",
      "name": "Parse Rankings & Detect Changes",
      "type": "n8n-nodes-base.code",
      "notes": "\u2699\ufe0f RANKING ANALYZER\n\nWhy this node?\n\u2022 Processes complex SERP API responses into usable data\n\u2022 Finds your website's position in search results\n\u2022 Calculates ranking changes compared to previous data\n\u2022 Determines which keywords need attention/alerts\n\nWhat it does:\n\u2022 Searches through 100 organic results for your URL\n\u2022 Matches domain/subdomain to find exact ranking\n\u2022 Calculates ranking improvement/drop numbers\n\u2022 Sets alert flags for drops > 2 positions\n\u2022 Handles cases where website is not found in top 100",
      "position": [
        -760,
        -140
      ],
      "parameters": {
        "jsCode": "// Parse SERP API response and find our website rankings\nconst items = [];\n\nfor (const item of $input.all()) {\n  const serpData = item.json;\n  const keyword = item.json.keyword || 'Unknown';\n  const targetUrl = item.json.url || '';\n  const previousRank = parseInt(item.json.previous_rank) || 0;\n  \n  let currentRank = null;\n  let foundUrl = '';\n  \n  // Search through organic results\n  if (serpData.organic_results && Array.isArray(serpData.organic_results)) {\n    for (let i = 0; i < serpData.organic_results.length; i++) {\n      const result = serpData.organic_results[i];\n      const resultUrl = result.link || '';\n      \n      // Check if this result matches our target URL (domain matching)\n      if (targetUrl && resultUrl.includes(targetUrl.replace('https://', '').replace('http://', '').split('/')[0])) {\n        currentRank = result.position || (i + 1);\n        foundUrl = resultUrl;\n        break;\n      }\n    }\n  }\n  \n  // Calculate ranking change\n  const rankingChange = previousRank && currentRank ? (previousRank - currentRank) : 0;\n  const rankingDrop = currentRank && previousRank ? (currentRank - previousRank) : 0;\n  \n  // Determine status\n  let status = 'not_found';\n  if (currentRank) {\n    if (rankingChange > 0) status = 'improved';\n    else if (rankingChange < 0) status = 'dropped';\n    else status = 'stable';\n  }\n  \n  items.push({\n    json: {\n      keyword: keyword,\n      target_url: targetUrl,\n      current_rank: currentRank,\n      previous_rank: previousRank,\n      ranking_change: rankingChange,\n      ranking_drop: rankingDrop,\n      status: status,\n      found_url: foundUrl,\n      check_date: new Date().toISOString().split('T')[0],\n      needs_alert: rankingDrop > 2 || currentRank === null\n    }\n  });\n}\n\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "7615956e-9f02-4e10-b4f0-986439f18c10",
      "name": "Filter Significant Ranking Drops",
      "type": "n8n-nodes-base.filter",
      "notes": "\ud83d\udea8 ALERT FILTER\n\nWhy this node?\n\u2022 Only sends alerts for significant ranking changes\n\u2022 Prevents spam from minor 1-2 position fluctuations\n\u2022 Focuses attention on keywords that truly need action\n\u2022 Saves notification costs and reduces alert fatigue\n\nWhat it does:\n\u2022 Filters keywords with drops > 2 positions\n\u2022 Includes keywords that disappeared from top 100\n\u2022 Passes only actionable ranking issues\n\u2022 Reduces noise in alerting system",
      "position": [
        -540,
        -240
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "condition-1",
              "operator": {
                "type": "boolean",
                "operation": "equals"
              },
              "leftValue": "={{ $json.needs_alert }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2
    },
    {
      "id": "18daa200-2105-48e7-b624-571afb9e8b56",
      "name": "Update Rankings in Google Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "\ud83d\udcc8 RANKING HISTORY TRACKER\n\nWhy this node?\n\u2022 Maintains historical ranking data for trend analysis\n\u2022 Updates previous_rank for next day's comparison\n\u2022 Creates audit trail of all ranking changes\n\u2022 Enables manual review and reporting\n\nWhat it does:\n\u2022 Updates current rankings in Google Sheet\n\u2022 Records ranking changes and status\n\u2022 Timestamps each check for historical tracking\n\u2022 Stores found URLs for verification\n\u2022 Prepares data for next day's comparison",
      "position": [
        -320,
        60
      ],
      "parameters": {
        "columns": {
          "value": {},
          "schema": [],
          "mappingMode": "autoMapInputData",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "id",
          "value": "=234567uijhgfds"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "234y"
        },
        "authentication": "serviceAccount"
      },
      "credentials": {
        "googleApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4
    },
    {
      "id": "317313f3-5cc6-472b-b1c4-4b020ae1a1ed",
      "name": "Send Slack Ranking Alert",
      "type": "n8n-nodes-base.slack",
      "notes": "\ud83d\udcac SLACK NOTIFICATION\n\nWhy this node?\n\u2022 Provides instant alerts to SEO team\n\u2022 Rich formatting shows ranking details clearly\n\u2022 Integrates with existing team communication\n\u2022 Allows immediate team discussion and action\n\nWhat it does:\n\u2022 Sends formatted alert to designated Slack channel\n\u2022 Includes keyword, ranking change, and URLs\n\u2022 Shows current vs previous ranking comparison\n\u2022 Provides actionable information for quick response\n\u2022 Can mention specific team members if needed",
      "position": [
        -320,
        -340
      ],
      "parameters": {
        "text": "\ud83d\udea8 *SEO RANKING ALERT* \ud83d\udea8\n\n*Keyword*: {{ $json.keyword }}\n*Current Rank*: {{ $json.current_rank || 'Not Found (>100)' }}\n*Previous Rank*: {{ $json.previous_rank }}\n*Change*: {{ $json.ranking_drop > 0 ? '\ud83d\udcc9 Dropped ' + $json.ranking_drop + ' positions' : '\u2753 Disappeared from top 100' }}\n*Target URL*: {{ $json.target_url }}\n*Date*: {{ $json.check_date }}\n\n{{ $json.current_rank ? '*Found URL*: ' + $json.found_url : '*Action Needed*: Check if page still exists or investigate technical issues' }}\n\n*Status*: {{ $json.status.toUpperCase() }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "C1234567890"
        },
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "16d01b26-bb28-4f81-ab7b-560e199b4a86",
      "name": "Send Email Ranking Alert",
      "type": "n8n-nodes-base.emailSend",
      "notes": "\ud83d\udce7 EMAIL NOTIFICATION\n\nWhy this node?\n\u2022 Provides detailed alert for serious ranking drops\n\u2022 Creates email trail for management reporting\n\u2022 Reaches team members who may not use Slack\n\u2022 Professional format suitable for stakeholder updates\n\nWhat it does:\n\u2022 Sends HTML-formatted email with ranking details\n\u2022 Includes actionable recommendations\n\u2022 Shows before/after ranking comparison\n\u2022 Provides links for immediate investigation\n\u2022 Can be forwarded to management or clients",
      "position": [
        -320,
        -140
      ],
      "parameters": {
        "options": {},
        "subject": "\ud83d\udea8 SEO Alert: {{ $json.keyword }} ranking dropped",
        "toEmail": "user@example.com",
        "fromEmail": "user@example.com"
      },
      "credentials": {
        "smtp": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "f341f2b0-3aa7-4e32-b166-13d503f24c33",
      "name": "Generate SEO Monitoring Summary",
      "type": "n8n-nodes-base.code",
      "notes": "\ud83d\udcca EXECUTION SUMMARY\n\nWhy this node?\n\u2022 Provides overview of daily SEO monitoring results\n\u2022 Tracks workflow performance and success metrics\n\u2022 Helps identify trends in keyword performance\n\u2022 Useful for reporting and debugging\n\nWhat it does:\n\u2022 Counts total keywords processed\n\u2022 Summarizes alerts sent and ranking changes\n\u2022 Calculates average ranking across all keywords\n\u2022 Logs detailed execution summary to console\n\u2022 Creates data for dashboard reporting\n\u2022 Confirms workflow completed successfully",
      "position": [
        -100,
        -140
      ],
      "parameters": {
        "jsCode": "// Generate comprehensive SEO monitoring summary\nconst allItems = $input.all();\nconst processedKeywords = allItems.length;\nconst alertsSent = allItems.filter(item => item.json.needs_alert).length;\nconst improvements = allItems.filter(item => item.json.status === 'improved').length;\nconst drops = allItems.filter(item => item.json.status === 'dropped').length;\nconst stable = allItems.filter(item => item.json.status === 'stable').length;\nconst notFound = allItems.filter(item => item.json.status === 'not_found').length;\n\n// Calculate average ranking\nconst rankedKeywords = allItems.filter(item => item.json.current_rank);\nconst avgRanking = rankedKeywords.length > 0 ? \n  (rankedKeywords.reduce((sum, item) => sum + item.json.current_rank, 0) / rankedKeywords.length).toFixed(1) : 'N/A';\n\n// Log detailed summary\nconsole.log('=== SEO MONITORING SUMMARY ===');\nconsole.log(`Execution Date: ${new Date().toISOString()}`);\nconsole.log(`Total Keywords Processed: ${processedKeywords}`);\nconsole.log(`Alerts Triggered: ${alertsSent}`);\nconsole.log(`Ranking Status:`);\nconsole.log(`  - Improved: ${improvements}`);\nconsole.log(`  - Dropped: ${drops}`);\nconsole.log(`  - Stable: ${stable}`);\nconsole.log(`  - Not Found: ${notFound}`);\nconsole.log(`Average Ranking: ${avgRanking}`);\nconsole.log('================================');\n\nreturn [{\n  json: {\n    execution_summary: {\n      date: new Date().toISOString(),\n      keywords_processed: processedKeywords,\n      alerts_sent: alertsSent,\n      improvements: improvements,\n      drops: drops,\n      stable: stable,\n      not_found: notFound,\n      average_ranking: avgRanking,\n      success: true\n    }\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "bd06a584-1291-4918-a0bf-cd2e49c548d2",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1720,
        -540
      ],
      "parameters": {
        "color": 6,
        "width": 800,
        "height": 320,
        "content": "## How it works\n* **Daily SEO Check Trigger** initiates the workflow on a scheduled basis\n* **Get Keywords Database** retrieves your keyword list and current ranking data\n* **Filter Active Keywords Only** processes only keywords marked as active for monitoring\n* **Fetch Google Rankings via SERP API** gets current ranking positions for each keyword\n* **Wait For Response** Wait for gets current ranking positions\n* **Parse Rankings & Detect Changes** compares new rankings with historical data and identifies significant drops\n* **Filter Significant Ranking Drops** isolates keywords that dropped beyond your threshold (e.g., 5+ positions)\n* **Send Slack Ranking Alert** notifies your team channel about ranking drops\n* **Send Email Ranking Alert** sends detailed email reports to stakeholders\n* **Update Rankings in Google Sheet** saves new ranking data for historical tracking\n* **Generate SEO Monitoring Summary** creates a comprehensive report of all ranking changes"
      },
      "typeVersion": 1
    },
    {
      "id": "7610d5a4-f63c-4f21-9087-ca88bac58bb1",
      "name": "Wait For Response",
      "type": "n8n-nodes-base.wait",
      "position": [
        -980,
        -140
      ],
      "parameters": {},
      "typeVersion": 1.1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "a5af8f36-78aa-4f02-8c09-adf5f330b315",
  "connections": {
    "Wait For Response": {
      "main": [
        [
          {
            "node": "Parse Rankings & Detect Changes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Keywords Database": {
      "main": [
        [
          {
            "node": "Filter Active Keywords Only",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily SEO Check Trigger": {
      "main": [
        [
          {
            "node": "Get Keywords Database",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Email Ranking Alert": {
      "main": [
        [
          {
            "node": "Generate SEO Monitoring Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Slack Ranking Alert": {
      "main": [
        [
          {
            "node": "Generate SEO Monitoring Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Active Keywords Only": {
      "main": [
        [
          {
            "node": "Fetch Google Rankings via SERP API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Rankings & Detect Changes": {
      "main": [
        [
          {
            "node": "Filter Significant Ranking Drops",
            "type": "main",
            "index": 0
          },
          {
            "node": "Update Rankings in Google Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Rankings in Google Sheet": {
      "main": [
        [
          {
            "node": "Generate SEO Monitoring Summary",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Significant Ranking Drops": {
      "main": [
        [
          {
            "node": "Send Slack Ranking Alert",
            "type": "main",
            "index": 0
          },
          {
            "node": "Send Email Ranking Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Google Rankings via SERP API": {
      "main": [
        [
          {
            "node": "Wait For Response",
            "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 automated workflow monitors your website's keyword rankings daily and sends instant alerts to your team when significant ranking drops occur. It fetches current ranking positions, compares them with historical data, and triggers notifications through Slack and email when…

Source: https://n8n.io/workflows/6676/ — 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

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
Slack & Telegram

This workflow automates competitive price intelligence using Bright Data's enterprise web scraping API. On a scheduled basis (default: daily at 9 AM), the system loops through configured competitor pr

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

SEO managers, content marketers, bloggers, and growth teams who want to automatically catch declining content performance before it's too late — without manually checking Google Search Console every w

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

Automate tax deadline monitoring with AI-powered insights. This workflow checks your tax calendar daily at 8 AM, uses GPT-4 to analyze upcoming deadlines across multiple jurisdictions, detects overdue

Google Sheets, HTTP Request, Email Send +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