Shotlist guide
Capturing
What shotlist capture does step by step, the simulators it manages, change review, the manifest and run log, every flag, exit codes, and how to recover from failures.
Capturing
shotlist capture
One command builds your app, captures every cell (each scene on each slot, in each locale and appearance) and records how each compares with the last run. In the Mac app, the same run starts with Capture (⌘R) in the Matrix.
Capturing needs a subscription. See Subscription.
What you see
Progress goes to the terminal as the run goes, one line per event, and to Shotlist/Output/run.log:
[00:20:54.619] 16 cells: 2 slots × 2 locales, 4 scenes
[00:20:54.620] app app.shotlist.example.tca 1.0 (1) at …/.shotlist/DerivedData/Build/Products/Debug-iphonesimulator/TCAExample.app
[00:20:54.626] control channel on 127.0.0.1:62994; 2 simulators, 2 at a time
[00:21:28.361] iphone-6.3: Shotlist · iphone-6.3 · iOS-27-0 ready
[00:21:28.830] iphone-6.9: Shotlist · iphone-6.9 · iOS-27-0 ready
[00:21:54.594] en-GB/iphone-6.3/01-table-light unchanged
[00:21:54.621] en-GB/iphone-6.9/01-table-light unchanged
…
[00:22:23.188] done in 88 s: 16 captured (0 new, 0 changed, 16 unchanged), 0 failed
At the end, a summary:
16 captured · 0 new · 0 changed · 16 unchanged · 0 failed · 88 s
Manifest: …/Shotlist/Output/manifest.json
Log: …/Shotlist/Output/run.log
New and changed cells are listed under the summary line, and failed cells with their reasons:
1 captured · 1 new · 0 changed · 0 unchanged · 2 failed · 34 s
new en-GB/iphone-6.3/01-shop-light
✗ failed en-GB/iphone-6.3/02-missing-fixture-light: scene 'missing-fixture' failed: No fixture for GET /v1/products/no-such-product (…); looked in Fixtures/en-GB/GET/v1/products/no-such-product and Fixtures/shared/GET/v1/products/no-such-product
✗ failed en-GB/iphone-6.3/03-stuck-hold-light: scene 'stuck-hold' failed: not ready after 8 s: open hold 'a stock check that never finishes'
A cell is named <locale>/<slot>/<NN>-<scene>-<appearance>, where NN is the scene’s position in sequence.
Times in the log are UTC.
What a run does
- Reads
shotlist.jsonand works out the cells, applying any--slots,--localesand--scenes. - Takes the capture lock. One capture runs at a time on a Mac, because every project shares Shotlist’s simulators. A second capture stops at once: “another
shotlist captureis running on this Mac; captures share simulators, so wait for it to finish”. - Builds the app once:
xcodebuild buildfor your scheme and configuration (Debug by default), forgeneric/platform=iOS Simulator, into.shotlist/DerivedData.--skip-buildand--appskip this. - Keeps the last run’s captures of the cells it’s about to capture, in
Output/previous/raw/, for before-and-after review. - Opens a control channel on a loopback port. Each launch of your app connects to it with a one-off token, and the engine accepts only the process it launched.
- Prepares one simulator per device, up to
capture.concurrencyat a time (2 by default). For each:- picks the newest installed iOS runtime that supports the device;
- creates
Shotlist · <slot> · <runtime>if it doesn’t exist, or reuses it; - boots it headless;
- sets its region to
en_USonce, so the status bar reads 9:41 (your app’s locale comes from the run, not the region); - checks its framebuffer against the slot table;
- overrides the status bar: the time, full battery, full Wi-Fi and signal, no carrier;
- uninstalls and reinstalls your app, so the run starts with no app data.
- Works through each locale and appearance on that simulator, one after another:
- sets the simulator’s appearance;
- launches the app with the locale’s language and region, the run’s date, the slot, the scene timeout,
SWIFT_DETERMINISTIC_HASHING=1and yourtimeZone; - waits for the app’s hello;
- shows each scene in sequence order, relaunching first for a scene with
isolation: .relaunch; - waits for the scene to be ready (see Readiness), answering its HTTP requests from fixtures as it goes;
- captures until two consecutive frames match;
- checks the size is one the slot accepts and the image isn’t blank;
- flattens it onto opaque black as sRGB with no alpha channel, and writes
Output/raw/<locale>/<slot>/NN-<scene>-<appearance>.png; - compares it with the last run’s capture of the same cell, and records the result in the manifest.
- For the iPhone Duo, captures every outer-display cell folded, then unfolds once and captures every inner-display cell. See iPhone Duo.
- Shuts each simulator down when its cells are done, unless you pass
--keep-warm. - Writes the summary, and exits non-zero if any cell failed.
The manifest is written after every cell, so a run that stops early still leaves an accurate record.
Simulators
Shotlist only ever uses simulators it created, named Shotlist · <slot> · <runtime>:
Shotlist · iphone-6.9 · iOS-27-0
Shotlist · iphone-6.3 · iOS-27-0
Shotlist · ipad-13 · iOS-27-0
Shotlist · iphone-duo · iOS-27-1
Every simulator command addresses a device by its UDID, never as “booted”, so your own simulators and the apps on them are never touched, even while they’re running.
- They’re shared across projects. One set of simulators serves every app on the Mac, which is why one capture runs at a time.
- They’re reused. The first run creates them, which takes longer. Later runs boot the existing ones.
- A new runtime means new simulators. When a newer runtime is installed, the next run creates simulators for it and logs that the old ones are “for an older runtime and no longer used”. It doesn’t delete them; see Cleaning up.
- They run headless. Device Hub doesn’t need to be open, except to unfold the iPhone Duo.
Don’t quit Device Hub during a run. Quitting Device Hub shuts down every booted simulator on the Mac, yours included. Mid-run, every remaining cell on those simulators fails or has to recover.
Output
| Path | What it is |
|---|---|
Shotlist/Output/raw/<locale>/<slot>/NN-<scene>-<appearance>.png |
The flattened capture: the slot’s exact size, opaque sRGB, no alpha. |
Shotlist/Output/previous/raw/… |
The same cell’s file from the run before, for before and after. |
Shotlist/Output/manifest.json |
One entry per cell: what was captured, its hashes and how it compares. |
Shotlist/Output/run.log |
This run’s log. Each capture replaces it. |
.shotlist/DerivedData/ |
The build. |
.shotlist/capture/ |
Scratch frames while capturing. |
Output/ isn’t committed. Nothing in it needs to be: a capture recreates it.
Change review
Every captured cell is marked against the last run:
| State | In the CLI | In the Mac app | Meaning |
|---|---|---|---|
| New | new |
New | There’s no earlier capture of this cell. |
| Changed | changed |
Changed | Different from the last run. The manifest gives the perceptual distance (0 to 64). |
| Unchanged | unchanged |
Unchanged | Byte-identical to the last run, or different only by Liquid Glass rendering noise, in which case the last run’s file is kept. |
| Failed | failed |
Failed | The cell couldn’t be captured. Its error says why. |
| Skipped | counted as not applicable | Not used here | The scene doesn’t list this slot’s posture (iPhone Duo). |
When a cell is unchanged because of rendering noise, Shotlist puts the last run’s file back in place of the new one, so the bytes don’t change, and records the number of noisy pixels as renderingNoise. See Making runs identical.
To review changes:
- In the Mac app, the Matrix’s Needs Attention filter shows only new, changed and failed cells. Selecting one shows before and after, side by side or with a swipe divider.
- In the terminal, the summary lists new and changed cells. Compare
Output/previous/raw/<cell>.pngwithOutput/raw/<cell>.png.
There’s nothing to accept. The next run compares against whatever this run captured.
The manifest
Shotlist/Output/manifest.json has one entry per cell:
| Field | Meaning |
|---|---|
key |
The cell: en-GB/iphone-6.3/01-shop-light. |
scene, index, slot, locale, appearance, posture |
What the cell is. index is its position in sequence, from 1. |
rawPath |
The capture, relative to Output/. |
width, height |
Its size in pixels. |
sha256 |
The SHA-256 of the PNG file. |
perceptualHash |
A 64-bit difference hash, in hex. |
change |
new, changed, unchanged, failed or skipped. |
perceptualDistance |
For a changed cell: how many of the 64 hash bits differ from last run. |
renderingNoise |
For a cell unchanged by noise alone: how many pixels differed. |
error |
For a failed or skipped cell: why. |
buildNumber |
The app’s CFBundleVersion. |
capturedAt |
When it was captured. |
composedPath, composedInput, captionReview |
Added by compose: the composed file, a hash of everything it was made from, and its caption’s review state. |
The manifest also records the bundle ID, the build number and when the run started and finished. With --slots, --locales or --scenes, entries for cells outside the filter are kept from earlier runs, so the manifest always describes the whole matrix.
Options
| Option | Effect |
|---|---|
--slots <list> |
Only these slots, comma-separated: --slots iphone-6.3,ipad-13. |
--locales <list> |
Only these locales. |
--scenes <list> |
Only these scenes. |
--skip-build |
Use the last build in .shotlist/DerivedData (or app.derivedData) instead of building again. |
--app <path> |
Use this built .app instead of building. It must be a simulator build containing ShotlistKit. |
--keep-warm |
Leave the simulators booted, for a faster next run. |
--concurrency <n> |
Simulators to run at once, overriding capture.concurrency. |
--json |
Print the summary as JSON on stdout; progress goes to stderr as JSON lines. See CI and automation. |
--project-dir <folder> |
The folder that holds Shotlist/, if it isn’t the current one. |
Each filter must name values shotlist.json lists, or the run stops: “–slots names ipad-11, which shotlist.json doesn’t list”. If the filters together match nothing, it stops with “the filters match no cells”.
Typical runs
# Everything.
shotlist capture
# One scene you're working on, on one size, reusing the build.
shotlist capture --skip-build --slots iphone-6.3 --scenes library
# German only, after fixing a translation.
shotlist capture --locales de-DE
# Iterating quickly: keep simulators booted between runs.
shotlist capture --skip-build --keep-warm --slots iphone-6.3
--skip-build only helps when the app hasn’t changed. After changing your scenes or app code, build again.
Exit codes
| Code | Meaning |
|---|---|
| 0 | Every cell was captured or skipped. |
| 1 | The run finished, but at least one cell failed; or shotlist.json isn’t valid; or there’s no subscription. |
| 2 | The run couldn’t proceed: the build failed, another capture holds the lock, the filters matched nothing, or the built app couldn’t be found. |
| 64 | The command line was wrong: an unknown option, say. |
Recovering from failures
A failure stays with its cell. The rest of the run carries on, and the summary lists every failure with its reason.
A cell failed
Read its reason (in the summary, the run log, the manifest’s error, or by selecting it in the Mac app). The reason names the cause: an open hold, a pending URL, a missing fixture, a scene id the app doesn’t have. See Troubleshooting.
Fix it, then capture just the gap:
shotlist capture --locales de-DE --slots iphone-6.3 --scenes orders
Entries for every other cell are kept, so the manifest stays complete.
A simulator quit or crashed mid-run
The cell it was on fails. Shotlist notices the simulator has gone, boots it again, reinstalls the app and carries on with the rest of its cells. The run exits 1. Capture the failed cells again with filters.
If a simulator can’t be prepared at all (no runtime, a framebuffer that doesn’t match, a boot that never finishes), every cell on it fails with that reason, and the other simulators carry on.
The app crashed
The cell fails with “the app (pid …) closed the control channel while waiting for ready from scene ‘…’; it may have crashed”. Shotlist launches the app again for the next cell. Run the scene from Xcode, or look in the simulator’s crash logs, to find the crash.
The build failed
The run stops with “the build failed:”, the exact xcodebuild command, and up to 20 lines containing error:. Run that command yourself to see the full output.
Another capture is running
Wait for it to finish. The lock is released when the other run ends, however it ends. If you’re sure nothing is running and it still says so, a shotlist process is still alive: find it with pgrep -fl shotlist.
Stopping a run
Press Control-C, or Stop (⌘.) in the Mac app. Stopping terminates the commands in progress. The manifest records every cell captured so far. Simulators may be left booted; the next run reuses them.
How long a run takes
Rough figures from real projects on an Apple silicon Mac:
- 16 cells (2 slots × 2 locales × 4 scenes), reusing a build and booted simulators: about 90 seconds.
- 192 cells (3 slots × 8 locales × 8 scenes): about 6 minutes.
The first run is slower: it creates the simulators (boots of new simulators can take 30 seconds) and does a full build. Captures are slowest just after a simulator boots. Relaunching for isolation: .relaunch scenes adds a few seconds each.
Cleaning up
- Disk: each simulator takes 2 to 6 GB, and each project’s build about 1 GB in
.shotlist/. Delete.shotlist/to reclaim the build; the next capture rebuilds. - Old simulators: after a runtime update, simulators for the old runtime stay. List Shotlist’s with
xcrun simctl list devices | grep "Shotlist ·"and remove one withxcrun simctl delete <udid>. Delete only simulators namedShotlist · …. - Previous captures:
Output/previous/holds one earlier copy of each cell. It’s replaced as cells are recaptured.