How to Automate Competitor Monitoring with n8n

By The LightCube Labs team

The Problem

Competitors quietly change their pricing tiers, rename plans, and publish changelog entries that affect your positioning, yet most teams only notice weeks later when a prospect mentions it on a call. Manually visiting five or six pages every morning is tedious and nobody keeps it up. Generic page watchers fire on every banner rotation or timestamp, so the alerts get ignored. This guide builds an n8n workflow that fetches each page daily, extracts only the meaningful text, compares a hash against the stored value, and posts a readable diff to Slack only when something real changed.

What You Will Need

Step-by-Step Workflow Setup

1 Schedule the daily check

Competitor pages rarely change more than once a day, so a daily run is enough. The Schedule Trigger node in n8n supports a specific hour, which lets you get results before the team starts work.

Add a Schedule Trigger node to a new n8n workflow and configure:

Node: Schedule Trigger
Trigger Rules: Add Rule
Trigger Interval: Days
Days Between Triggers: 1
Trigger at Hour: 6am
Trigger at Minute: 0

The workflow timezone comes from the n8n workflow settings, so check that your n8n instance matches your office timezone.

Expression: {{ $now.format('yyyy-MM-dd') }}
Sample output:
{
  "timestamp": "2026-11-04T06:00:00.000-08:00",
  "Day of week": "Wednesday"
}

2 Fetch competitor pages

First read the URL list from your Tracker tab with a Google Sheets node (Operation: Get Row(s), Return All: On). Each row becomes one item, and the HTTP Request node then runs once per row.

Add an HTTP Request node to the n8n canvas and configure:

Node: HTTP Request
Method: GET
URL: {{ $json.url }}
Response Format: Text
Put Output in Field: html
Send Headers: On
Header Name: User-Agent
Header Value: Mozilla/5.0 (compatible; LightCubeMonitor/1.0)
Options > Batching > Items per Batch: 1
Options > Batching > Batch Interval: 2000
Settings > On Error: Continue

The 2 second batch interval keeps requests polite, and On Error: Continue means one broken competitor site does not stop the other checks in the n8n run.

Sample output:
{
  "html": "<html><body><h2>Pro</h2><p>$49 per month</p>...</body></html>"
}

3 Extract text and compute a hash

Hashing raw HTML produces false alarms because scripts, nonces, and ad markup change on every load. The n8n Code node strips that noise, keeps only visible text, and hashes the result. Cheerio is available only if your self-hosted n8n sets NODE_FUNCTION_ALLOW_EXTERNAL=cheerio, so this version uses plain regular expressions and the built-in crypto module.

Add a Code node and configure:

Node: Code
Mode: Run Once for Each Item
Language: JavaScript

const crypto = require('crypto');
let html = $json.html || '';
html = html.replace(/<script[\s\S]*?<\/script>/gi, ' ')
           .replace(/<style[\s\S]*?<\/style>/gi, ' ')
           .replace(/<nav[\s\S]*?<\/nav>/gi, ' ')
           .replace(/<footer[\s\S]*?<\/footer>/gi, ' ');
const text = html.replace(/<\/(h[1-6]|p|li|tr)>/gi, '\n')
                 .replace(/<[^>]+>/g, ' ')
                 .replace(/[ \t]+/g, ' ')
                 .split('\n').map(l => l.trim()).filter(Boolean).join('\n');
const hash = crypto.createHash('sha256').update(text).digest('hex');
return { json: { name: $('Read Tracker').item.json.name, url: $('Read Tracker').item.json.url, text, hash } };

Closing tags become newlines, so each heading, paragraph, list item, and table row lands on its own line. That makes the diff in step 6 line based and easy to read.

Sample output:
{
  "name": "Rival Co Pricing",
  "url": "https://rival.example.com/pricing",
  "text": "Pro\n$49 per month\nTeam\n$99 per month",
  "hash": "9b74c9897bac770ffc029102a200c5de..."
}

4 Read the stored hash

Next, look up the last known hash and text for each competitor. Use the Google Sheets node with a filter so n8n fetches just the matching row.

Add a Google Sheets node and configure:

Node: Google Sheets
Resource: Sheet Within Document
Operation: Get Row(s)
Sheet: Tracker
Filters > Column: name
Filters > Value: {{ $json.name }}
Settings > Always Output Data: On

Because the lookup replaces the item in n8n, use a Merge node in Combine by Position mode afterward so the new hash and the stored hash sit on one item. Name the fields newHash and oldHash.

