Docs / Reference
Clips, evals and demos
Your CI screenshots are what @latest shows. Three optional commands publish other images into the same project, beside them.
Overview
| Command | Publishes | Channels it moves |
|---|---|---|
| screenery clip | A capture of a live web page | @clip, @YYYY-MM-DD |
| screenery eval-publish | Screenshots and judge reports from an LLM UI eval | @eval, @eval-{run} |
| screenery demo | A rendered demo GIF and video | The same channels as your screenshots |
clip and eval-publish push to their own channels. They never move @latest.
Because they never touch the default branch's channel or @latest, you can run them from any CI job in your repository, including a pull-request job. See Which jobs may move it. They use the same credential as push: GitHub OIDC, or SCREENERY_TOKEN. See Credentials.
The screenery/push@v1 Action runs only push. Call these commands from a run: step with npx screenery.
Live-page clips
screenery clip captures a page that is already live, such as a marketing page or a docs page, and stores it in an existing project.
npx screenery clip https://example.com --project acme/docs --name docs/home
Each clip lands on two channels, and gets an immutable version URL:
| Channel | What it points at |
|---|---|
| @clip | The newest clip of that name. It moves on every clip. |
| @2026-10-02 | The newest clip of that name on that UTC day. |
| @v_… | These exact bytes, forever. |
| @latest | Not moved. It still follows your default branch. |
Each clip is also compared with the version you approved on @clip-approved, or with the previous clip, so a scheduled job can tell you when the live page drifts. See Monitoring live pages.
https://cdn.screenery.dev/acme/docs/docs/home@clip.png
The CLI captures the page in the first way that works:
- --file: an image you already have, PNG, JPEG, WebP or GIF.
- Playwright, if it is installed where the command runs.
- The screenshotit.app capture service, which fetches the public URL.
screenshotit.app cannot reach localhost. For a local page, install Playwright (npx playwright install chromium) and pass --source playwright, or pass --file.
A clip is not a replacement for CI screenshots. Nothing checks a clip when you merge, so do not swap the @latest images in your README for clips. Use clips in a project that already has CI screenshots, so that a CI build has created its @latest before anyone relies on that URL. From the CLI release after 0.4.0, a clip never creates @latest, not even as a project's first build. On CLI 0.4.0 and older, a project's first build creates its @latest, whatever that build is, so push from CI first.
Clips sit side by side. CLI 0.4.0 pushes every clip as partial, so clipping docs/home, then docs/pricing, keeps both on @clip and on that day's channel. One name at a time on CLI 0.3.0. There, each clip replaces the whole contents of those channels, and docs/home@clip answers 404 after the second clip. Its @v_… URL keeps working. Upgrade, or set the project's channel mode to additive on the console's Access screen.
Clip flags
| Flag | What it does |
|---|---|
| --project <org>/<project> | Required. An existing project. |
| --name <asset> | Required. The asset name, such as docs/home. |
| --source <source> | auto (the default), file, playwright or screenshotit. |
| --file <path> | Use this image instead of capturing. |
| --viewport <size> | default 1280×800, full the whole page, mobile 390×844, or social 1200×630. |
| --channel <name> | The live channel. Default clip. latest and pr-N are refused. |
| --approved-channel <name> | The channel a clip is compared with first. Default clip-approved. See Monitoring. |
| --alert-above <percent> | A change over this percent sends the asset.changed webhook. So does any change of image size, whatever its percent. Default: diff.alertAbove. |
| --date <YYYY-MM-DD> | The dated channel to use instead of today's. |
| --no-date | Do not move a dated channel. |
| --refresh | Ask screenshotit.app for a fresh capture. |
| --out <dir> | Also save the captured image here. |
| --commit <sha> | The commit to record with the clip. |
| --commit-timestamp <time> | The time that orders the clip. Default: now. |
| --dry-run | Print the plan and stop. Captures nothing, fetches nothing, pushes nothing. |
The eval channel
An eval here is a test run of UI that a language model generated: each scenario is rendered and screenshotted, and a judge model scores the result. screenery eval-publish stores those screenshots and the judge's report so a reviewer can open them by URL. It reads the output of uivet, or a plain folder of images.
npx screenery eval-publish ./ci-report --project acme/dashboard
Pass the directory that contains results.json, not its parent. A directory full of run directories is refused, and the error names them. A folder of images works too, with an optional judge.json next to them. JSON cannot be served as an image, so the judge report becomes a PNG card with its own URL.
| Channel | What it points at |
|---|---|
| @eval-{run} | One run. Its id comes from --run, or a UTC timestamp by default. |
| @eval | The newest run. It moves after the run's own channel. |
| @latest | Never moved. |
Assets are stored under eval/, one folder per scenario, so they never replace a product screenshot. The command ignores the job's branch and pull request, so an eval on main does not move @main either.
- A project with no @latest yet is refused. Push your product screenshots first.
- Promoting @eval onto @latest is refused.
- --dry-run builds the cards and prints the URLs. It needs no credential.
- --commit, --commit-timestamp and --force work as they do on push.
What eval-publish does and does not do
Screenery stores the judge's report. It does not judge.
Whatever scored the UI ran before Screenery sees the directory: uivet, or your own judge that wrote a judge.json. Today eval-publish does not:
- run a language-model or image judge, or need a model API key or prompt;
- open a browser to capture the eval's screenshots;
- upload results.json or report.html as files;
- move @latest or @main.
eval-publish reads results.json, or judge.json if there is none. The pass rule depends on what the file holds, not on its name:
| The file holds | Pass | Fail |
|---|---|---|
| Scenarios: a scenarios list, or one scenario with an id | Anything else, including a missing pass. | pass is false or "false". |
| One judge result: overall or scores, with no scenarios and no id | overall is a number of 6 or more. | overall is below 6, missing, or not a number. |
Pass and fail both publish. A failing card is the report a reviewer needs.
Evals in CI
A failing eval is the report a reviewer needs most, so publish on pass and on fail. Let your eval step fail the job, and run the publish step with if: always():
permissions:
id-token: write
contents: read
steps:
# … your eval step, which writes ./ci-report …
- if: always()
run: npx screenery eval-publish ./ci-report --project acme/dashboard
@eval moves when the command finishes, from any job, including a pull request. Merging moves neither @eval nor @latest.
Demo clips
screenery demo publishes a demo you already rendered, as a GIF and a video under one name, and prints the embeds. It does not record or render anything.
npx screenery demo ./demos --project acme/dashboard --dry-run
A directory is a demo if it has storyboard.json (or generated/storyboard.json) or output/final-demo.gif, .mp4 or .webm. A parent such as demos/ publishes every demo inside it. Elsewhere, a GIF and an MP4 with the same name also count as a demo.
| On disk | Asset |
|---|---|
| demos/onboarding/output/final-demo.gif | onboarding |
| demos/onboarding/output/final-demo.mp4 | onboarding/video |
| demos/onboarding/output/stills/poster.png | onboarding/poster |
--prefix demos nests the names, as demos/onboarding. --dry-run prints the mapping and the URLs and sends nothing. The other flags are push's: --commit, --branch, --pr, --commit-timestamp, --run-number, --run-attempt, --concurrency and --force.
Unlike a clip, a demo has no channel of its own. It moves the same channels a screenshot push from that job would, so a demo pushed from your default branch is on @latest, and a README can embed it:

GitHub plays a GIF in a README. Use the .mp4 in a <video> tag on your own pages, with the poster.
A demo push is partial. CLI 0.4.0 marks every demo push as partial, so it adds the demo and removes none of your screenshots. Your next full screenshot push still removes the demo from the channels it moves, so push demos to a project of their own, or set the project's channel mode to additive on the console's Access screen. On CLI 0.3.0, a demo push can remove screenshots, because it is sent as a full build. See What a channel holds.