This workflow checks a list of web pages every morning, works out which ones changed since yesterday, and posts a full-page screenshot of each changed page to a Slack channel. It uses only built-in n8n nodes, so it runs on n8n Cloud and on self-hosted instances, and it needs no browser on your n8n server.
The shape is six nodes:
Schedule Trigger → Code (the URL list) → HTTP Request (page text) → Code (compare with yesterday) → HTTP Request (screenshot, only for changed pages) → Slack (post it)
Page rendering happens in SnapRender: real Chromium, cookie banners and ads removed, a fresh render on every call. A free key covers 200 calls a month, enough for six pages checked daily.
1. Credentials
Create one credential and reuse it in both HTTP Request nodes. In n8n, add a Header Auth credential:
- Name:
X-API-Key
- Value: your SnapRender API key
Header Auth keeps the key out of node parameters and out of exported workflow JSON.
2. Schedule Trigger
Add a Schedule Trigger node set to run daily at 07:00 in your timezone, or use the cron expression 0 7 * * 1-5 for weekdays only.
3. Code node: the list of pages
The URL list lives in a Code node so you can edit it in one place. Each item carries the URL and the CSS selector that holds the content worth watching; scoping to main or a pricing table keeps navigation and footer changes from triggering alerts.
const pages = [
{ url: 'https://example.com/pricing', selector: 'main' },
{ url: 'https://example.com/legal/terms', selector: 'article' },
{ url: 'https://competitor.example/pricing', selector: 'main' },
];
return pages.map((p) => ({ json: p }));
If the list belongs to someone who does not edit workflows, replace this node with a Google Sheets node that reads a sheet with url and selector columns.
4. HTTP Request: get the rendered text
Add an HTTP Request node:
- Method: GET
- URL:
https://app.snap-render.com/v1/extract
- Authentication: Generic Credential Type, Header Auth, the credential from step 1
- Send Query Parameters: on
url = {{ $json.url }}
type = text
selector = {{ $json.selector }}
The node runs once per item and returns JSON with the page's visible text in content. Under the node's Settings, turn on Continue On Fail so one unreachable page does not stop the rest.
5. Code node: what changed since yesterday?
This node keeps one fingerprint per URL in workflow static data, which n8n persists between production runs. Set the node's mode to Run Once for All Items.
const crypto = require('crypto');
const memory = $getWorkflowStaticData('global');
memory.fingerprints = memory.fingerprints || {};
const pages = $('Code').all(); // the URL list node, to recover each page's url
const changed = [];
$input.all().forEach((item, i) => {
const url = pages[i].json.url;
const text = (item.json.content || '').replace(/\s+/g, ' ').trim();
if (!text) return; // failed or empty extract: skip, do not overwrite the fingerprint
const fp = crypto.createHash('sha256').update(text).digest('hex');
const before = memory.fingerprints[url];
memory.fingerprints[url] = fp;
if (before && before !== fp) changed.push({ json: { url } });
});
return changed;
The first production run only records fingerprints. From the second run on, the node outputs one item per changed page and nothing for unchanged ones, so the rest of the workflow runs only when there is something to report.
Two notes. If your Code node is named differently, change $('Code') to its name. On self-hosted n8n, require('crypto') needs the NODE_FUNCTION_ALLOW_BUILTIN=crypto environment variable; n8n Cloud allows it already.
Static data is saved only for production executions, the ones triggered by the schedule after the workflow is activated. Manual test runs do not persist it, which is why the workflow looks like it never remembers anything until you activate it.
6. HTTP Request: screenshot each changed page
Add a second HTTP Request node after the comparison:
- Method: GET
- URL:
https://app.snap-render.com/v1/screenshot
- Authentication: the same Header Auth credential
- Query Parameters:
url = {{ $json.url }}, format = png, full_page = true
- Options, Response, Response Format: File
The image arrives as binary data in the item, usually under the property name data.
7. Slack: post the screenshot
Add a Slack node with the operation that uploads a file, point it at the binary property data, choose the channel, and use a message such as:
Page changed: {{ $json.url }}
Each changed page becomes one Slack message with the full-page capture attached. If nothing changed, the workflow ends quietly after step 5 and posts nothing.
Variations
Send a digest instead. Add an Aggregate node before Slack to combine all changed URLs into one message, and attach screenshots only for the first few.
Archive every day, alert on change. Branch after step 4: one branch saves a daily screenshot of every page to Google Drive or S3, the other runs the comparison and alerts. That gives you a dated history and a quiet channel.
Use the community node on self-hosted n8n. The n8n-nodes-snaprender package adds a SnapRender node with Take Screenshot, Extract Content, Batch Screenshots and Sign URL operations, so steps 4 and 6 become one node each. Install it from Settings, Community Nodes on self-hosted instances. n8n Cloud allows only verified community nodes, which is why this guide uses HTTP Request.
What it costs
Each run makes one extract call per page, plus one screenshot call per changed page.
| Pages |
Schedule |
Calls a month (approx.) |
SnapRender plan |
| 6 |
daily |
180 plus changes |
Free |
| 30 |
weekdays |
660 plus changes |
Starter, $9 |
| 100 |
daily |
3,000 plus changes |
Growth, $29 |
Every row above is one GET request away. 200 free renders a month, no card.
Start free
n8n's own execution limits depend on your n8n plan; this workflow counts as one execution per scheduled run, however many pages it checks.
Related guides
To check what a page's capture looks like before building the workflow, paste it into the free screenshot tool.