AutomationFlowsAI & RAG › Detect Keyword Cannibalization with Gpt-4o and Google Search Console

Detect Keyword Cannibalization with Gpt-4o and Google Search Console

ByIncrementors @incrementors on n8n.io

AI-Powered Keyword Cannibalization Detection Workflow

Event trigger★★★★☆ complexityAI-powered27 nodesAgentOpenAI ChatOutput Parser StructuredGoogle Sheets TriggerGoogle SheetsHTTP Request
AI & RAG Trigger: Event Nodes: 27 Complexity: ★★★★☆ AI nodes: yes Added:

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

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

The workflow JSON

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

Download .json
{
  "nodes": [
    {
      "id": "90168f30-8e88-4b5a-9a80-586ad28f5a8f",
      "name": "Group GSC Data by Keyword (Client 2)",
      "type": "n8n-nodes-base.code",
      "notes": "Groups Google Search Console data by keyword for Client 2. Takes raw GSC response and organizes it by keyword, with each keyword containing an array of URLs that rank for it, including position, clicks, impressions, and CTR data.",
      "position": [
        336,
        464
      ],
      "parameters": {
        "jsCode": "const grouped = {};\n\nfor (const row of items[0].json.rows) {\n  const query = row.keys[0]; // keyword\n  const url = row.keys[1];   // page\n  \n  if (!grouped[query]) grouped[query] = [];\n  \n  grouped[query].push({\n    url,\n    position: row.position,\n    clicks: row.clicks,\n    impressions: row.impressions,\n    ctr: row.ctr  // CTR added here\n  });\n}\n\n// Convert grouped object into array of items for next node\nreturn Object.entries(grouped).map(([keyword, urls]) => ({\n  json: { keyword, urls }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "866dd165-dd86-43b9-b24e-e54ade95c42c",
      "name": "Group GSC Data by Keyword (Client 3)",
      "type": "n8n-nodes-base.code",
      "notes": "Groups Google Search Console data by keyword for Client 3. Transforms the GSC API response into a keyword-centric structure where each keyword has associated URLs with their ranking metrics.",
      "position": [
        336,
        848
      ],
      "parameters": {
        "jsCode": "const grouped = {};\n\nfor (const row of items[0].json.rows) {\n  const query = row.keys[0]; // keyword\n  const url = row.keys[1];   // page\n  \n  if (!grouped[query]) grouped[query] = [];\n  \n  grouped[query].push({\n    url,\n    position: row.position,\n    clicks: row.clicks,\n    impressions: row.impressions,\n    ctr: row.ctr  // CTR added here\n  });\n}\n\n// Convert grouped object into array of items for next node\nreturn Object.entries(grouped).map(([keyword, urls]) => ({\n  json: { keyword, urls }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "e679fffe-fb2b-4cff-b5a0-104179212ac0",
      "name": "Group GSC Data by Keyword (Client 4)",
      "type": "n8n-nodes-base.code",
      "notes": "Groups Google Search Console data by keyword for Client 4. Processes raw GSC data and restructures it to group all URLs ranking for each keyword together with their performance metrics.",
      "position": [
        336,
        1040
      ],
      "parameters": {
        "jsCode": "const grouped = {};\n\nfor (const row of items[0].json.rows) {\n  const query = row.keys[0]; // keyword\n  const url = row.keys[1];   // page\n  \n  if (!grouped[query]) grouped[query] = [];\n  \n  grouped[query].push({\n    url,\n    position: row.position,\n    clicks: row.clicks,\n    impressions: row.impressions,\n    ctr: row.ctr  // CTR added here\n  });\n}\n\n// Convert grouped object into array of items for next node\nreturn Object.entries(grouped).map(([keyword, urls]) => ({\n  json: { keyword, urls }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "7f125793-0b03-4466-b25b-1ef3bb4a76d4",
      "name": "Group GSC Data by Keyword (Client 1)",
      "type": "n8n-nodes-base.code",
      "notes": "Groups Google Search Console data by keyword for Client 1. Converts the flat GSC response into a grouped structure where each keyword contains all its ranking URLs with position, clicks, impressions, and CTR.",
      "position": [
        336,
        272
      ],
      "parameters": {
        "jsCode": "const grouped = {};\n\nfor (const row of items[0].json.rows) {\n  const query = row.keys[0]; // keyword\n  const url = row.keys[1];   // page\n  \n  if (!grouped[query]) grouped[query] = [];\n  \n  grouped[query].push({\n    url,\n    position: row.position,\n    clicks: row.clicks,\n    impressions: row.impressions,\n    ctr: row.ctr  // CTR added here\n  });\n}\n\n// Convert grouped object into array of items for next node\nreturn Object.entries(grouped).map(([keyword, urls]) => ({\n  json: { keyword, urls }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "94c42a7f-50aa-48c9-9643-58eeaddd8ff8",
      "name": "Merge All Client GSC Data",
      "type": "n8n-nodes-base.merge",
      "notes": "Combines GSC data from all 4 clients plus the target keywords from the Google Sheet. This creates a unified dataset containing both the keyword targets and actual GSC performance data.",
      "position": [
        800,
        608
      ],
      "parameters": {
        "numberInputs": 5
      },
      "typeVersion": 3.2
    },
    {
      "id": "d79e5c22-ac7a-43f9-af04-0997fc7af3ce",
      "name": "Match Keywords from Sheet with GSC Data",
      "type": "n8n-nodes-base.code",
      "notes": "Cross-references target keywords from the Google Sheet with actual GSC performance data. Identifies which target keywords are ranking in GSC and which are missing, adding status flags for tracking.",
      "position": [
        1056,
        656
      ],
      "parameters": {
        "jsCode": "const out = [];\n\n// Step 1: Collect all Google Sheet keywords\nconst sheetKeywords = items\n  .filter(i => i.json.Targetted_Keywords) // from Sheets\n  .map(i => (i.json.Targetted_Keywords || \"\").toLowerCase().trim());\n\n// Deduplicate\nconst uniqueSheetKeywords = [...new Set(sheetKeywords)];\n\n// Step 2: Collect all GSC keywords\nconst gscData = items.filter(i => i.json.keyword);\n\n// Step 3: Match - GSC keywords found in sheet\nconst foundKeywords = new Set();\n\nfor (const item of gscData) {\n  const gscKeyword = (item.json.keyword || \"\").toLowerCase().trim();\n  \n  if (uniqueSheetKeywords.includes(gscKeyword)) {\n    foundKeywords.add(gscKeyword); // Track found keywords\n    \n    // Make sure URLs array includes CTR for each URL\n    const urlsWithCtr = (item.json.urls || []).map(urlObj => ({\n      url: urlObj.url,\n      position: urlObj.position,\n      clicks: urlObj.clicks,\n      impressions: urlObj.impressions,\n      ctr: urlObj.ctr  // CTR included here\n    }));\n    \n    out.push({\n      json: {\n        keyword: item.json.keyword,\n        urls: urlsWithCtr,\n        status: 'found_in_gsc' // Optional: to identify matched keywords\n      }\n    });\n  }\n}\n\n// Step 4: Add sheet keywords that were NOT found in GSC\nfor (const sheetKeyword of uniqueSheetKeywords) {\n  if (!foundKeywords.has(sheetKeyword)) {\n    out.push({\n      json: {\n        keyword: sheetKeyword,\n        urls: [], // No URLs since not found in GSC\n        status: 'not_found_in_gsc' // Optional: to identify missing keywords\n      }\n    });\n  }\n}\n\nreturn out;"
      },
      "typeVersion": 2
    },
    {
      "id": "2f8ae342-1586-4f0f-8cd5-d650c4a62b30",
      "name": "Analyze Keyword Cannibalization Risk",
      "type": "@n8n/n8n-nodes-langchain.agent",
      "notes": "Uses AI to analyze keyword cannibalization risk by examining how many pages from the same domain rank for each keyword. Categorizes risk as High, Moderate, Low, or No risk based on page count and performance distribution.",
      "position": [
        1744,
        352
      ],
      "parameters": {
        "text": "=You are a Keyword Cannibalization Risk Detector.\n\nYou will receive:\n\n{{ $json.keyword }}\n{{ JSON.stringify($json.urls) }}\n\n\n\n\nYour Tasks:\n\nExtract the domain from each URL and group the pages by domain.\n\nFor each domain, analyze whether multiple pages are competing for the same keyword.\n\nHigh \u2192 5 or more pages from the same domain rank for the keyword.\n\nModerate \u2192 3 pages from the same domain rank closely in the top 10.\n\nLow \u2192 2 pages rank, but one clearly dominates in clicks/impressions.\n\nNo \u2192 Only 1 page from that domain ranks.\n\nIf the main domain's homepage is ranking at position 1 AND other URLs from the same domain are also ranking, still classify as appropriate risk level based on total page count (don't give them No Risk just because the homepage ranks #1).\n\nIf multiple different domains rank for the same keyword, highlight cross-domain competition separately.\n\nReturn your findings in structured and concise form, showing the keyword, domains, and their respective risk levels.",
        "options": {},
        "promptType": "define",
        "hasOutputParser": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "0957bb5f-6200-40a4-a61a-002c965b53ee",
      "name": "OpenAI GPT-4o Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "notes": "Provides the AI language model (GPT-4o) for the cannibalization analysis agent. Handles the natural language processing to understand keyword competition patterns.",
      "position": [
        1728,
        544
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o",
          "cachedResultName": "gpt-4o"
        },
        "options": {}
      },
      "typeVersion": 1.2
    },
    {
      "id": "503e683a-53c1-4aed-a86d-49ccdecd8b84",
      "name": "Parse AI Analysis to Structured JSON",
      "type": "@n8n/n8n-nodes-langchain.outputParserStructured",
      "notes": "Converts the AI agent's natural language response into a structured JSON format with specific fields for keyword, domain, URLs, risk level, reasoning, observations, summary, and remediation steps.",
      "position": [
        1904,
        544
      ],
      "parameters": {
        "jsonSchemaExample": "{\n  \"Keyword\": \"\",\n  \"Domain\": \"\",\n  \"URLs for Keyword\": [\n    {\n      \"url\": \"\",\n      \"position\": \"\",\n      \"clicks\": \"\",\n      \"impressions\": \"\",\n      \"ctr\": \"\"\n    }\n  ],\n  \"Risk Level\": \"\",\n  \"Reasoning\": \"\",\n  \"Observation\": \"\",\n  \"Summary\": \"\",\n  \"Remediation steps\": \"\"\n}\n"
      },
      "typeVersion": 1.3
    },
    {
      "id": "7ab77205-1560-45ae-8a94-fb088dcfa62e",
      "name": "Monitor Keywords Sheet for Changes",
      "type": "n8n-nodes-base.googleSheetsTrigger",
      "notes": "Monitors the Keywords Google Sheet for any changes and triggers the workflow when modifications are detected. Polls every minute to ensure real-time processing of keyword updates.",
      "maxTries": 5,
      "position": [
        -944,
        656
      ],
      "parameters": {
        "options": {},
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1256649775,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1UfQvam8UhT58LSBobnyFsd3fi1dk7ilzvbQ57rFTIIo/edit#gid=1256649775",
          "cachedResultName": "Keywords"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/1UfQvam8UhT58LSBobnyFsd3fi1dk7ilzvbQ57rFTIIo/edit?gid=1256649775#gid=1256649775"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1
    },
    {
      "id": "6dbee777-42fc-4576-a27b-4650af47455e",
      "name": "Save Cannibalization Analysis Results",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Writes the final cannibalization analysis results back to Google Sheets. Updates or appends rows with risk levels, reasoning, observations, remediation steps, and all associated keyword data.",
      "position": [
        2272,
        672
      ],
      "parameters": {
        "columns": {
          "value": {
            "Data": "={{ $json.output['URLs for Keyword'].map(i => `${i.url} | Position: ${i.position} | Clicks: ${i.clicks} | Impressions: ${i.impressions} | CTR: ${i.ctr}`).join('\n') }}",
            "Date": "={{ $now.format('yyyy-MM-dd') }}",
            "Domain": "={{ $json.output.Domain }}",
            "Status": "={{ $('Match Keywords from Sheet with GSC Data').item.json.status }}",
            "Summary": "={{ $json.output.Summary }}",
            "Reasoning": "={{ $json.output.Reasoning }}",
            "Risk Level": "={{ $json.output['Risk Level'] }}",
            "Observation": "={{ $json.output.Observation }}",
            "Target page": "={{ $json.output['URLs for Keyword'].map(u => u.url).join(', ') }}\n",
            "remediation steps": "={{ $json.output['remediation steps'] }}",
            "Targetted_Keywords": "={{ $json.output.Keyword }}\n{{ $json.keyword }}"
          },
          "schema": [
            {
              "id": "Targetted_Keywords",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Targetted_Keywords",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Status",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Status",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Domain",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Domain",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Date",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Date",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Target page",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Target page",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Data",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Data",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Risk Level",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Risk Level",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Reasoning",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Reasoning",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Observation",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Observation",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Summary",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Summary",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "remediation steps",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "remediation steps",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "Targetted_Keywords"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "appendOrUpdate",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1761789723,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1UfQvam8UhT58LSBobnyFsd3fi1dk7ilzvbQ57rFTIIo/edit#gid=1761789723",
          "cachedResultName": "data"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/1UfQvam8UhT58LSBobnyFsd3fi1dk7ilzvbQ57rFTIIo/edit?gid=1256649775#gid=1256649775"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "f78e179c-dedd-4d79-9f96-ab40fbe40f3f",
      "name": "Route to Client 1",
      "type": "n8n-nodes-base.if",
      "notes": "Routes workflow execution to Client 1's GSC data fetching if the client website matches the specified URL pattern. Acts as a conditional switch for multi-client processing.",
      "position": [
        -288,
        288
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "loose"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "9e1de819-aebf-4242-8f79-7262e422eb57",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json['Client Website'].trimStart().trimEnd() }}",
              "rightValue": "https://theshroomgroove.com/"
            }
          ]
        },
        "looseTypeValidation": true
      },
      "executeOnce": false,
      "typeVersion": 2.2
    },
    {
      "id": "f44239f6-1831-4546-a829-2abf27d4eb2e",
      "name": "Route to Client 2",
      "type": "n8n-nodes-base.if",
      "notes": "Routes workflow execution to Client 2's GSC data fetching when the client website matches the specified domain. Enables parallel processing of multiple clients.",
      "position": [
        -288,
        480
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "9e1de819-aebf-4242-8f79-7262e422eb57",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json['Client Website'].trimStart().trimEnd() }}",
              "rightValue": "grooveguide.io"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "fc7efa81-5870-4bc7-bd21-2181214976ff",
      "name": "Route to Client 3",
      "type": "n8n-nodes-base.if",
      "notes": "Directs the workflow to fetch GSC data for Client 3 when the website URL matches the condition. Part of the multi-client routing logic to handle different domains.",
      "position": [
        -288,
        864
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "9e1de819-aebf-4242-8f79-7262e422eb57",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json['Client Website'].trimStart().trimEnd() }}",
              "rightValue": "https://groovegrillwellness.com/"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "c91f5ab4-29a1-4f83-b570-3fdf4b3e7c1a",
      "name": "Route to Client 4",
      "type": "n8n-nodes-base.if",
      "notes": "Routes to Client 4's GSC data collection process when the website URL condition is met. Completes the multi-client routing system for parallel data processing.",
      "position": [
        -288,
        1056
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "9e1de819-aebf-4242-8f79-7262e422eb57",
              "operator": {
                "type": "string",
                "operation": "contains"
              },
              "leftValue": "={{ $json['Client Website'].trimStart().trimEnd() }}",
              "rightValue": "https://example.com/"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "2c0ba70f-00df-4be9-98a2-7969653e10db",
      "name": "Fetch Client Website URLs",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Retrieves the list of client website URLs from the Google Sheet. This data is used to determine which clients to process and route them to their respective GSC data collection paths.",
      "position": [
        -656,
        656
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 146956146,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1UfQvam8UhT58LSBobnyFsd3fi1dk7ilzvbQ57rFTIIo/edit#gid=146956146",
          "cachedResultName": "URL"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1UfQvam8UhT58LSBobnyFsd3fi1dk7ilzvbQ57rFTIIo",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1UfQvam8UhT58LSBobnyFsd3fi1dk7ilzvbQ57rFTIIo/edit?usp=drivesdk",
          "cachedResultName": "Client URLs"
        }
      },
      "executeOnce": false,
      "typeVersion": 4.7
    },
    {
      "id": "a6a60ba4-3b48-4750-af64-bf19c22f6b5c",
      "name": "Fetch Target Keywords from Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "notes": "Reads the target keywords from the Google Sheet that need to be analyzed for cannibalization. These keywords serve as the reference list to match against actual GSC performance data.",
      "position": [
        -288,
        656
      ],
      "parameters": {
        "options": {},
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": 1256649775,
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1UfQvam8UhT58LSBobnyFsd3fi1dk7ilzvbQ57rFTIIo/edit#gid=1256649775",
          "cachedResultName": "Keywords"
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "1UfQvam8UhT58LSBobnyFsd3fi1dk7ilzvbQ57rFTIIo",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1UfQvam8UhT58LSBobnyFsd3fi1dk7ilzvbQ57rFTIIo/edit?usp=drivesdk",
          "cachedResultName": "Client URLs"
        }
      },
      "executeOnce": true,
      "typeVersion": 4.7
    },
    {
      "id": "5a72cf8f-cf73-4811-9b55-807f7966e826",
      "name": "Fetch GSC Data (Client 1)",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Makes API call to Google Search Console for Client 1 to retrieve the last 30 days of search performance data. Gets keyword-page combinations with position, clicks, impressions, and CTR metrics.",
      "onError": "continueRegularOutput",
      "position": [
        48,
        272
      ],
      "parameters": {
        "url": "=https://www.googleapis.com/webmasters/v3/sites/{{ encodeURIComponent($json['Client Website']) }}/searchAnalytics/query",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"startDate\": \"{{ $now.minus(30, 'days').format('yyyy-MM-dd') }}\",\n  \"endDate\": \"{{ $now.format('yyyy-MM-dd') }}\",\n  \"dimensions\": [\"query\", \"page\"]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleOAuth2Api"
      },
      "executeOnce": true,
      "typeVersion": 4.2
    },
    {
      "id": "03e271fe-0bfd-4f75-85af-cfdf0bcb8ac0",
      "name": "Filter Keywords Found in GSC",
      "type": "n8n-nodes-base.if",
      "notes": "Filters out keywords that were not found in GSC data, only passing through keywords that actually have ranking performance data. Prevents AI analysis of keywords with no GSC presence.",
      "position": [
        1424,
        656
      ],
      "parameters": {
        "options": {
          "ignoreCase": true
        },
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": false,
            "typeValidation": "loose"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "ccec25a9-75f6-4c82-a93f-aafb28aa3633",
              "operator": {
                "type": "string",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.status }}",
              "rightValue": "not_found_in_gsc"
            }
          ]
        },
        "looseTypeValidation": true
      },
      "typeVersion": 2.2
    },
    {
      "id": "df3c0be2-7000-4821-8eb2-bbd9fee541f6",
      "name": "Fetch GSC Data (Client 2)",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Retrieves Google Search Console data for Client 2 using domain property format. Collects 30 days of search analytics data including keywords, pages, positions, and engagement metrics.",
      "onError": "continueRegularOutput",
      "position": [
        48,
        464
      ],
      "parameters": {
        "url": "=https://www.googleapis.com/webmasters/v3/sites/sc-domain:{{ $json['Client Website'] }}/searchAnalytics/query",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"startDate\": \"{{ $now.minus(30, 'days').format('yyyy-MM-dd') }}\",\n  \"endDate\": \"{{ $now.format('yyyy-MM-dd') }}\",\n  \"dimensions\": [\"query\", \"page\"]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleOAuth2Api"
      },
      "executeOnce": true,
      "typeVersion": 4.2
    },
    {
      "id": "88d5018c-546f-4299-9ad9-d77065450cb6",
      "name": "Fetch GSC Data (Client 3)",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Connects to Google Search Console API for Client 3 to extract search performance data. Gathers keyword rankings, page URLs, positions, clicks, impressions, and CTR for the past 30 days.",
      "onError": "continueRegularOutput",
      "position": [
        48,
        848
      ],
      "parameters": {
        "url": "=https://www.googleapis.com/webmasters/v3/sites/{{ encodeURIComponent($json['Client Website']) }}/searchAnalytics/query",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"startDate\": \"{{ $now.minus(30, 'days').format('yyyy-MM-dd') }}\",\n  \"endDate\": \"{{ $now.format('yyyy-MM-dd') }}\",\n  \"dimensions\": [\"query\", \"page\"]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleOAuth2Api"
      },
      "executeOnce": true,
      "typeVersion": 4.2
    },
    {
      "id": "a7f0a16a-ca27-4bb2-b5ba-5b74759f5f70",
      "name": "Fetch GSC Data (Client 4)",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Pulls Google Search Console analytics data for Client 4 covering the last 30 days. Retrieves comprehensive search performance metrics including keyword-page relationships and ranking positions.",
      "onError": "continueRegularOutput",
      "position": [
        48,
        1040
      ],
      "parameters": {
        "url": "=https://www.googleapis.com/webmasters/v3/sites/{{ encodeURIComponent($json['Client Website']) }}/searchAnalytics/query",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"startDate\": \"{{ $now.minus(30, 'days').format('yyyy-MM-dd') }}\",\n  \"endDate\": \"{{ $now.format('yyyy-MM-dd') }}\",\n  \"dimensions\": [\"query\", \"page\"]\n}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "predefinedCredentialType",
        "nodeCredentialType": "googleOAuth2Api"
      },
      "executeOnce": true,
      "typeVersion": 4.2
    },
    {
      "id": "9d911368-b656-4519-8a24-36220cb9f4e0",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1008,
        416
      ],
      "parameters": {
        "width": 480,
        "height": 416,
        "content": "## Monitor Keywords Sheet for Changes\nMonitors the Keywords Google Sheet for any changes and triggers the workflow when modifications are detected. Polls every minute to ensure real-time processing of keyword updates.\n\n## Fetch Client Website URLs\nRetrieves the list of client website URLs from the Google Sheet. This data is used to determine which clients to process and route them to their respective GSC data collection paths.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "ddd27041-06de-475b-bb47-2d1a6032be6c",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -64,
        -48
      ],
      "parameters": {
        "width": 656,
        "height": 1248,
        "content": "## Fetch GSC Data (Client 1-4)\nMakes API call to Google Search Console for Client [X] to retrieve the last 30 days of search performance data. Gets keyword-page combinations with position, clicks, impressions, and CTR metrics.\n\n## Group GSC Data by Keyword (Client 1-4)\nGroups Google Search Console data by keyword for Client [X]. Takes raw GSC response and organizes it by keyword, with each keyword containing an array of URLs that rank for it, including position, clicks, impressions, and CTR data.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "581173a3-79e0-415d-99d2-a971bb5318d1",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        992,
        368
      ],
      "parameters": {
        "height": 448,
        "content": "## Match Keywords from Sheet with GSC Data\nCross-references target keywords from the Google Sheet with actual GSC performance data. Identifies which target keywords are ranking in GSC and which are missing, adding status flags for tracking."
      },
      "typeVersion": 1
    },
    {
      "id": "f6343388-a09f-433d-94dc-a6c7d9195eec",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1360,
        64
      ],
      "parameters": {
        "width": 1104,
        "height": 816,
        "content": "## Filter Keywords Found in GSC\nFilters out keywords that were not found in GSC data, only passing through keywords that actually have ranking performance data. Prevents AI analysis of keywords with no GSC presence.\n## Analyze Keyword Cannibalization Risk\nUses AI to analyze keyword cannibalization risk by examining how many pages from the same domain rank for each keyword. Categorizes risk as High, Moderate, Low, or No risk based on page count and performance distribution.\n## Save Cannibalization Analysis Results\nWrites the final cannibalization analysis results back to Google Sheets. Updates or appends rows with risk levels, reasoning, observations, remediation steps, and all associated keyword data.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "2e15b4f3-f8ac-41f6-a50f-beb2af6f384d",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1664,
        64
      ],
      "parameters": {
        "width": 432,
        "height": 1040,
        "content": "## Keyword Cannibalization Detection Workflow Summary\n### Overview\nThis n8n workflow is an automated keyword cannibalization detection system that monitors multiple client websites and analyzes their search performance using Google Search Console data and AI-powered risk assessment.\n\n### Workflow Process\n\ud83d\udd04 Automated Trigger: The workflow monitors a Google Sheets document for keyword changes, triggering execution every minute when modifications are detected to ensure real-time processing.\n\n\ud83d\udcca Multi-Client Data Collection: The system simultaneously processes up to 4 different client websites through intelligent routing nodes that direct each client's data to dedicated processing paths based on URL pattern matching.\n\n\ud83d\udd0d GSC Data Extraction: For each client, the workflow makes API calls to Google Search Console to retrieve 30 days of search performance data, collecting keyword rankings, page URLs, positions, clicks, impressions, and CTR metrics.\n\n\u2699\ufe0f Data Transformation: Raw GSC API responses are processed through JavaScript code nodes that group flat data by keywords, creating structured datasets where each keyword contains all competing URLs with their complete performance metrics.\n\n\ud83d\udd17 Data Integration: A merge node combines GSC data from all clients with target keywords from the Google Sheet, then cross-references to identify which keywords are actually ranking versus those missing from search results.\n\n\ud83e\udd16 AI-Powered Analysis: The system uses GPT-4o to analyze keyword cannibalization risk by examining how many pages from the same domain compete for each keyword, automatically categorizing risk levels as High (5+ pages), Moderate (3+ pages), Low (2 pages with clear dominance), or No Risk (single page).\n\n\ud83d\udcbe Automated Reporting: Final analysis results are written back to Google Sheets with comprehensive data including risk assessments, detailed reasoning, observations, actionable remediation steps, and complete performance metrics for client reporting."
      },
      "typeVersion": 1
    }
  ],
  "connections": {
    "Route to Client 1": {
      "main": [
        [
          {
            "node": "Fetch GSC Data (Client 1)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route to Client 2": {
      "main": [
        [
          {
            "node": "Fetch GSC Data (Client 2)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route to Client 3": {
      "main": [
        [
          {
            "node": "Fetch GSC Data (Client 3)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route to Client 4": {
      "main": [
        [
          {
            "node": "Fetch GSC Data (Client 4)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI GPT-4o Model": {
      "ai_languageModel": [
        [
          {
            "node": "Analyze Keyword Cannibalization Risk",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Client Website URLs": {
      "main": [
        [
          {
            "node": "Route to Client 1",
            "type": "main",
            "index": 0
          },
          {
            "node": "Route to Client 2",
            "type": "main",
            "index": 0
          },
          {
            "node": "Route to Client 3",
            "type": "main",
            "index": 0
          },
          {
            "node": "Route to Client 4",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch GSC Data (Client 1)": {
      "main": [
        [
          {
            "node": "Group GSC Data by Keyword (Client 1)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch GSC Data (Client 2)": {
      "main": [
        [
          {
            "node": "Group GSC Data by Keyword (Client 2)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch GSC Data (Client 3)": {
      "main": [
        [
          {
            "node": "Group GSC Data by Keyword (Client 3)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch GSC Data (Client 4)": {
      "main": [
        [
          {
            "node": "Group GSC Data by Keyword (Client 4)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge All Client GSC Data": {
      "main": [
        [
          {
            "node": "Match Keywords from Sheet with GSC Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Filter Keywords Found in GSC": {
      "main": [
        [
          {
            "node": "Analyze Keyword Cannibalization Risk",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Save Cannibalization Analysis Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Target Keywords from Sheet": {
      "main": [
        [
          {
            "node": "Merge All Client GSC Data",
            "type": "main",
            "index": 2
          }
        ]
      ]
    },
    "Monitor Keywords Sheet for Changes": {
      "main": [
        [
          {
            "node": "Fetch Client Website URLs",
            "type": "main",
            "index": 0
          },
          {
            "node": "Fetch Target Keywords from Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze Keyword Cannibalization Risk": {
      "main": [
        [
          {
            "node": "Save Cannibalization Analysis Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Group GSC Data by Keyword (Client 1)": {
      "main": [
        [
          {
            "node": "Merge All Client GSC Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Group GSC Data by Keyword (Client 2)": {
      "main": [
        [
          {
            "node": "Merge All Client GSC Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Group GSC Data by Keyword (Client 3)": {
      "main": [
        [
          {
            "node": "Merge All Client GSC Data",
            "type": "main",
            "index": 3
          }
        ]
      ]
    },
    "Group GSC Data by Keyword (Client 4)": {
      "main": [
        [
          {
            "node": "Merge All Client GSC Data",
            "type": "main",
            "index": 4
          }
        ]
      ]
    },
    "Parse AI Analysis to Structured JSON": {
      "ai_outputParser": [
        [
          {
            "node": "Analyze Keyword Cannibalization Risk",
            "type": "ai_outputParser",
            "index": 0
          }
        ]
      ]
    },
    "Match Keywords from Sheet with GSC Data": {
      "main": [
        [
          {
            "node": "Filter Keywords Found in GSC",
            "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

AI-Powered Keyword Cannibalization Detection Workflow

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

More AI & RAG workflows → · Browse all categories →

Related workflows

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

AI & RAG

This automation is designed to help you generate AI-powered music tracks, cover art, and fully rendered music videos — all triggered from a simple Telegram chat and managed via Google Sheets.

OpenAI Chat, Memory Buffer Window, Output Parser Structured +11
AI & RAG

Logistics teams spend hours manually validating shipment data, checking compliance, generating freight documents, and emailing stakeholders. Errors in HSN codes, weights, or carrier details can lead t

Google Sheets Trigger, OpenAI Chat, Output Parser Structured +6
AI & RAG

Googletranslate Noop. Uses stickyNote, googleTranslate, agent, lmChatOpenAi. Event-driven trigger; 22 nodes.

Google Translate, Agent, OpenAI Chat +5
AI & RAG

This workflow streamlines how new Google Form submissions are processed by automatically creating GitHub issues and sending real-time notifications to a Discord channel through a webhook. Developers l

OpenAI Chat, Memory Buffer Window, Output Parser Structured +5
AI & RAG

LinkedIn AI Agent: Auto-Post Creator & Multi-Group Distributor Transform simple topic ideas into engaging LinkedIn posts and automatically distribute them across your profile and multiple LinkedIn gro

Output Parser Structured, OpenAI Chat, Google Sheets Trigger +3