{
  "id": "1xrqMJ6EWwURrcKa",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "SEO Keyword Monitoring",
  "tags": [],
  "nodes": [
    {
      "id": "dd7dfdd7-afc0-4e42-abbb-00a79bad0cd4",
      "name": "Fetch Keywords from Airtable",
      "type": "n8n-nodes-base.airtableTrigger",
      "position": [
        0,
        0
      ],
      "parameters": {
        "baseId": {
          "__rl": true,
          "mode": "url",
          "value": "https://airtable.com/appjaqV0O7FkXT2qj/shrst7GnlbzMDz4te"
        },
        "tableId": {
          "__rl": true,
          "mode": "url",
          "value": "https://airtable.com/appjaqV0O7FkXT2qj/tblTAvRqVFOo5AVDF/viwEp0ssaidZOo4nl?blocks=hide"
        },
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "triggerField": "Keyword",
        "authentication": "airtableTokenApi",
        "additionalFields": {}
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "0cc36294-63c3-4ffb-9282-8a9d98a3606d",
      "name": "Check Rank via Firecrawl",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        300,
        -220
      ],
      "parameters": {
        "url": "https://api.firecrawl.dev/serp",
        "method": "POST",
        "options": {},
        "sendBody": true,
        "sendHeaders": true,
        "bodyParameters": {
          "parameters": [
            {
              "name": "query",
              "value": "={{ $json.fields.Keyword }}"
            },
            {
              "name": "country",
              "value": "us"
            },
            {
              "name": "language",
              "value": "en"
            }
          ]
        },
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "Bearer YOUR_TOKEN_HERE"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "3b38da51-67e5-45b5-a481-6b064c143eca",
      "name": "Combine Airtable + Firecrawl Result",
      "type": "n8n-nodes-base.merge",
      "position": [
        580,
        -20
      ],
      "parameters": {},
      "typeVersion": 3.1
    },
    {
      "id": "479ab102-bd2c-4278-b869-4a72fbdaa639",
      "name": "Compare Ranks",
      "type": "n8n-nodes-base.code",
      "position": [
        900,
        -20
      ],
      "parameters": {
        "jsCode": "// Find the item that contains results\nconst resultItem = items.find(item => Array.isArray(item.json.results));\nconst dataItem = items.find(item => item.json.fields && item.json.fields[\"Target URL\"]);\n\n// Defensive check\nif (!resultItem || !dataItem) {\n  throw new Error(\"Missing result or Airtable data\");\n}\n\nconst results = resultItem.json.results;\nconst fields = dataItem.json.fields;\n\nconst targetUrl = fields[\"Target URL\"];\nconst keyword = fields[\"Keyword\"];\n\nlet rank = null;\n\nfor (let i = 0; i < results.length; i++) {\n  const resultUrl = results[i].url?.toLowerCase() || '';\n  if (resultUrl.includes(targetUrl.toLowerCase())) {\n    rank = i + 1;\n    break;\n  }\n}\n\nreturn [\n  {\n    json: {\n      keyword,\n      target_url: targetUrl,\n      current_rank: fields[\"Current Rank\"],\n      new_rank: rank !== null ? rank : \"Not in Top 10\",\n      rank_changed: rank !== null && rank !== fields[\"Current Rank\"],\n      notes: fields[\"Notes\"] || \"\",\n      airtable_id: dataItem.json.id,\n      raw_results: results // Optional\n    }\n  }\n];\n"
      },
      "typeVersion": 2
    },
    {
      "id": "bf89e7d8-bc10-4e12-9671-3b21976f901d",
      "name": "Update Airtable Record",
      "type": "n8n-nodes-base.airtable",
      "position": [
        1120,
        -20
      ],
      "parameters": {
        "base": {
          "__rl": true,
          "mode": "list",
          "value": "appjaqV0O7FkXT2qj",
          "cachedResultUrl": "https://airtable.com/appjaqV0O7FkXT2qj",
          "cachedResultName": "Table no.1"
        },
        "table": {
          "__rl": true,
          "mode": "list",
          "value": "tblTAvRqVFOo5AVDF",
          "cachedResultUrl": "https://airtable.com/appjaqV0O7FkXT2qj/tblTAvRqVFOo5AVDF",
          "cachedResultName": "Table 1"
        },
        "columns": {
          "value": {
            "id": "={{ $json.airtable_id }}",
            "Current Rank": "={{ $json.new_rank }}"
          },
          "schema": [
            {
              "id": "id",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": true,
              "required": false,
              "displayName": "id",
              "defaultMatch": true
            },
            {
              "id": "Keyword",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Keyword",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Target URL",
              "type": "string",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Target URL",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Current Rank",
              "type": "number",
              "display": true,
              "removed": false,
              "readOnly": false,
              "required": false,
              "displayName": "Current Rank",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "id"
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "update"
      },
      "credentials": {
        "airtableTokenApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.1
    },
    {
      "id": "6cda42ab-19eb-4309-85f3-0d84a195931a",
      "name": "Check if Rank Changed",
      "type": "n8n-nodes-base.if",
      "position": [
        1460,
        -20
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "e16a90ab-e577-46f8-abf2-9c8de77059fc",
              "operator": {
                "type": "number",
                "operation": "notEquals"
              },
              "leftValue": "={{ $('Compare Ranks').item.json.current_rank }}",
              "rightValue": "={{ $('Compare Ranks').item.json.new_rank }}"
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "02ed48b0-4e24-42ea-8856-8a25ca0446cb",
      "name": "Send Slack Notification",
      "type": "n8n-nodes-base.slack",
      "position": [
        1740,
        -200
      ],
      "parameters": {
        "text": "hi",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "list",
          "value": "C08TTV0CC3E",
          "cachedResultName": "all-nathing"
        },
        "otherOptions": {}
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "dff0f256-73c3-4cdc-8b1c-db30070a3965",
      "name": "No Operation, do nothing",
      "type": "n8n-nodes-base.noOp",
      "position": [
        1740,
        180
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "a9171863-7fd9-4cd4-ac67-b6b4fb5b7dc7",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -40,
        -1320
      ],
      "parameters": {
        "width": 760,
        "height": 1520,
        "content": "## \ud83d\udd0d **Section 1: Fetch and Merge Data**\n\n### \ud83c\udfaf Goal: Get keyword data from Airtable, fetch rank info from Firecrawl, and merge both for comparison.\n\n---\n\n### \ud83e\uddf1 **Step 1: Fetch Keywords from Airtable**\n\n**Node Name:** `Fetch Keywords from Airtable`\n**\ud83d\udccc Description:**\nTriggers when a new or updated record is found in Airtable. It fetches the following fields:\n\n* `Keyword`\n* `Target URL`\n* `Current Rank`\n\nThis is the starting point of the workflow.\n\n---\n\n### \ud83c\udf10 **Step 2: Check Rank via Firecrawl**\n\n**Node Name:** `Check Rank via Firecrawl`\n**\ud83d\udccc Description:**\nSends a POST request to [Firecrawl API](https://api.firecrawl.dev) with the `Keyword` as the main parameter. It returns updated ranking information for that keyword.\n\n---\n\n### \ud83d\udd17 **Step 3: Combine Airtable + Firecrawl Result**\n\n**Node Name:** `Combine Airtable + Firecrawl Result`\n**\ud83d\udccc Description:**\nA `Merge` node that combines:\n\n* The original data from Airtable\n* The response from Firecrawl\n\nThis prepares a unified object to be used in the next comparison step.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "7c568ca6-16dd-4d16-bea1-7cbdbf5b973b",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        820,
        -980
      ],
      "parameters": {
        "color": 3,
        "width": 460,
        "height": 1180,
        "content": "## \ud83e\udde0 **Section 2: Compare & Update Airtable Record**\n\n### \ud83c\udfaf Goal: Check if the rank has changed. If yes, update Airtable with the new rank.\n\n---\n\n### \ud83e\uddee **Step 4: Compare Ranks**\n\n**Node Name:** `Compare Ranks`\n**\ud83d\udccc Description:**\nA `Code` node that compares the `Current Rank` from Airtable and the `New Rank` from Firecrawl.\nIt adds a new field:\n\n```js\nrankChanged: true // or false\n```\n\nThis acts as the decision flag for the next step.\n\n---\n\n### \ud83d\udcdd **Step 5: Update Airtable Record**\n\n**Node Name:** `Update Airtable Record`\n**\ud83d\udccc Description:**\nUses the unique record ID to update the Airtable row with the new rank.\nOptional fields to update:\n\n* `New Rank`\n* `Rank Changed Date`\n* Any notes or audit logs\n\n---\n\n"
      },
      "typeVersion": 1
    },
    {
      "id": "60ea4f45-7a33-4cf1-8952-7989df4c6edf",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1400,
        -900
      ],
      "parameters": {
        "color": 7,
        "width": 500,
        "height": 1220,
        "content": "## \u2705 **Section 3: Conditional Alert**\n\n### \ud83c\udfaf Goal: Notify via Slack if the rank has changed.\n\n---\n\n### \ud83d\udd00 **Step 6: Check if Rank Changed**\n\n**Node Name:** `Check if Rank Changed`\n**\ud83d\udccc Description:**\nAn `If` node that checks the `rankChanged` flag from the previous step.\n\n* **true:** Proceed to send Slack notification\n* **false:** Do nothing and stop workflow here\n\n---\n\n### \ud83d\udcac **Step 7: Send Slack Notification**\n\n**Node Name:** `Send Slack Notification`\n**\ud83d\udccc Description:**\nPosts a message in a specified Slack channel with:\n\n* \ud83d\udcc8 `Keyword`\n* \ud83d\udd17 `Target URL`\n* \ud83c\udd95 `New Rank`\n* \ud83d\udd53 Timestamp or context\n\nThis ensures the team is alerted about SEO improvements or drops.\n"
      },
      "typeVersion": 1
    },
    {
      "id": "bd9813ca-f877-4a50-8b4a-8284dc5cd2da",
      "name": "Sticky Note9",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1720,
        -1320
      ],
      "parameters": {
        "color": 4,
        "width": 1300,
        "height": 320,
        "content": "=======================================\n            WORKFLOW ASSISTANCE\n=======================================\nFor any questions or support, please contact:\n    Yaron@nofluff.online\n\nExplore more tips and tutorials here:\n   - YouTube: https://www.youtube.com/@YaronBeen/videos\n   - LinkedIn: https://www.linkedin.com/in/yaronbeen/\n=======================================\n"
      },
      "typeVersion": 1
    },
    {
      "id": "8c6e6722-1d72-4736-8c69-cf245d6cc06d",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1720,
        -980
      ],
      "parameters": {
        "color": 4,
        "width": 1289,
        "height": 2178,
        "content": "\n# \ud83d\ude80 **Automated Rank Tracker Workflow (n8n)**\n\n---\n\n## \ud83d\udccc **Purpose**\n\nThis workflow is designed to **automatically monitor keyword rankings**, compare them with new data from an SEO API (Firecrawl), and **alert your team on Slack** whenever there\u2019s a change. It helps you stay on top of your SEO game **without manual checking**! \u26a1\n\n---\n\n## \ud83e\udde0 **What It Does \u2014 In 3 Simple Steps**\n\n---\n\n### \ud83d\udd0d **1. Fetch & Merge Data**\n\n\ud83d\udce6 **Source:** Airtable\n\ud83c\udf10 **Analyzer:** Firecrawl API\n\ud83d\udd17 **Process:** Merge both for side-by-side comparison\n\n* \u2705 Grabs `Keyword`, `Target URL`, and `Current Rank` from Airtable\n* \ud83c\udf10 Sends the `Keyword` to Firecrawl API to get the **Latest Rank**\n* \ud83d\udd17 Merges original Airtable data + Firecrawl result\n* \ud83d\udee0\ufe0f Prepares a complete object for decision-making\n\n---\n\n### \ud83e\uddee **2. Compare & Update Airtable**\n\n\ud83e\udde0 **Compare:** Current Rank vs Latest Rank\n\ud83d\udcdd **Update:** Airtable if there's a difference\n\n* \ud83d\udd0d Uses a Code node to check:\n\n  ```js\n  if (currentRank !== latestRank) \u2192 rankChanged = true\n  ```\n* \ud83d\udcdd If there's a change, updates the original Airtable row with:\n\n  * New Rank\n  * Timestamp\n  * Optional status/comments\n\n---\n\n### \u2705 **3. Alert If Rank Changed**\n\n\ud83e\uddea **Decision:** Did rank change?\n\ud83d\udce3 **Notify:** Slack if yes\n\n* \ud83e\udd16 Uses an `If` node to evaluate the `rankChanged` flag\n* \ud83d\udcac Posts a message in Slack like:\n\n  > \u201c\ud83c\udfaf Keyword: *\u2018best running shoes\u2019* rank has changed from *5 \u27a1 2*! \ud83d\ude80\u201d\n\n---\n\n## \ud83e\udde9 **Visual Flow Overview**\n\n```\nAirtable Trigger\n   \u2193\nFirecrawl HTTP Request\n   \u2193\nMerge Results\n   \u2193\nCompare Ranks (Code Node)\n   \u2193\nUpdate Airtable\n   \u2193\nCheck If Rank Changed (If Node)\n   \u21b3 true \u2192 Send Slack Notification\n```\n\n---\n\n\n## \ud83d\udca1 Final Thoughts\n\nThis elegant workflow combines **data fetching, comparison, storage, and notification** in a streamlined way. It\u2019s ideal for:\n\n* SEO Teams\n* Content Managers\n* Digital Agencies\n* Growth Hackers\n\n\u2728 **Set it and forget it \u2014 your ranks are now being watched 24/7.**\n\n---\n\nLet me know if you'd like a **shareable Markdown version** or a **Notion-style document**.\n"
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "versionId": "aded958b-db96-4a80-b05b-27afdb05f950",
  "connections": {
    "Compare Ranks": {
      "main": [
        [
          {
            "node": "Update Airtable Record",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check if Rank Changed": {
      "main": [
        [
          {
            "node": "Send Slack Notification",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Operation, do nothing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Update Airtable Record": {
      "main": [
        [
          {
            "node": "Check if Rank Changed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check Rank via Firecrawl": {
      "main": [
        [
          {
            "node": "Combine Airtable + Firecrawl Result",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Keywords from Airtable": {
      "main": [
        [
          {
            "node": "Check Rank via Firecrawl",
            "type": "main",
            "index": 0
          },
          {
            "node": "Combine Airtable + Firecrawl Result",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Combine Airtable + Firecrawl Result": {
      "main": [
        [
          {
            "node": "Compare Ranks",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}