This workflow corresponds to n8n.io template #17024 — we link there as the canonical source.
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 →
{
"id": "oxxn77jFKXYea7h1",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Competitor Monitor for n8n (Free)",
"tags": [],
"nodes": [
{
"id": "cd698c35-4549-490a-9df9-085558321261",
"name": "Sticky Main",
"type": "n8n-nodes-base.stickyNote",
"position": [
2512,
1376
],
"parameters": {
"color": 3,
"width": 480,
"height": 592,
"content": "## Competitor Monitor (Free)\nWatches competitor web pages and alerts you on **Discord/Slack** when they change - raw diff, no AI.\n\n**How it works**\nFetch each page -> detect text changes -> post the diff to your webhook.\n\n**How to set up**\nOpen the **Targets** node (yellow, section 1) and paste your webhook URL + the pages to watch. That is the only node to edit.\n\n**Customization**\nChange the schedule interval, or add more target pages.\n\n_Want AI-written summaries + CSS-selector targeting? See the Pro version:_\nhttps://rubenfabioux.vercel.app"
},
"typeVersion": 1
},
{
"id": "e83d7a2a-dc15-49fa-ac56-f4436ee51650",
"name": "Sticky Section 1",
"type": "n8n-nodes-base.stickyNote",
"position": [
3040,
1456
],
"parameters": {
"color": 5,
"width": 500,
"height": 400,
"content": "## 1. Schedule & targets\nRuns automatically **every 6 hours** (editable), then loads your list of competitor pages and your webhook URL from the **Targets** node. Each target becomes one item in the flow."
},
"typeVersion": 1
},
{
"id": "2785a936-37bc-4b8f-9321-f2f25d096088",
"name": "Sticky Section 2",
"type": "n8n-nodes-base.stickyNote",
"position": [
3536,
1456
],
"parameters": {
"color": 5,
"width": 532,
"height": 400,
"content": "## 2. Check each page\nDownloads each page's HTML, strips it to plain text, **hashes** it, and compares it to the last run. First run stores a baseline; after that it builds a raw **line-by-line diff** of new/removed lines."
},
"typeVersion": 1
},
{
"id": "a87a10ff-c216-4d23-af0a-617b4836b4a2",
"name": "Sticky Section 3",
"type": "n8n-nodes-base.stickyNote",
"position": [
4064,
1456
],
"parameters": {
"color": 5,
"width": 500,
"height": 400,
"content": "## 3. Send alert\n**Alert Needed?** checks whether a change was found. If yes, the raw diff is posted to your **Discord/Slack webhook**. No change means nothing is sent."
},
"typeVersion": 1
},
{
"id": "d8477f19-b371-44c4-8f42-d9354928adc9",
"name": "Every 6 hours",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
3104,
1664
],
"parameters": {
"rule": {
"interval": [
{
"field": "hours",
"hoursInterval": 6
}
]
}
},
"typeVersion": 1.2
},
{
"id": "4ebe5573-8beb-45b5-81f0-8c8887f47479",
"name": "Targets",
"type": "n8n-nodes-base.code",
"position": [
3328,
1664
],
"parameters": {
"jsCode": "// ============================================================\n// Free version - raw diff alerts only. For AI-written summaries and\n// CSS selector targeting, see the Pro version:\n// https://rubenfabioux.vercel.app\n// ============================================================\n// CONFIGURATION - this is the ONLY place to edit\n// ============================================================\n\n// 1) Paste your Discord or Slack webhook URL here\nconst WEBHOOK_URL = \"PASTE_YOUR_WEBHOOK_URL_HERE\";\n\n// 2) List of competitor pages to watch\nconst TARGETS = [\n { name: \"Competitor A - Pricing\", url: \"https://example.com/pricing\" },\n { name: \"Competitor B - Blog\", url: \"https://example.org/blog\" }\n];\n\n// ============================================================\nreturn TARGETS.map(t => ({ json: { ...t, webhookUrl: WEBHOOK_URL } }));"
},
"typeVersion": 2
},
{
"id": "3e406933-5a67-439f-99db-9547ebd48b06",
"name": "Fetch Page",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
3616,
1664
],
"parameters": {
"url": "={{ $json.url }}",
"options": {
"timeout": 20000,
"response": {
"response": {
"responseFormat": "text"
}
}
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/126.0 Safari/537.36"
},
{
"name": "Accept-Language",
"value": "en-US,en;q=0.9"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "e1f357c2-a55d-476d-94af-4c74c66cbb8b",
"name": "Detect Changes",
"type": "n8n-nodes-base.code",
"position": [
3824,
1664
],
"parameters": {
"mode": "runOnceForEachItem",
"jsCode": "// Get the matching target config\nconst target = $('Targets').item.json;\nconst sd = $getWorkflowStaticData('global');\nsd.pages = sd.pages || {};\n\n// --- Network error / site blocking the request ---\nif ($json.error) {\n const msg = `\u26a0\ufe0f **${target.name}** - could not fetch the page.\\n${target.url}\\nError: ${$json.error.message || 'unknown'}`;\n return { json: { ...target, notify: true, message: msg } };\n}\n\n// --- HTML cleanup -> plain text ---\nlet html = String($json.data || '');\nhtml = html\n .replace(/<script[\\s\\S]*?<\\/script>/gi, ' ')\n .replace(/<style[\\s\\S]*?<\\/style>/gi, ' ')\n .replace(/<noscript[\\s\\S]*?<\\/noscript>/gi, ' ')\n .replace(/<[^>]+>/g, '\\n');\nlet text = html\n .replace(/ /gi, ' ')\n .replace(/&/gi, '&')\n .replace(/&#\\d+;/g, ' ')\n .replace(/[ \\t]+/g, ' ');\nconst lines = text.split('\\n').map(l => l.trim()).filter(l => l.length > 2);\ntext = lines.join('\\n');\n\n// --- FNV-1a hash (pure JS, no dependency) ---\nlet h = 2166136261;\nfor (let i = 0; i < text.length; i++) {\n h ^= text.charCodeAt(i);\n h = Math.imul(h, 16777619);\n}\nconst hash = (h >>> 0).toString(16);\n\nconst prev = sd.pages[target.name];\nsd.pages[target.name] = { hash, text, lastChecked: new Date().toISOString() };\n\n// --- First run: store baseline, no alert ---\nif (!prev) {\n return { json: { ...target, notify: false, firstRun: true } };\n}\n\n// --- No change ---\nif (prev.hash === hash) {\n return { json: { ...target, notify: false } };\n}\n\n// --- Change detected: line-by-line diff ---\nconst oldLines = new Set(prev.text.split('\\n'));\nconst newLines = new Set(text.split('\\n'));\nconst added = [...newLines].filter(l => !oldLines.has(l)).slice(0, 15);\nconst removed = [...oldLines].filter(l => !newLines.has(l)).slice(0, 15);\n\n// Clean truncation: cut at the last word/line boundary before max, add ... if truncated\nfunction clip(s, max) {\n s = String(s == null ? '' : s);\n if (s.length <= max) return s;\n var cut = s.slice(0, max - 1);\n var nl = cut.lastIndexOf(String.fromCharCode(10));\n var sp = cut.lastIndexOf(' ');\n var b = Math.max(nl, sp);\n if (b > max * 0.6) cut = cut.slice(0, b);\n return cut.trimEnd() + String.fromCharCode(8230);\n}\n\nlet msg = `\ud83d\udd14 **${target.name}** has changed!\\n${target.url}\\n`;\nif (added.length) msg += `\\n\u2795 **New:**\\n${added.map(l => '\u2022 ' + l.slice(0, 120)).join('\\n')}\\n`;\nif (removed.length) msg += `\\n\u2796 **Removed:**\\n${removed.map(l => '\u2022 ' + l.slice(0, 120)).join('\\n')}`;\n\nreturn { json: { ...target, notify: true, message: clip(msg, 1900) } };"
},
"typeVersion": 2
},
{
"id": "57a33f2c-f2b2-4f54-b11d-29a9a53e3e40",
"name": "Alert Needed?",
"type": "n8n-nodes-base.if",
"position": [
4112,
1664
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 1,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "loose"
},
"combinator": "and",
"conditions": [
{
"id": "c1",
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
},
"leftValue": "={{ $json.notify }}",
"rightValue": ""
}
]
}
},
"typeVersion": 2
},
{
"id": "00f855f4-2a48-4938-84a0-8933c76cd9d4",
"name": "Send Alert",
"type": "n8n-nodes-base.httpRequest",
"position": [
4336,
1664
],
"parameters": {
"url": "={{ $json.webhookUrl }}",
"method": "POST",
"options": {},
"jsonBody": "={{ JSON.stringify({ content: String($json.message).slice(0, 2000), text: String($json.message).slice(0, 2000) }) }}",
"sendBody": true,
"specifyBody": "json"
},
"typeVersion": 4.2
}
],
"active": false,
"settings": {
"binaryMode": "separate",
"availableInMCP": false,
"executionOrder": "v1"
},
"versionId": "b6a83884-356c-465f-8c71-0f54d8f96781",
"nodeGroups": [],
"connections": {
"Targets": {
"main": [
[
{
"node": "Fetch Page",
"type": "main",
"index": 0
}
]
]
},
"Fetch Page": {
"main": [
[
{
"node": "Detect Changes",
"type": "main",
"index": 0
}
]
]
},
"Alert Needed?": {
"main": [
[
{
"node": "Send Alert",
"type": "main",
"index": 0
}
]
]
},
"Every 6 hours": {
"main": [
[
{
"node": "Targets",
"type": "main",
"index": 0
}
]
]
},
"Detect Changes": {
"main": [
[
{
"node": "Alert Needed?",
"type": "main",
"index": 0
}
]
]
}
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow checks a list of competitor web pages every six hours, detects content changes by hashing cleaned page text, and posts a diff-style alert to a Discord or Slack channel via an incoming webhook. Runs every 6 hours on a schedule. Loads a configured list of target…
Source: https://n8n.io/workflows/17024/ — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Auto Hunt. Uses httpRequest, googleSheets, rssFeedRead, telegram. Scheduled trigger; 78 nodes.
Auto-Hunt. Uses httpRequest, googleSheets, rssFeedRead, telegram. Scheduled trigger; 78 nodes.
debug. Uses httpRequest, slack, redis, mailgun. Scheduled trigger; 60 nodes.
Seller Follow-Up Engine (Enhanced). Uses httpRequest, slack. Scheduled trigger; 44 nodes.
This workflow runs daily at 9 AM, uses Perplexity to compile vaping industry news, optionally captures article screenshots via Browserless, and generates a HeyGen avatar video from a template. It then