AutomationFlowsData & Sheets › Track Weekly Keyword Rankings with Google Serp, Serper Api, and Google Sheets

Track Weekly Keyword Rankings with Google Serp, Serper Api, and Google Sheets

ByAmuratech @amuratech on n8n.io

This template is designed for SEO specialists, content marketers, and digital growth teams who want to automate the process of tracking keyword rankings.

Cron / scheduled trigger★★★★☆ complexity18 nodesGoogle SheetsHTTP Request
Data & Sheets Trigger: Cron / scheduled Nodes: 18 Complexity: ★★★★☆ Added:

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

This workflow follows the Google Sheets → 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
{
  "id": "HPsY2j0Wk84xZipZ",
  "name": "My workflow 18",
  "tags": [],
  "nodes": [
    {
      "id": "30bf61f8-a164-4a3e-b2b4-554a1cd3cd3d",
      "name": "Loop Over Items",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        1248,
        752
      ],
      "parameters": {
        "options": {}
      },
      "typeVersion": 3
    },
    {
      "id": "dead8ec9-9186-4d66-ac87-bc6a8878b05c",
      "name": "Get Targeted Keywords",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        912,
        752
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1820594268,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1dXoeWqh7a3HO9cy8Z7-W90ZG8wTimm5mDogMgBMTugs/edit#gid=1820594268",
          "cachedResultName": "Sheet2"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1dXoeWqh7a3HO9cy8Z7-W90ZG8wTimm5mDogMgBMTugs",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1dXoeWqh7a3HO9cy8Z7-W90ZG8wTimm5mDogMgBMTugs/edit?usp=drivesdk",
          "cachedResultName": "Zuno - Weekly Keyword Ranking Tracker"
        },
        "authentication": "serviceAccount"
      },
      "retryOnFail": true,
      "typeVersion": 4.6,
      "alwaysOutputData": true
    },
    {
      "id": "ab647825-d72d-44eb-b6e3-14ec81807f86",
      "name": "Keyword Searching on Google",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        1856,
        592
      ],
      "parameters": {
        "url": "=https://google.serper.dev/search",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "q",
              "value": "={{ $json.Keyword }}"
            },
            {
              "name": "gl",
              "value": "in"
            },
            {
              "name": "num",
              "value": "100"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "X-API-KEY",
              "value": "={{$credentials.serperApiKey}}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "retryOnFail": false,
      "typeVersion": 4.2,
      "alwaysOutputData": true
    },
    {
      "id": "52387416-4587-42ca-ac8a-4a36a62f2f7e",
      "name": "Extracting Ranking & URL",
      "type": "n8n-nodes-base.code",
      "position": [
        2144,
        592
      ],
      "parameters": {
        "jsCode": "/**\n * n8n Code (Code v2)\n * Behavior:\n * - Looks for today's date (Asia/Kolkata). If any \"Date N\" already equals today, it updates that same N.\n * - Otherwise, it finds the first empty \"Date N\" and writes there.\n * - If domain not found in SERP \u2192 position = 100 and link = null.\n * - Prevents overwriting filled dates unless it's the same day (unless you enable OVERWRITE_IF_ALL_FILLED).\n *\n * Requires:\n * - A node named \"Get Targeted Keywords\" (or adapt the selector below) that returns the row with `row_number` (or `rowNumber`)\n * - Columns in the sheet:\n *   Ranking Date 1..12, Ranking URL Date 1..12, Date 1..12\n */\n\nconst MAX_SLOTS = 12;                // \u2705 now 12\nconst OVERWRITE_IF_ALL_FILLED = false; // set true to overwrite the last slot if all are filled\n\n// ---- Target domain / URL detection ----\n// Option A: hardcode a domain (kept for compatibility)\nconst HARDCODED_DOMAIN = \"Update Your Domain\";\n\n// --- helpers ---\nfunction dateInIST(d = new Date()) {\n  const istOffsetMin = 330; // +05:30\n  return new Date(d.getTime() + (istOffsetMin - d.getTimezoneOffset()) * 60000);\n}\nfunction todayISTString() {\n  return dateInIST().toISOString().split(\"T\")[0]; // YYYY-MM-DD\n}\nfunction safeTrim(v) {\n  return (v ?? \"\").toString().trim();\n}\nfunction getDomainFromUrl(u) {\n  try {\n    const url = new URL(u);\n    // normalize to scheme + host (like https://example.com/)\n    return `${url.protocol}//${url.host}/`;\n  } catch {\n    return \"\";\n  }\n}\n\n// --- get the sheet row (rename the node name if needed) ---\nconst row = $('Get Targeted Keywords').first().json || {};\nconst rowNumber = row.rowNumber ?? row.row_number;\nif (!rowNumber) {\n  return [{ json: { error: \"Missing rowNumber/row_number from sheet row.\", debug: row } }];\n}\n\n// Try to use the exact page/target URL from sheet if present\nconst targetUrl = row[\"Target Page\"] || row[\"Page\"] || \"\";\nconst inferredDomain = getDomainFromUrl(targetUrl);\nconst targetDomain = inferredDomain || HARDCODED_DOMAIN;\n\n// --- parse SERP (expects this node input to have an 'organic' array with {link}) ---\nconst organicResults = $json.organic || [];\n\n// Defaults when NOT found\nlet foundDomain = false;\nlet foundExactUrl = false;\nlet position = 100;\nlet link = null;\n\nfor (let i = 0; i < organicResults.length; i++) {\n  const currentLink = organicResults[i].link || \"\";\n  if (!currentLink) continue;\n\n  if (currentLink.includes(targetDomain)) {\n    foundDomain = true;\n    position = i + 1;      // actual rank\n    link = currentLink;    // actual URL\n    if (safeTrim(targetUrl) && currentLink === targetUrl) foundExactUrl = true;\n    break;\n  }\n}\n\n// --- choose the Date slot ---\nconst today = todayISTString();\n\n// 1) If any Date N already equals today, update that same N\nlet slot = null;\nfor (let i = 1; i <= MAX_SLOTS; i++) {\n  const dateKey = `Date ${i}`;\n  const val = safeTrim(row[dateKey]);\n  if (val === today) {\n    slot = i;\n    break;\n  }\n}\n\n// 2) Else pick the first empty Date N\nif (slot === null) {\n  for (let i = 1; i <= MAX_SLOTS; i++) {\n    const dateKey = `Date ${i}`;\n    const val = safeTrim(row[dateKey]);\n    if (val === \"\") {\n      slot = i;\n      break;\n    }\n  }\n}\n\n// 3) If all filled\nif (slot === null) {\n  if (!OVERWRITE_IF_ALL_FILLED) {\n    return [{\n      json: {\n        error: `All ${MAX_SLOTS} date slots are filled for row ${rowNumber}. Add more columns or enable OVERWRITE_IF_ALL_FILLED.`,\n        rowNumber\n      }\n    }];\n  } else {\n    slot = MAX_SLOTS; // overwrite the last slot\n  }\n}\n\n// Build dynamic keys for the chosen slot\nconst rankCol = `Ranking Date ${slot}`;\nconst urlCol  = `Ranking URL Date ${slot}`;\nconst dateCol = `Date ${slot}`;\n\n// Minimal payload to update ONLY these three cells\nreturn [{\n  json: {\n    rowNumber,\n    [rankCol]: position, // actual rank or 100 if not found\n    [urlCol]: link,      // actual URL or null if not found\n    [dateCol]: today,    // execution date (IST)\n    _debug: { foundDomain, foundExactUrl, position, link, targetUrl, inferredDomain: targetDomain, slot, today }\n  }\n}];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "e9fb2f80-f241-4c02-a966-7a1e4d4df014",
      "name": "Merging Result",
      "type": "n8n-nodes-base.merge",
      "position": [
        2416,
        752
      ],
      "parameters": {},
      "typeVersion": 3.2
    },
    {
      "id": "1e544677-74ea-4d92-8677-31697f08e41b",
      "name": "Formating output",
      "type": "n8n-nodes-base.code",
      "position": [
        2720,
        976
      ],
      "parameters": {
        "jsCode": "/**\n * INPUTS (from a Merge node or two incoming connections):\n *   A) update payload item (e.g. { rowNumber, \"Ranking Date 1\": 1, \"Ranking URL Date 1\": \"...\", \"Date 1\": \"YYYY-MM-DD\" })\n *   B) sheet row item       (e.g. { row_number: 2, Page, Keyword, \"Ranking Date 1\", \"Date 1\", ... })\n *\n * OUTPUT:\n *   Single item with:\n *     - rowNumber\n *     - ALL existing columns from the sheet row\n *     - overlaid updated fields (only the changed date/week keys)\n *\n * Notes:\n *   - Works with either Week or Date column schemes.\n *   - Keeps nulls as null (no coercion), so link can stay null when not found.\n */\n\nconst items = $input.all().map(i => i.json);\n\n// Detectors\nfunction looksLikeRow(obj) {\n  return (('row_number' in obj) || ('rowNumber' in obj)) && ('Keyword' in obj || 'Page' in obj);\n}\nfunction looksLikeUpdate(obj) {\n  const keys = Object.keys(obj);\n  // date scheme\n  const hasRankDate = keys.some(k => /^Ranking Date \\d+$/.test(k));\n  const hasUrlDate  = keys.some(k => /^Ranking URL Date \\d+$/.test(k));\n  const hasDate     = keys.some(k => /^Date \\d+$/.test(k));\n  // week scheme (kept for backwards-compat)\n  const hasRankWeek = keys.some(k => /^Ranking Week \\d+$/.test(k));\n  const hasUrlWeek  = keys.some(k => /^Ranking URL Week \\d+$/.test(k));\n  return (hasRankDate || hasUrlDate || hasDate || hasRankWeek || hasUrlWeek);\n}\n\nconst row = items.find(looksLikeRow) || {};\nconst upd = items.find(looksLikeUpdate) || {};\n\nconst rowNumber = row.rowNumber ?? row.row_number ?? upd.rowNumber;\nif (!rowNumber) {\n  return [{ json: { error: \"rowNumber missing. Ensure the sheet row and update payload reach this node.\", debug: items } }];\n}\n\n// Start with rowNumber\nconst payload = { rowNumber };\n\n// 1) copy ALL existing columns from the sheet row\nfor (const [k, v] of Object.entries(row)) {\n  if (k === 'row_number' || k === 'rowIndex' || k === 'rowNumber') continue; // internal keys\n  payload[k] = v;\n}\n\n// 2) overlay ONLY updated fields from the update item\nfor (const [k, v] of Object.entries(upd)) {\n  if (k === 'row_number' || k === 'rowNumber' || k.startsWith('_')) continue; // skip internal/meta\n  // leave nulls as null (so link can stay null when domain not found)\n  payload[k] = v;\n}\n\nreturn [{ json: payload }];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "c3f4ebaa-9836-49b8-96ac-8dd4550771c5",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        288,
        272
      ],
      "parameters": {
        "color": 6,
        "width": 3312,
        "height": 1104,
        "content": "## Weekly Target Keyword Ranking (Google SERP)\n"
      },
      "typeVersion": 1
    },
    {
      "id": "259e8d37-9b70-42c6-bf5c-ac40a73626ee",
      "name": "Run Every Monday",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        656,
        752
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 0 * * MON"
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "bff36e54-bf3a-4943-a558-3b4ba25a99b9",
      "name": "Update Rank and URLs (Google Sheet)",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2960,
        1072
      ],
      "parameters": {
        "columns": {
          "value": {
            "Page": "={{ $json.Page }}",
            "Sr.no": "={{ $json[\"Sr.no\"] }}",
            "Date 1": "={{ $json[\"Date 1\"] }}",
            "Date 2": "={{ $json[\"Date 2\"] }}",
            "Date 3": "={{ $json[\"Date 3\"] }}",
            "Date 4": "={{ $json[\"Date 4\"] }}",
            "Date 5": "={{ $json[\"Date 5\"] }}",
            "Date 6": "={{ $json[\"Date 6\"] }}",
            "Date 7": "={{ $json[\"Date 7\"] }}",
            "Date 8": "={{ $json[\"Date 8\"] }}",
            "Date 9": "={{ $json[\"Date 9\"] }}",
            "Date 10": "={{ $json[\"Date 10\"] }}",
            "Date 11": "={{ $json[\"Date 11\"] }}",
            "Date 12": "={{ $json[\"Date 12\"] }}",
            "Keyword": "={{ $json.Keyword }}",
            "Ranking Date 1": "={{ $json[\"Ranking Date 1\"] }}",
            "Ranking Date 2": "={{ $json[\"Ranking Date 2\"] }}",
            "Ranking Date 3": "={{ $json[\"Ranking Date 3\"] }}",
            "Ranking Date 4": "={{ $json[\"Ranking Date 4\"] }}",
            "Ranking Date 5": "={{ $json[\"Ranking Date 5\"] }}",
            "Ranking Date 6": "={{ $json[\"Ranking Date 6\"] }}",
            "Ranking Date 7": "={{ $json[\"Ranking Date 7\"] }}",
            "Ranking Date 8": "={{ $json[\"Ranking Date 8\"] }}",
            "Ranking Date 9": "={{ $json[\"Ranking Date 9\"] }}",
            "Ranking Date 10": "={{ $json[\"Ranking Date 10\"] }}",
            "Ranking Date 11": "={{ $json[\"Ranking Date 11\"] }}",
            "Ranking Date 12": "={{ $json[\"Ranking Date 12\"] }}",
            "Ranking URL Date 1": "={{ $json[\"Ranking URL Date 1\"] }}",
            "Ranking URL Date 2": "={{ $json[\"Ranking URL Date 2\"] }}",
            "Ranking URL Date 3": "={{ $json[\"Ranking URL Date 3\"] }}",
            "Ranking URL Date 4": "={{ $json[\"Ranking URL Date 4\"] }}",
            "Ranking URL Date 5": "={{ $json[\"Ranking URL Date 5\"] }}",
            "Ranking URL Date 6": "={{ $json[\"Ranking URL Date 6\"] }}",
            "Ranking URL Date 7": "={{ $json[\"Ranking URL Date 7\"] }}",
            "Ranking URL Date 8": "={{ $json[\"Ranking URL Date 8\"] }}",
            "Ranking URL Date 9": "={{ $json[\"Ranking URL Date 9\"] }}",
            "Ranking URL Date 10": "={{ $json[\"Ranking URL Date 10\"] }}",
            "Ranking URL Date 11": "={{ $json[\"Ranking URL Date 11\"] }}",
            "Ranking URL Date 12": "={{ $json[\"Ranking URL Date 12\"] }}"
          },
          "schema": [
            {
              "id": "Sr.no",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Sr.no",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Page",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Page",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Keyword",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Keyword",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking Date 1",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking Date 1",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking URL Date 1",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking URL Date 1",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date 1",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date 1",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking Date 2",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking Date 2",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking URL Date 2",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking URL Date 2",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date 2",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date 2",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking Date 3",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking Date 3",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking URL Date 3",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking URL Date 3",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date 3",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date 3",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking Date 4",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking Date 4",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking URL Date 4",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking URL Date 4",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date 4",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date 4",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking Date 5",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking Date 5",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking URL Date 5",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking URL Date 5",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date 5",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date 5",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking Date 6",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking Date 6",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking URL Date 6",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking URL Date 6",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date 6",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date 6",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking Date 7",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking Date 7",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking URL Date 7",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking URL Date 7",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date 7",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date 7",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking Date 8",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking Date 8",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking URL Date 8",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking URL Date 8",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date 8",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date 8",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking Date 9",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking Date 9",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking URL Date 9",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking URL Date 9",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date 9",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date 9",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking Date 10",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking Date 10",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking URL Date 10",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking URL Date 10",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date 10",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date 10",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking Date 11",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking Date 11",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking URL Date 11",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking URL Date 11",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date 11",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date 11",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking Date 12",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking Date 12",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Ranking URL Date 12",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Ranking URL Date 12",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date 12",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Date 12",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "row_number",
              "type": "number",
              "display": true,
              "removed": true,
              "readOnly": true,
              "required": false,
              "displayName": "row_number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Sr.no"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "your-sheet-name",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1dXoeWqh7a3HO9cy8Z7-W90ZG8wTimm5mDogMgBMTugs/edit#gid=1820594268",
          "cachedResultName": "Sheet2"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "your-google-sheet-id",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1dXoeWqh7a3HO9cy8Z7-W90ZG8wTimm5mDogMgBMTugs/edit?usp=drivesdk",
          "cachedResultName": "Zuno - Weekly Keyword Ranking Tracker"
        },
        "authentication": "serviceAccount"
      },
      "typeVersion": 4.6
    },
    {
      "id": "455b4dec-5fb5-4d2d-af45-505064b4701c",
      "name": "Sticky Note (Setup Checklist)",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        384,
        -80
      ],
      "parameters": {
        "width": 656,
        "height": 272,
        "content": "## Setup Checklist\n\n1. Add your Google Service Account credentials in n8n\n2. Replace `your-google-sheet-id` and `your-sheet-name` in Google Sheet nodes\n3. Add your Serper API key to n8n credentials (`serperApiKey`)\n4. (Optional) Update the `HARDCODED_DOMAIN` variable in the Code node\n\n\u2705 After setup, run manually or let it trigger every Monday."
      },
      "typeVersion": 1
    },
    {
      "id": "142853d2-4686-4e07-9c8f-61fcb30c42e0",
      "name": "Sticky Note (Keywords)",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        816,
        912
      ],
      "parameters": {
        "width": 336,
        "height": 272,
        "content": "## Google Sheets Setup (Read Keywords)\n\n- Replace your Google Sheet ID and sheet name\n- Connect your Google Service Account credentials\n- Sheet must include at least: `Keyword`, `Target Page`, `Sr.no`"
      },
      "typeVersion": 1
    },
    {
      "id": "9d8b0355-9564-4a96-a3d7-eb4388e88949",
      "name": "Sticky Note (Serper)",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1520,
        368
      ],
      "parameters": {
        "width": 448,
        "height": 208,
        "content": "## Serper API Setup\n\n- Queries Google SERP results using Serper API\n- Add your Serper API key in n8n credentials (`serperApiKey`)\n- Docs: https://serper.dev\n- Parameters: q=Keyword, gl=country code, num=100"
      },
      "typeVersion": 1
    },
    {
      "id": "f065a6fa-fa63-495c-ab77-4b38bb536f81",
      "name": "Sticky Note (Extract)",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2256,
        416
      ],
      "parameters": {
        "width": 352,
        "height": 208,
        "content": "## Extracting Ranking & URL\n\n- Parses SERP results and finds your domain/page\n- If found \u2192 saves rank + URL in sheet\n- If not found \u2192 sets position=100\n- Default domain is set in `HARDCODED_DOMAIN`"
      },
      "typeVersion": 1
    },
    {
      "id": "5345af1a-3760-4600-917a-cb70b9599dd7",
      "name": "Sticky Note (Update Sheet)",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3120,
        1040
      ],
      "parameters": {
        "width": 400,
        "height": 208,
        "content": "## Google Sheets Setup (Write Rankings)\n\n- Replace your Google Sheet ID and sheet name\n- Connect Google Service Account credentials\n- Workflow updates columns Ranking Date/URL/Date 1..12"
      },
      "typeVersion": 1
    },
    {
      "id": "27118c11-77ea-4d07-bd58-fbe39d9ffa38",
      "name": "Sticky Note (Cron)",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        560,
        576
      ],
      "parameters": {
        "width": 256,
        "content": "## Cron Trigger\n\n- Runs automatically every Monday at 00:00 (midnight)\n- Adjust the CRON expression in this node if needed"
      },
      "typeVersion": 1
    },
    {
      "id": "62a99293-63e0-4330-ac42-9fda4c79f1c1",
      "name": "Sticky Note (Formatting)",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2256,
        976
      ],
      "parameters": {
        "width": 352,
        "height": 192,
        "content": "## Formatting Output\n\n- Merges SERP ranking results with the existing Google Sheet row\n- Ensures only updated rank/date/url fields overwrite\n- Keeps other columns intact"
      },
      "typeVersion": 1
    },
    {
      "id": "72892fc6-105e-47e5-8f0e-0e04b31e8698",
      "name": "When clicking \u2018Execute workflow\u2019",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        448,
        1008
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "d843cd8f-8720-4ce2-9bcb-2b967835c135",
      "name": "Delay Between Keyword Queries",
      "type": "n8n-nodes-base.wait",
      "position": [
        1536,
        768
      ],
      "parameters": {
        "amount": 10
      },
      "typeVersion": 1.1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "376ae7b0-def1-48a0-aeb7-9a8661a45a5e",
  "connections": {
    "Merging Result": {
      "main": [
        [
          {
            "node": "Formating output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "Delay Between Keyword Queries",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Formating output": {
      "main": [
        [
          {
            "node": "Update Rank and URLs (Google Sheet)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Every Monday": {
      "main": [
        [
          {
            "node": "Get Targeted Keywords",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Targeted Keywords": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extracting Ranking & URL": {
      "main": [
        [
          {
            "node": "Merging Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keyword Searching on Google": {
      "main": [
        [
          {
            "node": "Extracting Ranking & URL",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Delay Between Keyword Queries": {
      "main": [
        [
          {
            "node": "Keyword Searching on Google",
            "type": "main",
            "index": 0
          },
          {
            "node": "Merging Result",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Update Rank and URLs (Google Sheet)": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "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 template is designed for SEO specialists, content marketers, and digital growth teams who want to automate the process of tracking keyword rankings.

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

ofn hook v0.24.0 beta. Uses start, httpRequest, functionItem, itemLists. Scheduled trigger; 42 nodes.

Start, HTTP Request, Function Item +3
Data & Sheets

Security teams, DevOps engineers, vulnerability analysts, and automation builders who want to eliminate repetitive Nessus scan parsing, AI-based risk triage, and manual reporting. Designed for orgs fo

Email Send, HTTP Request, Google Sheets +1