Screenshots are a build artifact, not a design deliverable
Store screenshots hand-exported once become a set nobody updates. Treat them as generated output with a source, a matrix and a regeneration step.
A store listing wants screenshots. Several per platform, at specific pixel sizes, per device class, multiplied by every locale you support. The number is in the dozens before you have done anything interesting.
Export them by hand once and you have a complete set and a problem you will not notice for months.
The case this post is about
One store-asset run, reporting success, producing 34 of 210 required files:
$ render-store-assets && audit
rendered 34 assets · ok
$ audit --strict
want 210 · have 34 · missing 176
first: ios/de/6.5/hero.png, ios/de/6.5/feature-1.png, …
The first command is honest: it rendered 34 assets and all 34 are correct. It has no opinion about how many there should be, because nothing had declared that.
Two platforms × four locales × three sizes × compositions is 210, and the run had quietly scoped
itself to one platform and one locale. The green ok describes the files it made rather than the
matrix it was supposed to fill.
This post follows those two numbers, because a count with no denominator cannot be short.
The symptom
The set goes stale, and it goes stale asymmetrically — which is what makes it hard to see.
You change a screen. You update the two screenshots that obviously show it, in the one locale you can read, at the one device size you happened to have open. The other twenty-six still show the old screen. Nothing is broken, nothing fails, and the listing is now a mixture of two versions of your product with no marker distinguishing them.

The second shape is the locale you cannot check. If you ship in languages you do not read, a hand-exported set has a part you are structurally unable to review. You cannot tell whether that screenshot shows the right screen, whether the text is truncated, or whether it is simply the English one that got uploaded into the wrong slot. The failure is invisible to the only person looking.
The third is the rebrand, and it is the one that actually hurts. A colour changes, or a logo, or a font — a one-line change in the product — and every screenshot in the set is now wrong. There is no partial response available: it is all of them or none, and "all of them" is a day of work that competes with everything else. So it gets deferred, and the listing carries the old brand for a release or two, which is precisely the moment you least want it to.
The fourth is the device the store added. A new screen size appears in the requirements, and your existing set does not cover it. Hand-exporting means finding a device or simulator at that size, reproducing the same states, matching the same framing — reconstructing a process that was never written down because it happened in an afternoon of clicking.
Why the obvious fix failed
Be disciplined about updating them. This is the rule everybody writes and it is asking for something unreasonable: that a person remember, every time a screen changes, which of twenty-eight images are affected across locales they cannot read. The failure is not carelessness, it is that the mapping from "this screen changed" to "these nine assets are stale" exists in nobody's head.
Keep a checklist of the assets. Better, and it decays the same way every checklist does — it is a document that has to be updated alongside a thing it describes, with nothing forcing the two to agree. It also does not help with the work itself, only with remembering that the work exists.
Hire it out each time. Perfectly reasonable for a launch and wrong as a steady state, because it puts a human round-trip between a code change and its listing. The cost is not the money, it is the latency: a set that takes three days to refresh is a set you will not refresh for a one-line colour change, which means your listing is permanently a few changes behind.
Screenshot the real app in a test run. The closest to right, and it is incomplete in a way that matters. You get real screens — which is the hard part — and you do not get the composition: the device frame, the caption, the background, the arrangement that makes a store screenshot a designed object rather than a capture. Half the artifact is missing and it is the half the store is actually displaying.
The actual mechanism
Treat the set as output, with a source, a matrix and a regeneration step — exactly like any other build artifact.
Three pieces, and the middle one is the one people miss.
A composed source. Each screenshot is defined as a composition — a screen state, a frame, a caption, a background — in a form that can be rendered. Not an image file: a definition that produces an image file. This is where the design work lives, and it is done once per screenshot rather than once per screenshot per size per locale.
A matrix. The set is the cross product of compositions × device sizes × locales × platforms, and that cross product is declared rather than enumerated by hand. Declaring it is what makes a new device size a one-line change instead of an archaeology project, and it is what makes "which assets are affected by this screen" a question with a computed answer.

One master for the icon. The app icon is the asset with the most required sizes and the least tolerance for inconsistency, and it is the one most often maintained as a folder of separately exported files that have quietly diverged. One master, every size derived, no exceptions — because a hand-exported icon set is a set where two of the sizes are from a previous design and nobody can tell which.

