How to Set Up an AI Agent to Screenshot a Webpage Every Hour and Detect Changes
Build an hourly screenshot monitor that compares each capture with a baseline, filters visual noise, and alerts only when a real change is likely.
To monitor a webpage hourly, schedule a browser capture job, save each successful screenshot, compare it with a baseline at fixed dimensions, and alert only when the difference passes a threshold you have calibrated for that page. Keep capture failures separate from confirmed visual changes. The recurring trigger and the screenshot service are separate parts of the system; verify the selected scheduler’s interval, timezone, and execution behavior in its current documentation.
An AI model does not need to decide whether every pixel changed. Let deterministic code capture, compare, and apply an alert rule. An AI agent can optionally summarize a diff after the rule fires, helping a person triage the change.
1. Choose the capture and comparison approach
For a public page that needs no interaction, a hosted screenshot endpoint can render the page and return an image. Cloudflare Browser Run documents a /screenshot Quick Action that processes page HTML and JavaScript before capture; it supports URL or HTML input and screenshot settings such as viewport, full-page capture, and selector capture. For login flows, clicks, custom browser state, or more involved automation, use a programmable browser such as Playwright or Puppeteer. Cloudflare screenshot endpoint documentation and Browser Run examples.
For this guide, the runnable implementation uses Playwright with Pixelmatch. It runs on a machine or hosted runtime where Node.js and Chromium can run. The job itself can be started hourly by the scheduler in that environment. The comparison code is deterministic; you can connect its alert payload to an AI agent or notification system afterward.
| Decision | Starting choice | When to change it |
|---|---|---|
| Capture method | Playwright browser for the complete example | Use a screenshot API for direct public-page captures; use browser automation when state or interaction is required. |
| Capture area | Fixed viewport | Use full-page for below-the-fold changes or a stable CSS selector for a specific region. |
| Comparison baseline | Approved baseline | Compare with the prior successful run if every hour-to-hour change matters. |
| Alert rule | Start with changed-pixel ratio, then calibrate | Use regional or semantic checks if a page contains unavoidable dynamic content. |
2. Create the hourly screenshot monitor
This example takes a viewport screenshot, compares it with an approved baseline, writes a diff image, and optionally posts an alert payload to a webhook. It creates the baseline on the first successful run. Review that image before treating it as the expected page state.
Install dependencies
mkdir hourly-page-monitor
cd hourly-page-monitor
npm init -y
npm install playwright pixelmatch pngjs
npx playwright install chromium
Save the following as monitor.mjs. Set TARGET_URL and optionally ALERT_WEBHOOK_URL in the environment. The webhook receives JSON; its URL and payload handling depend on your alert destination.
import { chromium } from 'playwright';
import pixelmatch from 'pixelmatch';
import { PNG } from 'pngjs';
import { mkdir, readFile, writeFile } from 'node:fs/promises';
import path from 'node:path';
const targetUrl = process.env.TARGET_URL;
if (!targetUrl) throw new Error('Set TARGET_URL to the page to monitor');
const width = Number(process.env.VIEWPORT_WIDTH ?? 1365);
const height = Number(process.env.VIEWPORT_HEIGHT ?? 900);
const threshold = Number(process.env.PIXEL_THRESHOLD ?? 0.12);
const alertRatio = Number(process.env.ALERT_RATIO ?? 0.005);
const outputDir = process.env.OUTPUT_DIR ?? './captures';
const baselinePath = path.join(outputDir, 'baseline.png');
const webhookUrl = process.env.ALERT_WEBHOOK_URL;
await mkdir(outputDir, { recursive: true });
const timestamp = new Date().toISOString().replaceAll(':', '-');
const currentPath = path.join(outputDir, `${timestamp}.png`);
const browser = await chromium.launch({ headless: true });
let page;
try {
page = await browser.newPage({ viewport: { width, height }, deviceScaleFactor: 1 });
const response = await page.goto(targetUrl, { waitUntil: 'networkidle', timeout: 60000 });
if (!response || !response.ok()) {
throw new Error(`Navigation failed: ${response?.status() ?? 'no HTTP response'}`);
}
await page.screenshot({ path: currentPath, fullPage: false, animations: 'disabled' });
} catch (error) {
console.error(JSON.stringify({
status: 'capture_error', url: targetUrl, time: new Date().toISOString(),
error: String(error)
}));
process.exitCode = 2;
} finally {
await browser.close();
}
if (process.exitCode) process.exit(process.exitCode);
let baselineBuffer;
try {
baselineBuffer = await readFile(baselinePath);
} catch (error) {
if (error.code !== 'ENOENT') throw error;
await writeFile(baselinePath, await readFile(currentPath));
console.log(JSON.stringify({ status: 'baseline_created', url: targetUrl, image: currentPath }));
process.exit(0);
}
const oldImage = PNG.sync.read(baselineBuffer);
const newImage = PNG.sync.read(await readFile(currentPath));
if (oldImage.width !== newImage.width || oldImage.height !== newImage.height) {
console.error(JSON.stringify({
status: 'comparison_error', reason: 'dimensions_mismatch',
baseline: `${oldImage.width}x${oldImage.height}`,
current: `${newImage.width}x${newImage.height}`, image: currentPath
}));
process.exit(3);
}
const diff = new PNG({ width, height });
const changedPixels = pixelmatch(
oldImage.data, newImage.data, diff.data, width, height,
{ threshold, includeAA: false }
);
const ratio = changedPixels / (width * height);
const diffPath = path.join(outputDir, `${timestamp}-diff.png`);
await writeFile(diffPath, PNG.sync.write(diff));
const changed = ratio >= alertRatio;
const result = {
status: changed ? 'visual_change' : 'no_significant_change',
url: targetUrl,
time: new Date().toISOString(),
changedPixels,
totalPixels: width * height,
changedRatio: ratio,
alertRatio,
currentImage: currentPath,
baselineImage: baselinePath,
diffImage: diffPath
};
console.log(JSON.stringify(result));
if (changed && webhookUrl) {
const alertResponse = await fetch(webhookUrl, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(result)
});
if (!alertResponse.ok) {
console.error(`Alert delivery failed with HTTP ${alertResponse.status}`);
process.exitCode = 4;
}
}
Run it once to create the initial baseline, inspect captures/baseline.png, and then schedule it. A newly created baseline is not proof that the page was in the desired state; verify it manually. If you want hourly changes relative to the immediately previous successful capture, update the baseline only after a successful comparison and retain a separate approved copy for review.
3. Schedule the job to run hourly
For a Linux host with cron, this example runs at minute 7 of every hour. Cron syntax and environment loading vary by system, so use absolute paths and verify behavior on the target host. Put secrets in the host’s secret manager or a protected environment file rather than committing them to source control.
7 * * * * cd /absolute/path/hourly-page-monitor && TARGET_URL='https://example.com' /usr/bin/node /absolute/path/hourly-page-monitor/monitor.mjs >> /absolute/path/hourly-page-monitor/monitor.log 2>&1
For a hosted scheduler, configure its recurring trigger for hourly execution and set the same environment variables there. Confirm its timezone, overlap behavior, retry behavior, execution timeout, and retention rules in that platform’s current documentation. Add a lock or concurrency limit if a slow run could overlap the next scheduled run.
Do not assume the screenshot API schedules captures. A recurring trigger must invoke the capture and comparison job. If a run fails, log it as a capture or delivery failure and retry according to your operational needs; do not label it as a visual change.
4. Make screenshots comparable
- Keep the viewport fixed. Width, height, device scale factor, browser, and screenshot scope should remain the same between runs.
- Choose a stable wait condition.
networkidleis useful for many pages but can time out on pages with continuous network activity. For those pages, wait for a known selector or a deliberate short delay after navigation. - Control browser state. Use the same authentication, locale, timezone, cookies, and user agent where they affect rendering. Avoid monitoring a personalized session unless that is the intended view.
- Reduce animation noise. The example disables animations during the screenshot. A page may still include rotating banners, timestamps, ads, randomized content, or live data.
- Limit the capture area. A selected element or stable viewport can avoid unrelated changes elsewhere on the page. Full-page captures can include more changing content and take longer.
- Capture at a stable time. Fonts, images, and client-side rendering can settle after navigation. If necessary, explicitly wait for a page-specific ready selector before capturing.
Playwright’s navigation API documents navigation wait options, and its screenshot guide covers page and element screenshots. For selector capture, replace page.screenshot(...) with await page.locator('main').screenshot({ path: currentPath, animations: 'disabled' }) after confirming the selector is unique and visible.
5. Calibrate the change rule
Pixelmatch compares image data of equal dimensions, returns the differing-pixel count, and can produce a diff image. Its threshold ranges from 0 to 1; lower values are more sensitive. It ignores anti-aliased pixels by default (includeAA: false). Pixelmatch documentation.
The example uses a per-pixel threshold to determine whether a pixel differs, then an aggregate ratio to determine whether the run should alert. Those are separate controls. There is no universal correct alert ratio: compare representative runs from the monitored page and tune the values to the expected noise. The example defaults are configuration starting points, not general performance or accuracy claims.
- Run captures several times when the page should be stable.
- Inspect the diff images and the reported changed-pixel ratios.
- Identify recurring noise, such as a clock or rotating banner. Prefer masking or excluding that region, or capture a stable element.
- Set an alert ratio that suppresses routine variation but still catches changes you care about.
- Test a known visible change and confirm the alert fires.
- Review alerted diffs before replacing the approved baseline.
A threshold that is too sensitive creates noisy alerts; one that is too tolerant can hide small but important changes. Consider separate regions or rules when a small element, such as a price or status label, matters more than the rest of the page. A screenshot diff tells you that pixels changed, not why they changed or whether the change is semantically important.
6. Keep baselines, alerts, and failures reliable
- Keep an approved baseline separate from the latest capture. Automatic baseline updates can normalize a gradual change or an incident before anyone reviews it.
- Retain evidence. Keep the current screenshot and diff with the alert. Record URL, timestamp, viewport, capture result, comparison result, and baseline identity.
- Separate failure states. Navigation timeout, HTTP error, blocked access, changed dimensions, comparison error, and webhook failure should have distinct statuses.
- Make notifications actionable. Include the target URL, time, measured difference, and links or attachments for baseline, current image, and diff when your alert destination supports them.
- Protect access. Screenshots of authenticated pages may contain sensitive data. Restrict storage and webhook access, set a retention policy, and avoid logging credentials or session cookies.
- Prevent overlapping runs. If a capture can take close to an hour, use a lock or platform concurrency control so two runs do not race to write the same baseline or alert.
If you also need to tell whether the visible change reflects a meaningful page-data change, add a separate text or structured-data check. Cloudflare’s snapshot endpoint can return HTML and a screenshot together. Treat that signal as additional context, not as a substitute for comparing the rendered image.
7. cURL, Python, and Node.js capture examples
These are standalone capture examples for ScreenshotNeo. Each makes a single request; put the chosen request inside your scheduled job, then pass the saved image to your comparison step. ScreenshotNeo accepts capture options as API parameters; see the ScreenshotNeo API documentation for parameter names and configuration details.
cURL
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
Python
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
timeout=90,
)
r.raise_for_status()
with open("shot.webp", "wb") as image:
image.write(r.content)
Node.js
const q = new URLSearchParams({
access_key: process.env.SCREENSHOTNEO_API_KEY,
url: 'https://example.com'
});
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: HTTP ${res.status}`);
await import('node:fs/promises').then(({ writeFile }) =>
writeFile('shot.webp', Buffer.from(await res.arrayBuffer()))
);
Use the same capture dimensions and state each time so the comparison is meaningful. The API request captures; your scheduler, storage, image comparison, baseline policy, and alert destination remain part of your monitoring implementation.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. For this monitor, schedule the request and compare its image with your baseline:
curl -G "https://api.screenshotneo.com/v1/shot" \
-d access_key=YOUR_API_KEY \
--data-urlencode url=https://example.com \
-o shot.webp
See the API documentation for request options. Cookie and consent banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks, blank pages, and failed loads are never billed, and response headers say which page verdict occurred and whether the request was billed. An MCP server lets AI agents use take_screenshot, get_page_info, and capture_pdf. The free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000 screenshots. Learn about ScreenshotNeo or sign up for 1,000 free screenshots a month with no card.
Options you may need for a production monitor
ScreenshotNeo provides full-page capture with lazy images loaded; CSS-selector element capture; dark mode; 12 device presets and custom viewports; retina scale; PDF settings; HTML or CSS to image; custom CSS and JavaScript; click-before-capture; hidden selectors; waits for a selector, delay, or network idle; request and resource blocking; custom headers, cookies, user agent, and Authorization; timezone and geolocation; transparent backgrounds; image resizing; cache TTL; signed links for public image tags; asynchronous jobs with signed webhooks; bulk capture of up to 100 URLs per call; a usage API; and an OpenAPI spec. Other screenshot APIs’ parameter names also work to make switching easier. Use only the options that make captures repeatable for your page, and consult the docs for exact request syntax.
For visual monitoring, caching needs special care: a cached screenshot may represent an earlier page state. Configure the cache behavior and TTL deliberately or disable caching for checks where every hourly run must reflect a fresh render. Likewise, choose the viewport, full-page or element scope, waits, cookies, and locale consistently across runs.
Performance, reliability, and cost
Capture time depends on page behavior, navigation waits, browser startup, image size, and selected capture scope. No universal runtime or cost estimate applies to every page and hosting setup. A full-page capture generally contains more image data to store and compare than a small viewport or selected element. Run representative jobs and size timeouts and storage based on observed behavior.
For reliability, distinguish capture success from comparison success and alert-delivery success. Keep enough history to investigate changes, prune old images under a clear retention policy, and monitor whether the hourly job itself is still running. Retries can help transient failures, but repeated capture failures should raise an operational alert rather than silently disappearing.
With a self-managed browser, account for the runtime and storage you operate. With ScreenshotNeo, the listed plans are Free: 1,000 shots/month with no card; Starter: $5 for 3,000; Growth: $15 for 15,000; Pro: $39 for 60,000; Scale: $99 for 250,000; and Business: $249 for 1,000,000. Yearly billing gives two months free, and every feature is on every plan. Budget captures based on monitored URLs and runs: one URL checked hourly is about 24 capture attempts per day before retries or additional areas. Failed loads and cache hits are not billed under ScreenshotNeo’s stated billing behavior.
Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Browser launch fails | Playwright’s Chromium browser is not installed or the runtime lacks browser dependencies. | Run npx playwright install chromium in the deployment environment and follow Playwright’s installation guidance for that operating system. |
| Navigation times out | The page is slow, blocked, or keeps network requests open. | Check the URL and access requirements. Increase the timeout only when justified; for continuously active pages, wait for a page-specific selector instead of networkidle. |
| Screenshot shows a loading state | Capture happened before the important content rendered. | Wait for a stable content selector or a known ready state. Confirm that client-side content and fonts have loaded. |
| Every run reports a change | Dynamic content, animations, personalization, or viewport variation is causing noise. | Fix browser settings and viewport; disable animations; capture a stable selector; remove or mask expected dynamic regions; recalibrate the aggregate alert ratio. |
| A meaningful change is missed | The per-pixel or aggregate threshold is too tolerant, or the changed region is diluted by a large capture. | Lower thresholds carefully, compare a smaller region, or add a separate check for the important text or value. Validate with a known change. |
| Dimension mismatch | Viewport, device scale, page layout, or capture scope changed. | Keep dimensions and capture settings fixed. If a layout change legitimately changes image size, report it distinctly and review it rather than comparing incompatible buffers. |
| Webhook does not alert | Webhook URL, network access, payload expectations, or receiver response is wrong. | Check the receiver’s logs and response code. Keep delivery failures separate from screenshot changes and retry delivery without recapturing when appropriate. |
| Job runs at an unexpected time or overlaps | Scheduler timezone, environment, or concurrency behavior differs from expectation. | Check the selected scheduler’s current documentation, configure timezone explicitly where supported, and add a lock or concurrency limit. |
| Hosted capture returns a bot check or blank page | The target served an interstitial, blocked the renderer, or failed to load. | Treat the verdict as a capture outcome, not proof of a visual change. Check access requirements and use a browser session if interaction or authenticated state is necessary. |
FAQ
Does this need an AI agent?
No. A scheduler, browser, and comparison rule can do recurring capture and detection. An AI agent is useful as an optional layer to summarize an already-detected diff or route it for review.
Should I compare with the previous hour or an approved baseline?
Compare with the previous successful run to detect any recent transition. Compare with an approved baseline when changes should remain visible until a person accepts a new expected state.
Can a screenshot tell me what changed semantically?
It detects rendered visual differences. Add text or structured-data extraction if the monitor must explain which content changed or distinguish a visual shift from a data change.
Can I monitor pages behind login?
Yes, if the capture environment can access the page with a stable authorized session. Use a programmable browser for multi-step login or state setup, store credentials securely, and avoid exposing private screenshots in alerts.


