{
  "name": "AI Competitor Page Monitor",
  "nodes": [
    {
      "id": "bf6a9167-d1d1-4d1d-9d1d-000000000001",
      "name": "Daily Check 9AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.3,
      "position": [
        0,
        0
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "triggerAtHour": 9
            }
          ]
        }
      }
    },
    {
      "id": "bf6a9167-d2d2-4d2d-9d2d-000000000002",
      "name": "Competitor URLs",
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        232,
        0
      ],
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "name": "urls",
              "value": "[\"https://openai.com/pricing\", \"https://www.anthropic.com/pricing\"]",
              "type": "string"
            }
          ]
        },
        "options": {}
      }
    },
    {
      "id": "bf6a9167-d3d3-4d3d-9d3d-000000000003",
      "name": "Fan Out URLs",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        472,
        0
      ],
      "parameters": {
        "jsCode": "const urls = JSON.parse($input.first().json.urls);\nreturn urls.map(url => ({ json: { url } }));"
      }
    },
    {
      "id": "bf6a9167-d4d4-4d4d-9d4d-000000000004",
      "name": "Fetch Page",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        712,
        0
      ],
      "parameters": {
        "url": "={{ $json.url }}",
        "options": {
          "response": {
            "response": {
              "responseFormat": "text",
              "neverError": true
            }
          },
          "timeout": 20000,
          "redirect": {
            "redirect": {
              "followRedirects": true
            }
          }
        },
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "User-Agent",
              "value": "Mozilla/5.0 (compatible; n8n-competitor-monitor/1.0)"
            }
          ]
        }
      }
    },
    {
      "id": "bf6a9167-d5d5-4d5d-9d5d-000000000005",
      "name": "Diff vs Last Snapshot",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        952,
        0
      ],
      "parameters": {
        "jsCode": "const crypto = require('crypto');\nconst html = $input.first().json.data || $input.first().json.body || '';\nconst url = $input.first().json.url || $input.first().json.request?.url || '';\n\n// Extract just the 'meaningful' text \u2014 strips nav/footer noise that changes every render\nconst stripped = html\n  .replace(/<script[^>]*>[\\s\\S]*?<\\/script>/gi, ' ')\n  .replace(/<style[^>]*>[\\s\\S]*?<\\/style>/gi, ' ')\n  .replace(/<nav[^>]*>[\\s\\S]*?<\\/nav>/gi, ' ')\n  .replace(/<footer[^>]*>[\\s\\S]*?<\\/footer>/gi, ' ')\n  .replace(/<header[^>]*>[\\s\\S]*?<\\/header>/gi, ' ')\n  .replace(/<[^>]+>/g, ' ')\n  .replace(/\\s+/g, ' ')\n  .trim();\n\nconst currentHash = crypto.createHash('sha1').update(stripped).digest('hex');\n\n// n8n persists staticData across workflow runs (production)\nconst wfStatic = $getWorkflowStaticData('global');\nwfStatic.snapshots = wfStatic.snapshots || {};\nconst prev = wfStatic.snapshots[url] || null;\nconst prevHash = prev?.hash;\nconst prevText = prev?.text || '';\n\nconst changed = !prevHash || prevHash !== currentHash;\n\n// Compute a rough diff: new lines vs old lines\nconst toLines = t => t.split(/(?<=[.!?])\\s+/).filter(l => l.length > 30);\nconst newLines = new Set(toLines(stripped));\nconst oldLines = new Set(toLines(prevText));\nconst added = [...newLines].filter(l => !oldLines.has(l)).slice(0, 12);\nconst removed = [...oldLines].filter(l => !newLines.has(l)).slice(0, 12);\n\n// Persist new snapshot for next run (cap stored text to 20k chars)\nwfStatic.snapshots[url] = { hash: currentHash, text: stripped.slice(0, 20000), capturedAt: new Date().toISOString() };\n\nreturn [{ json: {\n  url,\n  changed,\n  isFirstRun: !prevHash,\n  hashPrev: prevHash || null,\n  hashCurr: currentHash,\n  addedLines: added,\n  removedLines: removed,\n  fullTextNew: stripped.slice(0, 6000)\n}}];"
      }
    },
    {
      "id": "bf6a9167-d6d6-4d6d-9d6d-000000000006",
      "name": "If Changed",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1192,
        0
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "1",
              "leftValue": "={{ $json.changed && !$json.isFirstRun }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "bf6a9167-d7d7-4d7d-9d7d-000000000007",
      "name": "Summarize the Change",
      "type": "@n8n/n8n-nodes-langchain.chainLlm",
      "typeVersion": 1.5,
      "position": [
        1432,
        -120
      ],
      "parameters": {
        "promptType": "define",
        "text": "=You are a competitive-intelligence analyst. A competitor's page changed overnight. Summarize what is materially different, based strictly on the added/removed sentences below. Do not speculate beyond what is shown.\n\nPAGE: {{ $json.url }}\n\nADDED SENTENCES:\n{{ ($json.addedLines || []).map((l,i) => '+ ' + l).join('\\n') }}\n\nREMOVED SENTENCES:\n{{ ($json.removedLines || []).map((l,i) => '- ' + l).join('\\n') }}\n\nRULES\n- Output a punchy 3-5 bullet Slack message.\n- Lead with the most commercially-interesting change (pricing, feature, tier, customer, policy).\n- Call out specific numbers, tier names, or feature names when they appear in the diff.\n- If the change is cosmetic/copy-only, say that in one line and stop. Don't dress up filler.\n- No preamble. No \"As an AI...\". No hashtags.",
        "messages": {
          "messageValues": []
        },
        "batching": {}
      }
    },
    {
      "id": "bf6a9167-d8d8-4d8d-9d8d-000000000008",
      "name": "OpenAI Chat Model",
      "type": "@n8n/n8n-nodes-langchain.lmChatOpenAi",
      "typeVersion": 1.3,
      "position": [
        1368,
        104
      ],
      "parameters": {
        "model": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-5-mini"
        },
        "options": {}
      }
    },
    {
      "id": "bf6a9167-d9d9-4d9d-9d9d-000000000009",
      "name": "Format Alert",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1672,
        -120
      ],
      "parameters": {
        "jsCode": "const url = $('Diff vs Last Snapshot').item.json.url;\nconst summary = ($input.first().json.text || $input.first().json.output || '').trim();\nconst addedCount = ($('Diff vs Last Snapshot').item.json.addedLines || []).length;\nconst removedCount = ($('Diff vs Last Snapshot').item.json.removedLines || []).length;\n\nconst blocks = [\n  { type: 'header', text: { type: 'plain_text', text: '\ud83d\udea8 Competitor page changed' } },\n  { type: 'section', fields: [\n    { type: 'mrkdwn', text: `*Page:*\\n<${url}|${url}>` },\n    { type: 'mrkdwn', text: `*Diff:*\\n+${addedCount} new, -${removedCount} removed` }\n  ]},\n  { type: 'divider' },\n  { type: 'section', text: { type: 'mrkdwn', text: summary || '_no summary_' } },\n  { type: 'context', elements: [{ type: 'mrkdwn', text: `_Detected ${new Date().toISOString()} \u00b7 n8n Competitor Monitor_` }] }\n];\nreturn [{ json: { text: '\ud83d\udea8 Competitor page changed \u2014 see details', blocks } }];"
      }
    },
    {
      "id": "bf6a9167-dada-4dad-9dad-00000000000a",
      "name": "Post to Slack",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.4,
      "position": [
        1912,
        -120
      ],
      "parameters": {
        "method": "POST",
        "url": "https://hooks.slack.com/services/REPLACE/WITH/YOUR_WEBHOOK",
        "sendBody": true,
        "bodyContentType": "json",
        "jsonBody": "={{ JSON.stringify({ text: $json.text, blocks: $json.blocks }) }}",
        "options": {}
      }
    },
    {
      "id": "bf6a9167-dbdb-4dbd-9dbd-00000000000b",
      "name": "No Change \u2014 Log",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1432,
        120
      ],
      "parameters": {}
    }
  ],
  "connections": {
    "Daily Check 9AM": {
      "main": [
        [
          {
            "node": "Competitor URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Competitor URLs": {
      "main": [
        [
          {
            "node": "Fan Out URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fan Out URLs": {
      "main": [
        [
          {
            "node": "Fetch Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Page": {
      "main": [
        [
          {
            "node": "Diff vs Last Snapshot",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Diff vs Last Snapshot": {
      "main": [
        [
          {
            "node": "If Changed",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Changed": {
      "main": [
        [
          {
            "node": "Summarize the Change",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Change \u2014 Log",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Chat Model": {
      "ai_languageModel": [
        [
          {
            "node": "Summarize the Change",
            "type": "ai_languageModel",
            "index": 0
          }
        ]
      ]
    },
    "Summarize the Change": {
      "main": [
        [
          {
            "node": "Format Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Alert": {
      "main": [
        [
          {
            "node": "Post to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1"
  },
  "staticData": null
}