The sub-case that decides whether this is usable: the generated set must be cheap enough to regenerate on a whim. If a full regeneration takes forty minutes, people will do the partial thing, and the partial thing is the original problem. That means the expensive step — rendering real app screens — needs to be separable from the cheap step of recomposing frames and captions around them, so a caption fix does not pay for a device farm.
The fix
compositions → what each screenshot is: state, frame, caption, background
matrix → sizes × locales × platforms, declared in one place
icon master → one source, every required size derived
render → produce the full set from the above
The matrix is declared once and the render is a product of it, which is what stops a locale being silently absent from one platform:
const MATRIX = {
platforms: ["ios", "android"],
locales: ["en", "de", "fr", "es"],
sizes: { ios: ["6.7", "6.5", "5.5"], android: ["phone", "tablet7", "tablet10"] },
} as const;
export function expected(compositions: Composition[]) {
const want: string[] = [];
for (const p of MATRIX.platforms)
for (const l of MATRIX.locales)
for (const s of MATRIX.sizes[p])
for (const c of compositions)
want.push(`${p}/${l}/${s}/${c.id}.png`);
return want; // 2 x 4 x 3 x compositions — the full set
}
export async function auditRender(compositions: Composition[], outDir: string) {
const want = expected(compositions);
const missing: string[] = [];
for (const rel of want) {
if (!await exists(`${outDir}/${rel}`)) missing.push(rel);
}
// Coverage is the point. A render that produced "some" assets and exited 0 is
// indistinguishable from a complete one without this.
return { want: want.length, have: want.length - missing.length, missing };
}
A run that looked successful and covered a fraction of the matrix:
$ render-store-assets && audit
rendered 34 assets · ok
$ audit --strict
want 210 · have 34 · missing 176
first: ios/de/6.5/hero.png, ios/de/6.5/feature-1.png, …
The 34 came from a run that read the project's deploy targets instead of the declared matrix. It exited zero, printed a green summary, and skipped three locales on both platforms.
The property worth protecting is that nothing in the output is hand-touched. The moment one screenshot is "just quickly fixed" in an image editor, the set is no longer reproducible, and the next regeneration silently reverts that fix — which is the same class of problem as a hand-edited derived file anywhere else.
There is a second property that only shows up once the first one holds: the set becomes reviewable as a whole. A rendered matrix can be laid out as a contact sheet — every composition, every size, every locale, side by side — and that is the first time anybody can actually look at the listing as the object a user encounters rather than as twenty-eight files in a folder. Several of the problems in the symptom section are instantly visible in that view and effectively invisible without it: the locale whose text overflows, the one device size where the caption collides with the frame, the screenshot that is still showing last quarter's screen.
Hand-exported sets cannot be reviewed that way, not because nobody thought of it, but because assembling the contact sheet is itself a half-day of work that has to be redone every time anything changes. When the set is generated the contact sheet is just another render, which is the sort of second-order benefit that is hard to argue for in advance and obvious the first time you use it.
That also makes the output directory safe to delete, which is a better test of the design than any check: if you can remove the whole set and reproduce it exactly, it is generated. If you hesitate, something in there is a source and you have not noticed.
How we proved it
The pipeline runs end to end — compositions through a declared matrix to a rendered set, with the icon derived from a single master. The part I would point at as evidence rather than description is the icon: one source, every size produced from it, no folder of separately exported files to drift.
The failure that taught me the ordering is worth stating because it is the kind of thing that reads as obvious afterwards. The render and export steps consume the composed decks and the icon master; running them directly, without the step that produces those inputs, fails on missing inputs — zero media, no icon resolvable. The pipeline has a prerequisite structure, and bypassing the orchestration to call the last stage directly does not skip ahead, it just fails in a confusing place.
What I have not measured is whether the generated set performs better than a hand-made one. Everything above is an argument about staleness and cost, not about conversion. A beautifully hand-crafted set that is three months out of date and a generated set that is current are different trade-offs, and I have assumed current beats crafted without testing it.
What I have not solved: the compositions themselves still need design judgement, and that judgement is not generated. What the pipeline removes is the multiplication — doing the same design decision twenty-eight times — not the decision. A badly composed screenshot renders badly at every size in every locale, perfectly consistently, which is an improvement in reproducibility and not in quality.

And a real limitation: screen states. A screenshot showing a populated list needs that list populated, which means the generated path has to produce realistic state — and the gap between "renders the screen" and "renders the screen with convincing content" is where most of the remaining manual work lives. Mine is handled with fixtures, which are themselves a thing that goes stale, one layer down from the problem I started with. That is a real regress rather than a rhetorical one: I have moved the staleness from twenty-eight images to one fixture set, which is a much better place for it to live and is not the same as having removed it.
What this generalizes to
Any set of artifacts produced by multiplying a small number of decisions across a large number of variants should be generated, and the giveaway is the multiplication rather than the volume.
Twenty-eight screenshots is not a lot of images. It is four decisions taken seven times each, and that is the signal: when you find yourself making the same judgement repeatedly with one parameter changed, the parameter belongs in a matrix and the judgement belongs in a source.
The test is the one above — can you delete the entire output and reproduce it? If yes, it is generated and staleness is a regeneration away. If no, some part of the output is secretly an input, and that part will be the thing that is wrong in six months, because nothing regenerates it and nothing checks it.
One post a week, no digest
New writing when it lands. No roundups, no course pitch, no "10 tips".