{
  "id": "ITcZfdCK8V0QaprP",
  "meta": {
    "builderVariant": "mcp",
    "aiBuilderAssisted": true
  },
  "name": "Track keyword rankings on Google with Apify and send AI SEO reports by email",
  "tags": [
    {
      "id": "BDQCNxQKN4A7X8jf",
      "name": "apify",
      "createdAt": "2026-07-14T19:34:02.338Z",
      "updatedAt": "2026-07-14T19:34:02.338Z"
    },
    {
      "id": "MwvO3sW35Gd7mwdB",
      "name": "seo",
      "createdAt": "2026-07-14T19:34:02.329Z",
      "updatedAt": "2026-07-14T19:34:02.329Z"
    },
    {
      "id": "wPS89f8pvHV6tTxu",
      "name": "google-sheets",
      "createdAt": "2026-07-14T19:34:02.347Z",
      "updatedAt": "2026-07-14T19:34:02.347Z"
    },
    {
      "id": "yh5MHSGdSao9Ntar",
      "name": "openai",
      "createdAt": "2026-07-14T19:34:02.344Z",
      "updatedAt": "2026-07-14T19:34:02.344Z"
    }
  ],
  "nodes": [
    {
      "id": "a63b5f11-a918-46a6-8d88-7861082d2563",
      "name": "Run daily at 8 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        0,
        112
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "c22a8345-a8dd-40c1-82f6-29fbdc1d2a85",
      "name": "Configuration",
      "type": "n8n-nodes-base.set",
      "position": [
        224,
        112
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-keywords",
              "name": "keywords",
              "type": "string",
              "value": "best running shoes, trail running shoes"
            },
            {
              "id": "cfg-target-domain",
              "name": "target_domain",
              "type": "string",
              "value": "yourwebsite.com"
            },
            {
              "id": "cfg-country",
              "name": "country_code",
              "type": "string",
              "value": "us"
            },
            {
              "id": "cfg-language",
              "name": "language_code",
              "type": "string",
              "value": "en"
            },
            {
              "id": "cfg-results",
              "name": "results_per_keyword",
              "type": "number",
              "value": 10
            },
            {
              "id": "cfg-model",
              "name": "openai_model",
              "type": "string",
              "value": "gpt-4o"
            },
            {
              "id": "cfg-email",
              "name": "report_email",
              "type": "string",
              "value": "you@example.com"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "381dc9c8-f876-400e-9873-0ca0c39764e6",
      "name": "Prepare keyword list",
      "type": "n8n-nodes-base.set",
      "position": [
        448,
        112
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "kw-list",
              "name": "keyword_list",
              "type": "array",
              "value": "={{ $json.keywords.split(\",\").map(k => k.trim()).filter(k => k.length > 0) }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "7103c4e4-b05a-4a92-a5f2-3c28eb4e5e56",
      "name": "Split keywords into items",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        672,
        112
      ],
      "parameters": {
        "options": {
          "destinationFieldName": "keyword"
        },
        "fieldToSplitOut": "keyword_list"
      },
      "typeVersion": 1
    },
    {
      "id": "1110594b-eb25-485b-87c1-f44c532b8462",
      "name": "Scrape Google results with Apify",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        896,
        112
      ],
      "parameters": {
        "url": "https://api.apify.com/v2/acts/apify~google-search-scraper/run-sync-get-dataset-items",
        "method": "POST",
        "options": {
          "timeout": 180000,
          "batching": {
            "batch": {
              "batchSize": 1,
              "batchInterval": 2000
            }
          }
        },
        "jsonBody": "={{ JSON.stringify({ queries: $json.keyword, countryCode: $(\"Configuration\").first().json.country_code, languageCode: $(\"Configuration\").first().json.language_code, resultsPerPage: $(\"Configuration\").first().json.results_per_keyword, maxPagesPerQuery: 1, mobileResults: false }) }}",
        "sendBody": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "0bc738cc-1f50-4088-b8a3-5e1f94711c7d",
      "name": "Extract and rank SERP results",
      "type": "n8n-nodes-base.code",
      "position": [
        1168,
        160
      ],
      "parameters": {
        "jsCode": "const config = $('Configuration').first().json;\nconst targetDomain = String(config.target_domain || '').toLowerCase().replace(/^www\\./, '');\nconst checkedAt = new Date().toISOString().slice(0, 10);\nconst rows = [];\n\nfor (const item of $input.all()) {\n  const page = item.json;\n  const keyword = (page.searchQuery && page.searchQuery.term) ? page.searchQuery.term : '';\n  const results = Array.isArray(page.organicResults) ? page.organicResults : [];\n\n  for (let i = 0; i < results.length; i++) {\n    const r = results[i];\n    let domain = '';\n    try {\n      domain = new URL(r.url).hostname.toLowerCase().replace(/^www\\./, '');\n    } catch (e) {\n      domain = '';\n    }\n\n    rows.push({\n      json: {\n        checked_at: checkedAt,\n        keyword: keyword,\n        rank: r.position || (i + 1),\n        title: r.title || '',\n        url: r.url || '',\n        domain: domain,\n        description: r.description || '',\n        is_target_domain: targetDomain !== '' && domain.endsWith(targetDomain)\n      }\n    });\n  }\n}\n\nreturn rows;"
      },
      "typeVersion": 2
    },
    {
      "id": "f68463e0-c817-4a5b-8d0a-9e72b7c7550c",
      "name": "Log rankings to Google Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1376,
        160
      ],
      "parameters": {
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": ""
        },
        "documentId": {
          "__rl": true,
          "mode": "list",
          "value": "",
          "cachedResultName": "Keyword rankings"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "49f86837-5865-4f52-bf1d-4d515eebd5a2",
      "name": "Combine rankings for AI analysis",
      "type": "n8n-nodes-base.aggregate",
      "position": [
        1616,
        112
      ],
      "parameters": {
        "options": {},
        "aggregate": "aggregateAllItemData",
        "destinationFieldName": "rankings"
      },
      "typeVersion": 1
    },
    {
      "id": "e3549218-e59d-4eed-a763-0c982b453637",
      "name": "Generate AI SEO report",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "position": [
        1792,
        112
      ],
      "parameters": {
        "text": "=Here is the Google SERP ranking data collected today for my tracked keywords.\n\nTarget domain to monitor: {{ $(\"Configuration\").first().json.target_domain }}\nKeywords tracked: {{ $(\"Configuration\").first().json.keywords }}\nDate: {{ $now.toFormat(\"yyyy-MM-dd\") }}\n\nRanking data (JSON):\n{{ JSON.stringify($json.rankings) }}\n\nWrite the SEO report now.",
        "batching": {},
        "messages": {
          "messageValues": [
            {
              "message": "You are a senior SEO analyst. You receive raw Google SERP ranking data (keyword, rank, title, url, domain, is_target_domain) and produce a concise, actionable SEO report.\n\nYour report must contain:\n1. A short executive summary (2-3 sentences).\n2. For each keyword: the current position of the target domain (or state clearly that it is not ranking in the tracked results), and the top 3 competitors with their positions.\n3. A \"Key opportunities\" section with 3-5 concrete, prioritized SEO actions based on the data.\n\nFormat the entire report as clean, simple HTML (use only h2, h3, p, ul, li, strong tags - no markdown, no CSS, no html/head/body wrapper). Keep it under 500 words. Be factual: never invent rankings that are not in the data."
            }
          ]
        },
        "promptType": "define"
      },
      "typeVersion": 1.9
    },
    {
      "id": "fbdf1ba6-139d-4498-b253-fb304cfbc550",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "position": [
        1872,
        336
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "id",
          "value": "={{ $('Configuration').first().json.openai_model }}"
        },
        "options": {},
        "builtInTools": {}
      },
      "credentials": {
        "openAiApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "54fa19aa-ba63-4f7a-a16f-e91e8a2c7c77",
      "name": "Email the SEO report",
      "type": "n8n-nodes-base.gmail",
      "position": [
        2144,
        112
      ],
      "parameters": {
        "sendTo": "={{ $('Configuration').first().json.report_email }}",
        "message": "={{ $json.text }}",
        "options": {
          "senderName": "SEO Rank Tracker",
          "appendAttribution": false
        },
        "subject": "=SEO ranking report - {{ $now.toFormat(\"yyyy-MM-dd\") }}"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "3d922f1d-c766-4d2f-9ef6-30644ed04950",
      "name": "Sticky Note 4613280d",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -800,
        -512
      ],
      "parameters": {
        "color": 4,
        "width": 640,
        "height": 2040,
        "content": "# Track keyword rankings on Google with Apify and send AI SEO reports by email\n# \ud83d\udce5  [Open full documentation on Notion](https://automatisation.notion.site/Course-Track-keyword-rankings-on-Google-with-Apify-and-send-AI-SEO-reports-by-email-39d3d6550fd981078ad0fc8553e63a21)\n\n## How it works\nThis workflow tracks where websites rank on Google for your target keywords, every day, without any manual checking.\n\n1. A schedule trigger runs the workflow daily at 8 AM.\n2. The **Configuration** node holds every variable you may want to change (keywords, target domain, country, language, AI model, report email).\n3. Each keyword is sent to the **Apify Google Search Scraper**, which returns the top organic results for your country and language.\n4. A Code node flattens the results into clean rows: keyword, rank, title, URL, domain, description, and whether the result belongs to your target domain.\n5. Every row is appended to a **Google Sheets** ranking log, building a day-by-day SEO history.\n6. All rankings are combined and sent to **OpenAI**, which writes a short SEO report: where your domain ranks, who the top competitors are, and 3-5 prioritized opportunities.\n7. The report lands in your inbox via **Gmail** as a clean HTML email.\n\n## Setup\n1. Open the **Configuration** node and set your keywords (comma-separated), target domain, country code, language code, results per keyword, OpenAI model and report email. This is the only node you need to edit.\n2. Select your **Apify** credential on the \"Scrape Google results with Apify\" node (header auth with your Apify API token).\n3. Select your **Google Sheets** credential and pick the spreadsheet + tab on the \"Log rankings to Google Sheets\" node. Create a sheet with the columns: checked_at, keyword, rank, title, url, domain, description, is_target_domain.\n4. Select your **OpenAI** credential on the \"OpenAI Chat Model\" node.\n5. Select your **Gmail** credential on the \"Email the SEO report\" node.\n6. Run the workflow once manually to verify, then activate it.\n\n## Requirements\n- An [Apify](https://apify.com) account and API token (the Google Search Scraper actor is used via the REST API)\n- An OpenAI API key\n- A Google account with Sheets and Gmail access\n\n## Customization\n- Change the schedule (hourly, weekly) in the trigger node.\n- Track more keywords: just extend the comma-separated list in Configuration.\n- Increase `results_per_keyword` (up to 100) to monitor deeper SERP positions.\n- Localize your tracking with `country_code` and `language_code` (e.g. fr / fr for France).\n- Edit the system prompt in \"Generate AI SEO report\" to change the tone or structure of the report.\n- Replace Gmail with Slack or Telegram to receive the report elsewhere.\n\nNeed help customizing?\nContact me for consulting and support : [Linkedin](https://www.linkedin.com/in/doctor-firass/)\n\n# MY NEW YOUTUBE CHANNEL\n\ud83d\udc49 [Subscribe to my new YouTube channel](https://www.youtube.com/@DrFiras_AI). Here I'll share videos and Shorts with practical tutorials and FREE templates for n8n.\n\n[![The AI Doctor](https://www.dr-firas.com/the-ai-doctor.png)](https://www.youtube.com/@DrFiras_AI)"
      },
      "typeVersion": 1
    },
    {
      "id": "6ce91983-82d8-462c-bd1c-dc8e6c36cd49",
      "name": "Sticky Note 03b9eeb7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -64,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 460,
        "height": 460,
        "content": "## 1. Schedule & configuration\nThe workflow runs daily. Set ALL your variables in the **Configuration** node: keywords, target domain, country, language, results per keyword, OpenAI model and report email."
      },
      "typeVersion": 1
    },
    {
      "id": "fd8c31f7-fa21-4cbc-9908-6499b16abbc9",
      "name": "Sticky Note 5bbeb576",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        432,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 680,
        "height": 460,
        "content": "## 2. Scrape Google results with Apify\nEach keyword becomes one item and is sent to the Apify Google Search Scraper (REST API, synchronous run). Requests are throttled to 1 per 2 seconds."
      },
      "typeVersion": 1
    },
    {
      "id": "f7971bd0-9a68-4617-9d14-8348c9e24c76",
      "name": "Sticky Note 4333a547",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1120,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 420,
        "height": 460,
        "content": "## 3. Parse & log rankings\nA Code node flattens the SERP results into clean rows (keyword, rank, title, URL, domain, target-domain flag) and appends them to your Google Sheets ranking history."
      },
      "typeVersion": 1
    },
    {
      "id": "acf5a00e-678a-4694-bbe4-f55d91d1ed91",
      "name": "Sticky Note 70a5333e",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1568,
        -80
      ],
      "parameters": {
        "color": 7,
        "width": 760,
        "height": 620,
        "content": "## 4. AI SEO report by email\nAll rankings are combined into one payload. OpenAI writes a short HTML report (your positions, top competitors, prioritized opportunities) and Gmail delivers it to your inbox."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "availableInMCP": true,
    "executionOrder": "v1"
  },
  "versionId": "a1ec7ea2-6f90-4a25-bb47-964c1e9d8e55",
  "nodeGroups": [],
  "connections": {
    "Configuration": {
      "main": [
        [
          {
            "node": "Prepare keyword list",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Generate AI SEO report",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Run daily at 8 AM": {
      "main": [
        [
          {
            "node": "Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare keyword list": {
      "main": [
        [
          {
            "node": "Split keywords into items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate AI SEO report": {
      "main": [
        [
          {
            "node": "Email the SEO report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split keywords into items": {
      "main": [
        [
          {
            "node": "Scrape Google results with Apify",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract and rank SERP results": {
      "main": [
        [
          {
            "node": "Log rankings to Google Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log rankings to Google Sheets": {
      "main": [
        [
          {
            "node": "Combine rankings for AI analysis",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine rankings for AI analysis": {
      "main": [
        [
          {
            "node": "Generate AI SEO report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Scrape Google results with Apify": {
      "main": [
        [
          {
            "node": "Extract and rank SERP results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}