{
  "id": "EdbRd7sQPXqlrRrY",
  "name": "Track Google Rankings Automatically with Decodo & Google Sheets",
  "tags": [],
  "nodes": [
    {
      "id": "sched_1",
      "name": "Schedule Run",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        240,
        192
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "set_input_1",
      "name": "Input Defaults",
      "type": "n8n-nodes-base.set",
      "position": [
        448,
        192
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "a1",
              "name": "keyword",
              "type": "string",
              "value": "best standing desk"
            },
            {
              "id": "a2",
              "name": "country",
              "type": "string",
              "value": "USA"
            },
            {
              "id": "a3",
              "name": "language",
              "type": "string",
              "value": "en"
            },
            {
              "id": "a4",
              "name": "device",
              "type": "string",
              "value": "desktop"
            },
            {
              "id": "a5",
              "name": "top_n",
              "type": "number",
              "value": 5
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "http_decodo_1",
      "name": "Fetch Google SERP",
      "type": "@decodo/n8n-nodes-decodo.decodo",
      "position": [
        688,
        192
      ],
      "parameters": {
        "geo": "={{ $json.country }}",
        "query": "={{ $json.keyword }}",
        "locale": "={{ $json.language }}",
        "operation": "google_search"
      },
      "credentials": {
        "decodoApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "if_has_results_1",
      "name": "Check Results",
      "type": "n8n-nodes-base.if",
      "position": [
        960,
        192
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "c1",
              "operator": {
                "type": "number",
                "operation": "gte"
              },
              "leftValue": "={{ Array.isArray($json.results) ? $json.results.length : 0 }}",
              "rightValue": 1
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "split_out_1",
      "name": "Split Payload Results",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        1200,
        176
      ],
      "parameters": {
        "include": "allOtherFields",
        "options": {},
        "fieldToSplitOut": "results"
      },
      "typeVersion": 1
    },
    {
      "id": "set_norm_1",
      "name": "Map Result Row",
      "type": "n8n-nodes-base.set",
      "position": [
        1920,
        176
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "n1",
              "name": "keyword",
              "type": "string",
              "value": "={{ $(\"Input Defaults\").item.json.keyword }}"
            },
            {
              "id": "n2",
              "name": "country",
              "type": "string",
              "value": "={{ $(\"Input Defaults\").item.json.country }}"
            },
            {
              "id": "n3",
              "name": "language",
              "type": "string",
              "value": "={{ $(\"Input Defaults\").item.json.language }}"
            },
            {
              "id": "n4",
              "name": "device",
              "type": "string",
              "value": "={{ $(\"Input Defaults\").item.json.device }}"
            },
            {
              "id": "n5",
              "name": "rank",
              "type": "string",
              "value": "={{ $json.organic.pos_overall || $json.organic.pos || \"\" }}"
            },
            {
              "id": "n6",
              "name": "title",
              "type": "string",
              "value": "={{ $json.organic.title || \"\" }}"
            },
            {
              "id": "n7",
              "name": "url",
              "type": "string",
              "value": "={{ $json.organic.url || \"\" }}"
            },
            {
              "id": "n8",
              "name": "description",
              "type": "string",
              "value": "={{ $json.organic.desc || \"\" }}"
            },
            {
              "id": "n9",
              "name": "checkedAt",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "if_valid_row_1",
      "name": "Check Row Valid",
      "type": "n8n-nodes-base.if",
      "position": [
        2160,
        176
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "u1",
              "operator": {
                "type": "string",
                "operation": "notEmpty"
              },
              "leftValue": "={{ $json.url || \"\" }}",
              "rightValue": ""
            },
            {
              "id": "u2",
              "operator": {
                "type": "number",
                "operation": "lte"
              },
              "leftValue": "={{ Number($json.rank || 999) }}",
              "rightValue": "={{ Number($(\"Input Defaults\").item.json.top_n || 5) }}"
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "gs_success_1",
      "name": "Save SERP Results",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2448,
        160
      ],
      "parameters": {
        "columns": {
          "value": {
            "URL": "={{ $json.url }}",
            "Rank": "={{ $json.rank }}",
            "Title": "={{ $json.title }}",
            "Keyword": "={{ $json.keyword }}",
            "Description": "={{ $json.description }}"
          },
          "schema": [
            {
              "id": "Keyword",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Keyword",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Title",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "URL",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "URL",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Description",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Description",
              "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": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Au8cuHixZqIgrfYd5QZcViMyhq9Ee-5qhNWcmCRGePI/edit#gid=0",
          "cachedResultName": " SERP_Results"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Au8cuHixZqIgrfYd5QZcViMyhq9Ee-5qhNWcmCRGePI",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Au8cuHixZqIgrfYd5QZcViMyhq9Ee-5qhNWcmCRGePI/edit?usp=drivesdk",
          "cachedResultName": "Keyword monitoring"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "set_err_no_results_1",
      "name": "Build Empty Error",
      "type": "n8n-nodes-base.set",
      "position": [
        2448,
        320
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "e1",
              "name": "checkedAt",
              "type": "string",
              "value": "={{ $now.toISO() }}"
            },
            {
              "id": "e2",
              "name": "keyword",
              "type": "string",
              "value": "={{ $(\"Input Defaults\").item.json.keyword }}"
            },
            {
              "id": "e3",
              "name": "country",
              "type": "string",
              "value": "={{ $(\"Input Defaults\").item.json.country }}"
            },
            {
              "id": "e4",
              "name": "language",
              "type": "string",
              "value": "={{ $(\"Input Defaults\").item.json.language }}"
            },
            {
              "id": "e5",
              "name": "device",
              "type": "string",
              "value": "={{ $(\"Input Defaults\").item.json.device }}"
            },
            {
              "id": "e6",
              "name": "errorStage",
              "type": "string",
              "value": "no_results"
            },
            {
              "id": "e7",
              "name": "errorMessage",
              "type": "string",
              "value": "={{ $json.message || ($json.error && $json.error.message) || \"Decodo returned empty results or unexpected payload.\" }}"
            },
            {
              "id": "e8",
              "name": "httpStatus",
              "type": "string",
              "value": "={{ $json.statusCode || $json.code || \"\" }}"
            },
            {
              "id": "e9",
              "name": "rawPreview",
              "type": "string",
              "value": "={{ JSON.stringify($json).slice(0, 300) }}"
            },
            {
              "id": "e10",
              "name": "workflowId",
              "type": "string",
              "value": "EdbRd7sQPXqlrRrY"
            },
            {
              "id": "e11",
              "name": "executionId",
              "type": "string",
              "value": "={{ $execution.id || \"\" }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "set_err_bad_item_1",
      "name": "Build Row Error",
      "type": "n8n-nodes-base.set",
      "position": [
        2672,
        208
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "f1",
              "name": "checkedAt",
              "type": "string",
              "value": "={{ $json.checkedAt || $now.toISO() }}"
            },
            {
              "id": "f2",
              "name": "keyword",
              "type": "string",
              "value": "={{ $json.keyword || \"\" }}"
            },
            {
              "id": "f3",
              "name": "country",
              "type": "string",
              "value": "={{ $json.country || \"\" }}"
            },
            {
              "id": "f4",
              "name": "language",
              "type": "string",
              "value": "={{ $json.language || \"\" }}"
            },
            {
              "id": "f5",
              "name": "device",
              "type": "string",
              "value": "={{ $json.device || \"\" }}"
            },
            {
              "id": "f6",
              "name": "errorStage",
              "type": "string",
              "value": "invalid_item"
            },
            {
              "id": "f7",
              "name": "errorMessage",
              "type": "string",
              "value": "={{ \"Result missing URL. title=\" + ($json.title || \"\") }}"
            },
            {
              "id": "f8",
              "name": "httpStatus",
              "type": "string",
              "value": ""
            },
            {
              "id": "f9",
              "name": "rawPreview",
              "type": "string",
              "value": "={{ JSON.stringify($json).slice(0, 300) }}"
            },
            {
              "id": "f10",
              "name": "workflowId",
              "type": "string",
              "value": "EdbRd7sQPXqlrRrY"
            },
            {
              "id": "f11",
              "name": "executionId",
              "type": "string",
              "value": "={{ $execution.id || \"\" }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "gs_error_1",
      "name": "Save SERP Errors",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        2848,
        320
      ],
      "parameters": {
        "columns": {
          "value": {
            "keyword": "={{ $json.keyword }}",
            " checkedAt": "={{ $json.checkedAt.toDateTime()}}",
            "errorStage": "={{ $json.errorStage }}",
            "httpStatus": "={{ $json.httpStatus }}",
            "rawPreview": "={{ $json.rawPreview }}",
            "errorMessage": "={{ $json.errorMessage }}"
          },
          "schema": [
            {
              "id": " checkedAt",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": " checkedAt",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "keyword",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "keyword",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "errorStage",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "errorStage",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "errorMessage",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "errorMessage",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "httpStatus",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "httpStatus",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "rawPreview",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "rawPreview",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "SERP_Errors"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1Au8cuHixZqIgrfYd5QZcViMyhq9Ee-5qhNWcmCRGePI",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1Au8cuHixZqIgrfYd5QZcViMyhq9Ee-5qhNWcmCRGePI/edit?usp=drivesdk",
          "cachedResultName": "Keyword monitoring"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "extract_organic_1",
      "name": "Extract Organic List",
      "type": "n8n-nodes-base.set",
      "position": [
        1440,
        176
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "o1",
              "name": "organic",
              "type": "array",
              "value": "={{ ($json.results && $json.results.content && $json.results.content.results && $json.results.content.results.results && $json.results.content.results.results.organic) || ($json.content && $json.content.results && $json.content.results.results && $json.content.results.results.organic) || [] }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "split_organic_1",
      "name": "Split Organic Items",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        1680,
        176
      ],
      "parameters": {
        "include": "allOtherFields",
        "options": {},
        "fieldToSplitOut": "organic"
      },
      "typeVersion": 1
    },
    {
      "id": "a747d799-124f-4c76-a0cc-53c05ef39d91",
      "name": "Flow Summary",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -368,
        -288
      ],
      "parameters": {
        "width": 500,
        "height": 750,
        "content": "## Daily SERP monitor: capture top search results\n\n### How it works\n1. Runs on a daily schedule and loads the configured keyword, country, language, device, and top_n.\n2. Calls Decodo to perform a Google search for the keyword and returns SERP payloads.\n3. Extracts organic results, splits them into rows, and maps title, URL, description, rank, and timestamp.\n4. Validates rows (URL present and rank \u2264 top_n). Valid rows are appended to the SERP_Results Google Sheet; invalid rows or API/no-result errors are logged to SERP_Errors with a timestamp and raw preview.\n\n### Setup\n- [ ] Connect your Google Sheets account used to store results.\n- [ ] Add Decodo API credentials in n8n (Decodo Search node).\n- [ ] Edit the \"Set Search Input\" node to set keyword, country, language, device, and top_n.\n- [ ] Update the Google Sheet ID and sheet names (SERP_Results and SERP_Errors).\n- [ ] Set schedule time or run a manual test run.\n- [ ] Verify appended rows and review SERP_Errors for any failures."
      },
      "typeVersion": 1
    },
    {
      "id": "24fc6079-d7de-404b-bfd6-4cf252d67266",
      "name": "Results Mapping Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2320,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 748,
        "height": 444,
        "content": "## Store Results & Log Errors\nKeeps only valid top-ranked rows and appends them to the results sheet. If there are no results or a row is missing key fields, it writes an error entry to the errors sheet."
      },
      "typeVersion": 1
    },
    {
      "id": "86f89d19-935b-4f42-99d8-1232d668a2af",
      "name": "Error Mapping Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        176,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 700,
        "height": 444,
        "content": "## Schedule & Search Setup\nRuns daily and defines thesearch and scrape with decodo"
      },
      "typeVersion": 1
    },
    {
      "id": "1b4a86e5-b42c-4c26-a498-a1a987093699",
      "name": "Input Example Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        896,
        16
      ],
      "parameters": {
        "color": 7,
        "width": 1404,
        "height": 444,
        "content": "## Parse & Filter Top Results\nBreaks the response into items, extracts organic listings, maps fields, and keeps only valid rows within the top-N ranks."
      },
      "typeVersion": 1
    }
  ],
  "active": true,
  "settings": {
    "timezone": "Asia/Jakarta",
    "callerPolicy": "workflowsFromSameOwner",
    "availableInMCP": false,
    "executionOrder": "v1"
  },
  "versionId": "e4a63262-905a-4245-846a-b4a502554ccc",
  "connections": {
    "Schedule Run": {
      "main": [
        [
          {
            "node": "Input Defaults",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Results": {
      "main": [
        [
          {
            "node": "Split Payload Results",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Empty Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Input Defaults": {
      "main": [
        [
          {
            "node": "Fetch Google SERP",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Map Result Row": {
      "main": [
        [
          {
            "node": "Check Row Valid",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Row Error": {
      "main": [
        [
          {
            "node": "Save SERP Errors",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Row Valid": {
      "main": [
        [
          {
            "node": "Save SERP Results",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Build Row Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Empty Error": {
      "main": [
        [
          {
            "node": "Save SERP Errors",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Google SERP": {
      "main": [
        [
          {
            "node": "Check Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Organic Items": {
      "main": [
        [
          {
            "node": "Map Result Row",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Organic List": {
      "main": [
        [
          {
            "node": "Split Organic Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Payload Results": {
      "main": [
        [
          {
            "node": "Extract Organic List",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}