Sample merged item:
{
  "name": "Rival Co Pricing",
  "newHash": "9b74c9897bac...",
  "oldHash": "41ab07e2c9d1...",
  "oldText": "Pro\n$39 per month\nTeam\n$99 per month"
}

5 Branch when the hash changed

An IF node routes only changed pages onward. An empty oldHash means a first run, which should store a baseline without alerting.

Add an IF node and configure:

Node: IF
Conditions: String
Value 1: {{ $json.newHash }}
Operation: Is Not Equal
Value 2: {{ $json.oldHash }}
Add Condition (AND): String, {{ $json.oldHash }}, Is Not Empty

The false output connects directly to the hash update in step 7 only for first runs, if you want baselines stored. Otherwise leave it unconnected.

6 Build the diff and alert Slack

A second n8n Code node compares old and new text line by line and prints added and removed lines. Then a Slack node posts the summary.

Add a Code node and configure:

Node: Code
Mode: Run Once for Each Item
Language: JavaScript

const oldLines = new Set(($json.oldText || '').split('\n'));
const newLines = new Set(($json.text || '').split('\n'));
const added = [...newLines].filter(l => oldLines.has(l) === false);
const removed = [...oldLines].filter(l => newLines.has(l) === false);
const summary = added.map(l => '+ ' + l).concat(removed.map(l => '- ' + l)).join('\n');
return { json: { ...$json, added, removed, summary } };

Then add a Slack node:

Node: Slack
Resource: Message
Operation: Send
Channel: By Name, competitor-watch
Message Text: Change detected on {{ $json.name }}\n{{ $json.url }}\n```{{ $json.summary }}```
Sample diff output:
{
  "name": "Rival Co Pricing",
  "added": ["$49 per month"],
  "removed": ["$39 per month"],
  "summary": "+ $49 per month\n- $39 per month"
}

7 Update the stored hash and history

Finally, have n8n write the new hash and text back, so tomorrow compares against today. Use Google Sheets with Append or Update Row.

Node: Google Sheets
Operation: Append or Update Row
Sheet: Tracker
Column to Match On: name
name: {{ $json.name }}
hash: {{ $json.newHash }}
text: {{ $json.text }}
checkedAt: {{ $now.toISO() }}

Add a second Google Sheets node with Operation: Append Row on the History tab, writing name, changedAt, and summary. That tab becomes a searchable timeline of every pricing move, which is the historical diff log your sales team can cite from the n8n workflow output.

Troubleshooting

The n8n hash changes every single day

Something dynamic is still in the extracted text, such as a copyright year, a visitor counter, or a rotating testimonial. Print the summary for two consecutive runs, find the changing line, and strip it with another replace call in the n8n Code node.

The HTTP Request returns 403 or a bot challenge page

Some sites block unknown clients. Try a realistic User-Agent header first. If the page still returns a challenge, the content likely needs a browser renderer, which is outside this basic n8n approach, so consider a changelog RSS feed instead.

Google Sheets rejects the text stored by n8n

A single cell holds up to 50000 characters. For long pages, store only the first 40000 characters in the n8n Code node, or store the text in a separate file and keep just the hash in the sheet.

Free n8n Workflow Resources

Get started with the free workflow pack on the LightCube Labs templates page.

Production-Ready Workflow Packs

Get this workflow and more, with setup guides, editable configurations, and a shared failure-alert system.

View the Starter ($29) and Pro ($99) Packs

Frequently Asked Questions

Why does n8n hash the text instead of comparing HTML directly?

In n8n, a hash is a short fixed value that fits easily in a cell and compares instantly. Hashing extracted text, rather than HTML, ignores markup churn so only wording and price changes count.

Can n8n watch a page that needs JavaScript to render?

The HTTP Request node fetches only the server response. For client rendered pages, look for an underlying JSON endpoint in the browser network tab and fetch that, or use a hosted rendering service called through the same node.

How do I avoid alerts for tiny edits like typo fixes?

Add a condition after the diff Code node that requires at least one added or removed line to contain a digit or a keyword such as plan, price, or limit. This keeps the n8n alert channel focused on commercial changes.

Is it acceptable to monitor competitor pages with n8n this way?

Fetching a public page once a day with n8n is ordinary browsing volume. Respect robots.txt and each site terms, keep the batch interval, and never attempt to access pages behind a login.