Docs / Reference
Monitoring live pages
Is your live marketing site still what your docs and screenshots say it is? Clip it every day. Each clip is compared with the version you approved, and Screenery tells you when it drifts.
Monitoring needs the CLI release after 0.4.0. On CLI 0.4.0 a clip is graded against the previous clip only, with no approved channel and no asset.changed.
How a clip is graded
screenery clip captures a live page into @clip (see Live-page clips). Before it stores the capture, it compares it with an earlier version of the same name, using the same visual diff every push uses, and records how much it changed and how badly.
| Compared with | When |
|---|---|
| @clip-approved | The version you approved. Used whenever that channel holds the name. |
| @clip | The previous capture, when nothing is approved yet. |
| @latest | Never. That channel is your CI screenshots, not the live site. |
Until you approve a change, every clip of the drifted page is compared with the approved version. With an alert bar set (--alert-above or diff.alertAbove), every one of those clips sends asset.changed; with no bar, none does.
That is the point of an approved version: drift does not go quiet just because it happened yesterday. With nothing approved, each clip is compared with the one before, so a change shows up once, on the day it appears, and sends asset.changed that day only if an alert bar is set.
A clip is a monitoring build. It never moves @latest, not even in a project that has none yet, and diff.failAbove never refuses it: the live page already looks like this, and refusing to record it would hide the drift. The clip prints what it found:
drift 14.2% · major from v_9f2c41a07d3e (@clip-approved) over the alert bar: asset.changed raised if this change is intended, approve it: screenery promote acme/site --to clip-approved --version v_51b0e8c2aa47 --asset live/home
Approving drift
Approving is a promotion: point @clip-approved at the capture you accept. Clip never moves that channel, so only a person (or an agent you asked) approves.
Approve one page, exactly the capture the clip printed:
npx screenery promote acme/site --to clip-approved --version v_51b0e8c2aa47 --asset live/home
Approve every page as it is on @clip now. Do this once, after the first clip, to set the baseline:
npx screenery promote acme/site --from clip --to clip-approved
The console's channel screen promotes the same way, and an agent connected over MCP can call screenery_promote. screenery_clip returns the drift and tells the agent to ask you before it approves. Promoting needs the publisher role; see How @latest moves for how promotion works.
A daily workflow
Screenery does not run on a timer. Your CI does: copy this into .github/workflows/screenery-monitor.yml in any repository bound to the project.
name: Watch the live site
on:
schedule:
- cron: "17 6 * * *" # every day, 06:17 UTC
workflow_dispatch:
permissions:
id-token: write # GitHub OIDC: no secret to store
contents: read
jobs:
clip:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4 # for screenery.config.json, if you keep one
- uses: actions/setup-node@v4
with:
node-version: 22
- run: npm install --no-save "screenery@>0.4.0" playwright
- run: npx playwright install --with-deps chromium
- run: npx screenery clip https://www.example.com/ --project acme/site --name live/home --alert-above 1
- run: npx screenery clip https://www.example.com/pricing --project acme/site --name live/pricing --alert-above 1
- screenery@>0.4.0 asks for a CLI that can monitor. Until that release is on npm, the install step fails, so the workflow never clips with a CLI that cannot grade or alert. A plain screenery would install whatever npm calls latest.
- Installing playwright next to screenery makes the clip capture in a real browser on the runner. Without it, clip uses the screenshotit.app capture service, which works for public pages; add --refresh so each run is a fresh capture.
- A clip moves @clip and the day's channel, never your default branch's channel or @latest, so it is safe from any job. See Which jobs may move it.
- Outside GitHub Actions, set SCREENERY_TOKEN to a publish token. See Credentials.
- GitHub may delay or skip scheduled runs on a busy hour; a minute past the hour is quieter.
Alerts
Two ways to hear about drift. Use either, or both.
In chat. On the channels screen, + drift rule starts a rule that posts to a chat target when a clip changed at least moderately:
{
"on": "build.finalized",
"filter": { "conditions": { "mode": "monitoring", "worst_at_least": "moderate" } },
"actions": [
{ "kind": "notify_slack", "target": "ntg_…" }
]
}
It reads "When a build finalizes from a clip with a change at least moderate, post to #marketing on Slack." The mode condition is monitoring for a clip and ci for every other push, so the same project's CI pushes do not post here. The message says the live page drifted and how to approve it. See Chat notifications.
By webhook. A webhook subscribed to asset.changed gets one request per page that changed more than the alert bar: --alert-above on the clip, or diff.alertAbove in screenery.config.json.
The asset.changed webhook
Add the webhook on the console's Access screen and tick asset.changed. It is sent once per screenshot whose change is over the bar: more than the percent of pixels, or any change of size. A push with no bar sends none. It fires for CI pushes too, when diff.alertAbove is set; mode says which it was.
{
"delivery": "whd_…",
"event": "asset.changed",
"event_id": "ev_…",
"created_at": "2026-10-10T06:17:42.118Z",
"data": {
"project": "acme/site",
"build_id": "b_…",
"mode": "monitoring",
"commit": null,
"branch": "clip",
"pr": null,
"asset": "live/home",
"version": "v_51b0e8c2aa47",
"content_type": "image/png",
"visibility": "public",
"channels": ["clip"],
"alert_above": 1,
"change": {
"schema": "screenery.version-change/1",
"from": "v_9f2c41a07d3e",
"status": "changed",
"percent": 14.2,
"worst": "major",
"regions": 3
},
"finalized_at": "2026-10-10T06:17:41.904Z"
}
}
change.from is the version it was compared with: the approved one, or the previous clip. status is changed or size_changed (then percent is null). Like every Screenery webhook, the request is signed: x-screenery-signature: sha256=… is an HMAC-SHA256, with your webhook's secret, of x-screenery-timestamp, a dot, and the raw body. Refuse a timestamp more than five minutes old, and use delivery to ignore a retry you already handled.
Flags
These two are new; the rest are in Clip flags.
| Flag | What it does |
|---|---|
| --approved-channel <name> | The channel a clip is compared with first. Default clip-approved. It cannot be the live channel, latest or pr-N. |
| --alert-above <percent> | The alert bar, 0 to 100. A change over it sends asset.changed, and so does any change of image size, whatever its percent. Default: diff.alertAbove from screenery.config.json, else no bar. |
Only the live channel's capture is compared. The dated channel stores the same bytes, so it is not compared again and does not alert twice.
What it does not do
- It does not schedule anything. The workflow above, or any cron you have, runs the clips.
- It does not tell you when a page has not been clipped for a while. If the workflow stops, nothing alerts.
- It does not compare a clip with your CI screenshots. A live page and a test render differ in data, fonts and timing; compare like with like.
- Approving is never automatic. A person promotes, or an agent after asking.