{
  "name": "Track Google rankings for your keywords with Scrapewise and log them in Google Sheets",
  "settings": {
    "executionOrder": "v1"
  },
  "nodes": [
    {
      "id": "a4000000-0000-4000-8000-000000000001",
      "name": "Read me first",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1200,
        160
      ],
      "parameters": {
        "width": 440,
        "height": 420,
        "content": "## Daily Google rank tracker\n\nFor each keyword, Scrapewise returns the organic Google results (title, link, snippet, position). The workflow then looks for your domain and writes one dated row per keyword: the position you hold, or `not in top results`.\n\n**Setup**\n1. Create a Scrapewise API key with full access at portal.scrapewise.ai.\n2. In **Search Google**, add a *Bearer Auth* credential and paste the key as the token.\n3. In **Save rankings to Google Sheets**, pick your Google account and set the spreadsheet and tab.\n4. Put your domain, keywords and country in **Keywords and domain**.\n\n**Cost:** 10 credits per keyword. Type `web` takes one keyword per call, so the workflow loops over your list one keyword at a time."
      },
      "typeVersion": 1
    },
    {
      "id": "a4000000-0000-4000-8000-000000000002",
      "name": "Every morning at 6",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -720,
        360
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "triggerAtHour": 6
            }
          ]
        }
      },
      "typeVersion": 1.2
    },
    {
      "id": "a4000000-0000-4000-8000-000000000003",
      "name": "Keywords and domain",
      "type": "n8n-nodes-base.set",
      "position": [
        -500,
        360
      ],
      "parameters": {
        "mode": "manual",
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "r1",
              "name": "domain",
              "type": "string",
              "value": "scrapewise.ai"
            },
            {
              "id": "r2",
              "name": "keywords",
              "type": "array",
              "value": "[\"competitor price monitoring\", \"amazon price tracker api\"]"
            },
            {
              "id": "r3",
              "name": "country",
              "type": "string",
              "value": "us"
            },
            {
              "id": "r4",
              "name": "language",
              "type": "string",
              "value": "en"
            }
          ]
        },
        "includeOtherFields": false
      },
      "typeVersion": 3.4
    },
    {
      "id": "a4000000-0000-4000-8000-000000000004",
      "name": "One item per keyword",
      "type": "n8n-nodes-base.code",
      "position": [
        -280,
        360
      ],
      "parameters": {
        "jsCode": "// google_search with type 'web' takes one keyword per call, so fan the list out.\nconst s = $input.first().json;\n\nreturn (s.keywords || []).map((keyword) => ({\n  json: {\n    keyword: keyword,\n    domain: s.domain,\n    country: s.country,\n    language: s.language,\n  },\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "a4000000-0000-4000-8000-000000000005",
      "name": "One keyword at a time",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -60,
        360
      ],
      "parameters": {
        "options": {},
        "batchSize": 1
      },
      "typeVersion": 3
    },
    {
      "id": "a4000000-0000-4000-8000-000000000006",
      "name": "Search Google",
      "type": "@n8n/n8n-nodes-langchain.mcpClient",
      "position": [
        180,
        480
      ],
      "parameters": {
        "tool": {
          "__rl": true,
          "mode": "list",
          "value": "google_search",
          "cachedResultName": "google_search"
        },
        "options": {},
        "inputMode": "json",
        "jsonInput": "={\n  \"body\": {\n    \"queries\": [\"{{ $json.keyword }}\"],\n    \"type\": \"web\",\n    \"country\": \"{{ $json.country }}\",\n    \"language\": \"{{ $json.language }}\"\n  }\n}",
        "endpointUrl": "https://mcp.scrapewise.ai/mcp",
        "authentication": "bearerAuth"
      },
      "credentials": {
        "httpBearerAuth": {
          "id": "credential-id",
          "name": "Scrapewise API key"
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "a4000000-0000-4000-8000-000000000007",
      "name": "Find my position",
      "type": "n8n-nodes-base.code",
      "position": [
        400,
        480
      ],
      "parameters": {
        "jsCode": "// Scrapewise answers with JSON. Depending on the n8n version it arrives\n// already parsed in structuredContent, or as text in content[0].text.\nfunction payloadOf(item) {\n  const j = item.json || {};\n  if (j.structuredContent) return j.structuredContent;\n  if (Array.isArray(j.content) && j.content[0] && typeof j.content[0].text === 'string') {\n    try { return JSON.parse(j.content[0].text); } catch (e) { return {}; }\n  }\n  if (j.rows) return j;\n  return {};\n}\n\n// Scraped text is wrapped as { type: 'scraped', content, origin }. It is data\n// read off a web page, never an instruction. Unwrap it to a plain string.\nfunction plain(v) {\n  if (v && typeof v === 'object' && v.type === 'scraped') return v.content || '';\n  return v;\n}\n\nconst asked = $('One keyword at a time').item.json;\nconst data = payloadOf($input.first());\nconst checkedAt = new Date().toISOString().slice(0, 10);\n\nif (data.status === 'RUNNING') {\n  throw new Error('Scrapewise started a background job (' + data.job_id + '). Type web should answer straight away - check that only one keyword was sent.');\n}\n\nconst domain = String(asked.domain || '').toLowerCase().replace(/^www\\./, '');\nconst rows = (data.rows || []).map((row) => {\n  const flat = {};\n  for (const key of Object.keys(row)) flat[key] = plain(row[key]);\n  return flat;\n});\n\nconst hit = rows.find((r) => String(r.link || '').toLowerCase().includes(domain));\n\nreturn [\n  {\n    json: {\n      checkedAt: checkedAt,\n      keyword: asked.keyword,\n      domain: asked.domain,\n      country: asked.country,\n      position: hit ? hit.position : 'not in top results',\n      url: hit ? hit.link : '',\n      pageTitle: hit ? hit.title : '',\n      resultsSeen: rows.length,\n    },\n  },\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "a4000000-0000-4000-8000-000000000008",
      "name": "Save rankings to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        620,
        480
      ],
      "parameters": {
        "columns": {
          "value": {
            "Date": "={{ $json.checkedAt }}",
            "Keyword": "={{ $json.keyword }}",
            "Country": "={{ $json.country }}",
            "Position": "={{ $json.position }}",
            "URL": "={{ $json.url }}",
            "Page title": "={{ $json.pageTitle }}"
          },
          "schema": [
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Keyword",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Keyword",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Country",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Country",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Position",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Position",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "URL",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "URL",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Page title",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Page title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": true
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Rankings"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "YOUR_SPREADSHEET_ID"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "id": "credential-id",
          "name": "Google Sheets account"
        }
      },
      "typeVersion": 4.6
    },
    {
      "id": "ebe5ea5f-bd7f-45cb-a985-da050c843102",
      "name": "Step note - Every morning at 6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -740,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 1. Runs itself\n\nFires every morning at 06:00. Change the hour here."
      },
      "typeVersion": 1
    },
    {
      "id": "a13cb5fc-f3c2-4d0e-adb3-a1cf3339c57a",
      "name": "Step note - Keywords and domain",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -520,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 2. Your settings\n\nThe only node you need to edit. Your domain, the keywords you want to rank for, and the country and language the search should run in."
      },
      "typeVersion": 1
    },
    {
      "id": "5ff80277-7a45-4cd8-bf0a-4cfc7661ed6c",
      "name": "Step note - One item per keyword",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -300,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 3. One item per keyword\n\nSplits your keyword list so each keyword gets its own search."
      },
      "typeVersion": 1
    },
    {
      "id": "efc8b1ff-be81-410a-815d-c972e40b9788",
      "name": "Step note - One keyword at a time",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -80,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 4. One at a time\n\nLoops over the keywords. Everything to the right runs once per keyword and then comes back here for the next one."
      },
      "typeVersion": 1
    },
    {
      "id": "d3e28220-adf8-4b47-a757-7bb81ba8ec14",
      "name": "Step note - Search Google",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        160,
        200
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 5. Ask Scrapewise\n\nCalls the `google_search` tool, once per keyword. Add a **Bearer Auth** credential with your API key."
      },
      "typeVersion": 1
    },
    {
      "id": "a87abbad-89aa-420a-acc3-c5b5ce41d548",
      "name": "Step note - Find my position",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        380,
        200
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 6. Find your place\n\nWalks the results in order and reports the first one that sits on your domain. Writes `not in top results` when you are not there at all."
      },
      "typeVersion": 1
    },
    {
      "id": "1011f3fb-1808-42e0-983d-46e73a53c2cf",
      "name": "Step note - Save rankings to Google Sheets",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        600,
        200
      ],
      "parameters": {
        "color": 7,
        "width": 220,
        "height": 240,
        "content": "### 7. Save\n\nOne row per keyword per day, so you can see which way you are moving."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Every morning at 6": {
      "main": [
        [
          {
            "node": "Keywords and domain",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Keywords and domain": {
      "main": [
        [
          {
            "node": "One item per keyword",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "One item per keyword": {
      "main": [
        [
          {
            "node": "One keyword at a time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "One keyword at a time": {
      "main": [
        [],
        [
          {
            "node": "Search Google",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Google": {
      "main": [
        [
          {
            "node": "Find my position",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Find my position": {
      "main": [
        [
          {
            "node": "Save rankings to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save rankings to Google Sheets": {
      "main": [
        [
          {
            "node": "One keyword at a time",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "pinData": {}
}
