Screenery

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 withWhen
@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

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.

FlagWhat 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