AutomationFlowsData & Sheets › Monitor Google Search Rankings with Serpapi and Google Sheets

Monitor Google Search Rankings with Serpapi and Google Sheets

BySerpApi @serpapi on n8n.io

This workflow enables SEO monitoring by checking Google rank positions for a list of keywords and domains. It uses SerpApi's Google Search API, but can be customized to use any of SerpApi's APIs.

Cron / scheduled trigger★★★★☆ complexity16 nodesGoogle SheetsN8N Nodes Serpapi
Data & Sheets Trigger: Cron / scheduled Nodes: 16 Complexity: ★★★★☆ Added:

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

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": "0UMNp6VvaRTKvh2L",
  "name": "Monitor Google Search rank with SerpApi & Google Sheets",
  "tags": [],
  "nodes": [
    {
      "id": "be9f2df9-ac78-4b5a-b2a2-5f4d2105cd40",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        624,
        400
      ],
      "parameters": {
        "color": 7,
        "width": 624,
        "height": 460,
        "content": "## Kick Off the Workflow\n\nConfigured to run at 10 AM UTC every day. Adjust as needed or trigger it manually.\n\nAdd your own Google Sheet in the Google Sheet node to fetch your keywords and domains to match. \n\nThe workflow will loop over each keyword and domain pair."
      },
      "typeVersion": 1
    },
    {
      "id": "ef8aad69-9f80-4f3e-8ab3-9a95b9d57d90",
      "name": "Loop Over Keywords",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1104,
        704
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "91dae5e2-f466-429f-86eb-218e995b7691",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2560,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 632,
        "height": 628,
        "content": "## Update Google Sheet\n\nLogs results and updates last run overview sheet.\n\nAdd your own Google Sheet here.\n\nIf the mappings get cleared when you add your Google Sheet, here they are:\n\nsearched_at: `{{ $now.toISO() }}`\ntarget: `{{ $('Loop Over Keywords').item.json.target }}`\nkeyword: `{{ $('Search Google').item.json.search_parameters.q }}`\nrank: `{{ $json.rank }}`\n\nThe update last run node should match on `row_number`. Enter this expression to match on:\n\n`{{ $('Loop Over Keywords').item.json.row_number }}`\n\nAfter the update, workflow will wait 4 seconds before going to the next keyword to avoid the Google Sheets API rate limit. You can remove/adjust this if you have a a higher limit on the Google Sheets API. "
      },
      "typeVersion": 1
    },
    {
      "id": "818e696f-02f0-4731-a327-571b4fd26dac",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        0,
        208
      ],
      "parameters": {
        "width": 598,
        "height": 912,
        "content": "## Google Keyword SEO Monitoring\n\n### What this is about\n\nThis workflow enables SEO monitoring by checking Google rank positions for a list of keywords and domains. It uses SerpApi's Google Search API, but can be customized to use any of SerpApi's APIs.\n\n### How it works\n\nThe workflow accepts Google Sheet with a list of keywords and domains to identify their rank in Google search results.\n\nThe results are synced to two sheets in a Google Sheet. The first is a log of all past runs. The second is an overview list showing the results from the latest run.\n\nThe workflow includes a Wait node that delays between each keyword to avoid Google Sheets API rate limiting. Adjust this if you have a custom rate limit.\n\n### Setup\n\n1. Create a free SerpApi account here: https://serpapi.com/\n1. Add SerpApi credentials to n8n. Your SerpApi API key is here: https://serpapi.com/manage-api-key\n1. Connect your Google Sheets accounts to n8n. Help available here: https://n8n.io/integrations/google-sheets/\n1. Copy this Google Sheet to your own Google account: https://docs.google.com/spreadsheets/d/148gjSSqSY5x9Gz5JWE_FDXHOuB7ASTomSyxkZjrjuNc/edit?gid=1750873622#gid=1750873622\n1. Set your list of keywords and domains in the 'Latest Run' sheet.\n1. Connect your Google Sheet in the 'Get Keywords and Domains to Match' node\n1. Connect your Google Sheet in the 'Update Rank Log' node\n1. Connect your Google Sheet again in the 'Update Latest Run' node\n1. (Optional) Customize the schedule to your needs\n1. (Optional) Set a custom page limit and match type in 'Set Page Limit & Match Type'\n\n### Documentation\n\n[SerpApi Google Search API](https://serpapi.com/search-api)\n[SerpApi n8n Node Intro Guide](https://serpapi.com/blog/boost-your-n8n-workflows-with-serpapis-verified-node/)\n"
      },
      "typeVersion": 1
    },
    {
      "id": "d77d05eb-09cb-40b3-be88-efc9e8c01b2e",
      "name": "Update Latest Run",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2832,
        688
      ],
      "parameters": {
        "columns": {
          "value": {
            "rank": "={{ $('Parse Rank for Target Domain or Page').item.json.rank }}",
            "target": "={{ $('Get Keywords and Domains to Match').item.json.target }}",
            "keyword": "={{ $('Search Google').item.json.search_parameters.q }}",
            "row_number": "={{ $('Get Keywords and Domains to Match').item.json.row_number }}",
            "searched_at": "={{ $now.toISO() }}"
          },
          "schema": [
            {
              "id": "target",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "target",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "keyword",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "keyword",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "domain_keyword_pair",
              "type": "string",
              "display": true,
              "removed": true,
              "required": false,
              "displayName": "domain_keyword_pair",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rank",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "rank",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "searched_at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "searched_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "row_number"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/106cvWeYYcT044aE8P7eKCreVaSS0MZ4XT_vKC3MzFNo/edit?pli=1&gid=1750873622#gid=1750873622"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/106cvWeYYcT044aE8P7eKCreVaSS0MZ4XT_vKC3MzFNo/edit?pli=1&gid=1750873622#gid=1750873622"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "0ff07a23-0a37-4ac7-9d7a-0cf5809b93c1",
      "name": "Search Google",
      "type": "n8n-nodes-serpapi.serpApi",
      "position": [
        1712,
        704
      ],
      "parameters": {
        "q": "={{ $('Get Keywords and Domains to Match').item.json.keyword }}",
        "location": "Austin, Texas, United States",
        "requestOptions": {},
        "additionalFields": {
          "start": "={{ $json.start }}",
          "no_cache": false
        }
      },
      "typeVersion": 1
    },
    {
      "id": "627b4185-71b0-4fd7-ac7b-f79ccdcec6b2",
      "name": "Update Rank Log",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2624,
        688
      ],
      "parameters": {
        "columns": {
          "value": {
            "rank": "={{ $json.rank }}",
            "target": "={{ $('Get Keywords and Domains to Match').item.json.target }}",
            "keyword": "={{ $('Search Google').item.json.search_parameters.q }}",
            "searched_at": "={{ $now.toISO() }}"
          },
          "schema": [
            {
              "id": "searched_at",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "searched_at",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "target",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "target",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "keyword",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "keyword",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rank",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "rank",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/106cvWeYYcT044aE8P7eKCreVaSS0MZ4XT_vKC3MzFNo/edit?pli=1&gid=0#gid=0"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/106cvWeYYcT044aE8P7eKCreVaSS0MZ4XT_vKC3MzFNo/edit?pli=1&gid=0#gid=0"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "754e9a3a-6cbc-4df2-8fa8-eaf47e511cf5",
      "name": "Get Keywords and Domains to Match",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        880,
        704
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/106cvWeYYcT044aE8P7eKCreVaSS0MZ4XT_vKC3MzFNo/edit?pli=1&gid=1750873622#gid=1750873622"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/106cvWeYYcT044aE8P7eKCreVaSS0MZ4XT_vKC3MzFNo/edit?pli=1&gid=1750873622#gid=1750873622"
        }
      },
      "typeVersion": 4.5
    },
    {
      "id": "04bc2052-d3ea-46da-8e21-29f4dd089872",
      "name": "If - Next Page or Next Keyword",
      "type": "n8n-nodes-base.if",
      "position": [
        2368,
        704
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "bf3c9bb0-8bdf-4a1c-99c9-1631afd7426e",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.rank }}",
              "rightValue": "N/A"
            },
            {
              "id": "30a48eb7-a3ff-4728-a00a-e56b4629c054",
              "operator": {
                "type": "number",
                "operation": "lte"
              },
              "leftValue": "={{ $json.page_limit * 10 }}",
              "rightValue": "={{ $json.start }}"
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.3
    },
    {
      "id": "0c5b2240-64ef-415f-8083-47cc88e3df24",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1264,
        400
      ],
      "parameters": {
        "color": 7,
        "width": 364,
        "height": 460,
        "content": "## Set Page Limit & Match Type\n\nTemplate defaults to checking up to 10 pages to find a domain. Update `data.pagination_limit` here to set your own limit.\n\nTemplate defaults to match on domain. Update `data.match_type` to match for page URLs.\n- `'page'` - Match exact URL path (e.g., `example.com/page1`)\n- `'domain'` - Match any URL from the domain (e.g., any page from `example.com`)"
      },
      "typeVersion": 1
    },
    {
      "id": "5b8d033b-276b-40b5-8f26-b4d95b11c890",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2304,
        400
      ],
      "parameters": {
        "color": 7,
        "width": 236,
        "height": 460,
        "content": "## Next Page or Next Keyword\n\nIf a match is found or the pagination limit is hit, it goes to the next keyword. Otherwise, it requests another page for the same keyword. "
      },
      "typeVersion": 1
    },
    {
      "id": "46c24178-51bc-45b2-b0ec-cfcb737b9926",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1648,
        224
      ],
      "parameters": {
        "color": 7,
        "width": 636,
        "height": 636,
        "content": "## Search & Parse Rank\n\nThe SerpApi node searches the keyword in through Google Search API.\n\nThe code finds and parses the target domain's rank. Assigns \"N/A\" if a domain is not found in the results.\n\n### URL Matching Behavior\nMake sure to set your `data.match_type` from **'Set Page Limit & Match Type'** node: \n\n**Page Mode:**\n- Target with path (e.g., `example.com/blog`) \u2192 matches exact path only\n- Target without path (e.g., `example.com`) \u2192 matches any page from domain\n\n**Domain Mode:**\n- Always matches any page from the domain, ignoring path\n\n### URL Normalization\nURLs are normalized by removing protocols, `www.`, trailing slashes, and converting to lowercase for consistent matching."
      },
      "typeVersion": 1
    },
    {
      "id": "d901ec60-bdf1-4cc6-8a33-6d8d7f435945",
      "name": "Parse Rank for Target Domain or Page",
      "type": "n8n-nodes-base.code",
      "position": [
        2032,
        704
      ],
      "parameters": {
        "jsCode": "const data = $getWorkflowStaticData('global');\n\nif ($input.first().json.search_information.organic_results_state != \"Fully empty\") {\n  \n  const targetUrl = $('Loop Over Keywords').first().json.target;\n  const matchType = $('Loop Over Keywords').first().json.match_type || data.match_type;\n  \n  function normalizeUrl(url) {\n    if (!url) return null;\n    let cleanUrl = url.trim().replace(/^[\"']|[\"']$/g, '');\n    cleanUrl = cleanUrl.replace(/^https?:\\/\\//, '');\n    cleanUrl = cleanUrl.replace(/^www\\./, '');\n    cleanUrl = cleanUrl.replace(/\\/$/, '');\n    return cleanUrl.toLowerCase();\n  }\n  \n  const normalizedTarget = normalizeUrl(targetUrl);\n  const targetHasPath = normalizedTarget.includes('/');\n  \n  if (matchType === 'domain' || !targetHasPath) {\n    const targetDomain = normalizedTarget.split('/')[0];\n    index = $input.first().json.organic_results.findIndex(obj => {\n      const resultUrl = normalizeUrl(obj.link);\n      const resultDomain = resultUrl ? resultUrl.split('/')[0] : null;\n      return resultDomain === targetDomain;\n    });\n  } else {\n    index = $input.first().json.organic_results.findIndex(obj => {\n      const resultUrl = normalizeUrl(obj.link);\n      return resultUrl === normalizedTarget;\n    });\n  }\n\n  if (index >= 0) {\n    data.rank = data.start + index + 1;\n  } else {\n    data.rank = \"N/A\"\n  }\n  \n  data.start += 10\n  \n} else {\n  data.rank = \"N/A\"\n  data.start = 100000\n}\n\nreturn data;"
      },
      "typeVersion": 2
    },
    {
      "id": "38f38f68-2d4a-4b80-9fb3-61e6ec608288",
      "name": "Set Page Limit & Match Type",
      "type": "n8n-nodes-base.code",
      "position": [
        1408,
        704
      ],
      "parameters": {
        "jsCode": "const data = $getWorkflowStaticData('global');\ndata.start = 0; // Leave as is\n\n// Update below value to limit how many pages to check before moving to the next keyword\n// Default is 10 to check up to 100 results\n// Set to 1 to only check the first page\n// Set to a high value (e.g. 50) to ensure all possible pages are checked\ndata.page_limit = 10;\n\n// Set your default match type here: 'domain' or 'page'\n// 'domain' will match on the first result matching your provided domain\n// 'page' will match on a specific page's URL\ndata.match_type = 'domain';\n\nreturn data;"
      },
      "typeVersion": 2
    },
    {
      "id": "3fd04db6-1be6-45fa-996c-33b33515e2e2",
      "name": "Wait Before Next Keyword",
      "type": "n8n-nodes-base.wait",
      "position": [
        3040,
        688
      ],
      "parameters": {
        "amount": 4
      },
      "typeVersion": 1.1
    },
    {
      "id": "6f5ac293-183c-4b90-b755-d936f40afb20",
      "name": "Schedule Workflow",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        688,
        704
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 10
            }
          ]
        }
      },
      "typeVersion": 1.2
    }
  ],
  "active": false,
  "settings": {
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "b1bd15b3-213d-418b-a0ad-f8b8754dc83f",
  "connections": {
    "Search Google": {
      "main": [
        [
          {
            "node": "Parse Rank for Target Domain or Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Rank Log": {
      "main": [
        [
          {
            "node": "Update Latest Run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Schedule Workflow": {
      "main": [
        [
          {
            "node": "Get Keywords and Domains to Match",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Latest Run": {
      "main": [
        [
          {
            "node": "Wait Before Next Keyword",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Keywords": {
      "main": [
        [],
        [
          {
            "node": "Set Page Limit & Match Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait Before Next Keyword": {
      "main": [
        [
          {
            "node": "Loop Over Keywords",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Page Limit & Match Type": {
      "main": [
        [
          {
            "node": "Search Google",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If - Next Page or Next Keyword": {
      "main": [
        [
          {
            "node": "Update Rank Log",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Search Google",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Keywords and Domains to Match": {
      "main": [
        [
          {
            "node": "Loop Over Keywords",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Rank for Target Domain or Page": {
      "main": [
        [
          {
            "node": "If - Next Page or Next Keyword",
            "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 enables SEO monitoring by checking Google rank positions for a list of keywords and domains. It uses SerpApi's Google Search API, but can be customized to use any of SerpApi's APIs.

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

More Data & Sheets workflows → · Browse all categories →

Related workflows

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

Data & Sheets

This workflow will be useful for anyone looking to do SEO tracking on the Google Play Store. It automates checking Google Play Store rank positions and average ratings for a list of app titles.

Google Sheets, N8N Nodes Serpapi
Data & Sheets

This workflow automates video distribution to 9 social platforms simultaneously using Blotato's API. It includes both a scheduled publisher (checks Google Sheets for videos marked "Ready") and a subwo

Google Sheets, HTTP Request, Form Trigger +2
Data & Sheets

YogiAI. Uses googleSheets, googleSheetsTool, httpRequest, stopAndError. Scheduled trigger; 61 nodes.

Google Sheets, Google Sheets Tool, HTTP Request +1
Data & Sheets

This 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 relev

Google Calendar, Google Sheets, HTTP Request +1
Data & Sheets

Useful if a team is working within a single instance and you want to be notified of what workflows have changed since you last visited them. Another use-case might be monitoring your managed instances

Google Sheets, Execute Workflow Trigger, n8n