Prompt packs for empty / error / loading states — stop shipping happy-path only
Agents are weirdly good at the happy path. Give them a Figma frame of a filled dashboard and you get a filled dashboard. Give them a “build a projects list” prompt and you get rows, avatars, and a neat toolbar. What you usually don’t get: the empty list on day one, the 500 that lands mid-scroll, or

Agents are weirdly good at the happy path. Give them a Figma frame of a filled dashboard and you get a filled dashboard. Give them a “build a projects list” prompt and you get rows, avatars, and a neat toolbar.
What you usually don’t get: the empty list on day one, the 500 that lands mid-scroll, or the skeleton that doesn’t jump when real data arrives. Those states are where users decide whether the product feels finished — and they’re the first thing agents skip unless you ask for them on purpose.
So I stopped describing “the screen” and started shipping prompt packs for the three non-happy states every surface needs.
For any list / detail / settings panel the agent touches, I keep a tiny markdown pack next to the feature (or in the PR description). Four required stories:
If the agent only returns #1, the PR is incomplete. Same bar as “missing tests.”
Example pack stub I paste into the agent chat:
## Surface: Projects list (`ProjectsPage`) Implement ALL four states before considering this done. ### Happy - 3–12 projects; name, updatedAt, owner avatar - Primary CTA: New project (top-right) - Row click → project detail ### Empty (three variants — do all) - First-run: zero projects ever → illustration + “Create your first project” - Filtered empty: filters active, zero matches → “No projects match” + Clear filters - Archived-only: toggle shows archived but none → short copy, no big empty art ### Error - List fetch failed → inline error in list region, Retry button, keep chrome - Do NOT full-page crash the app shell - Preserve last-known list if refetch fails (stale-while-error) ### Loading - Initial: skeleton rows matching final layout (same columns / gutters) - Refetch: subtle top progress or opacity — do NOT remount full-page spinner - No layout shift when data lands (reserve avatar + title width) Enter fullscreen mode Exit fullscreen mode That pack is boring on purpose. Agents love inventing a clever empty illustration and forgetting Clear filters.
“Empty state” is not one design. First-run is marketing-adjacent. Filtered-empty is a recovery path. Permission-empty (“you can’t see any projects yet”) is a different CTA entirely.
If you only say “add an empty state,” the model picks the prettiest one and ships it for every case. Then support gets “I filtered by tag and the whole page turned into onboarding.”
Name the variants in the pack. Force the agent to emit separate components or clear branches:
type ListPhase = | { kind: ' loading '; mode: ' initial ' | ' refetch ' } | { kind: ' error '; error: Error; stale: Project [] | null } | { kind: ' empty '; reason: ' first-run ' | ' filtered ' | ' archived ' | ' forbidden ' } | { kind: ' ready '; items: Project [] }; Enter fullscreen mode Exit fullscreen mode I ask the agent to type the phase union first. The UI has somewhere to hang after that.
Common agent mistakes I reject in review:
### Error contract - Error UI lives *inside* the list landmark ( `role="region"` / labelled heading) - Retry calls the same query fn; disable button while in flight - If `stale` exists, show dimmed list + banner — don’t blank the page - Log `error.message` for support; never render raw stack to the user Enter fullscreen mode Exit fullscreen mode Agents follow contracts better than vibes.
Happy-path demos hide CLS. Real users notice when the toolbar drops 40px after load.
I put explicit anti-shift rules in the loading section:
### Loading anti-shift - Skeleton row height === real row height (measure once, hardcode) - Keep page title + primary CTA mounted during initial load - Prefer `content-visibility` / fixed skeleton count over “spinner in a void” - Refetch must not replace the list with a centered spinner Enter fullscreen mode Exit fullscreen mode If the design system already has <Skeleton>, tell the agent which one — otherwise it invents a new shimmer.
Don’t merge agent UI that only has a happy path Storybook story.
export default { title: ' Projects/List ', component: ProjectsList }; export const Happy = () => < ProjectsList phase = { { kind: ' ready ', items: mockProjects } } />; export const FirstRun = () => < ProjectsList phase = { { kind: ' empty ', reason: ' first-run ' } } />; export const FilteredEmpty = () => < ProjectsList phase = { { kind: ' empty ', reason: ' filtered ' } } />; export const LoadError = () => ( < ProjectsList phase = { { kind: ' error ', error: new Error ( ' 503 ' ), stale: null } } /> ); export const InitialLoading = () => ( < ProjectsList phase = { { kind: ' loading ', mode: ' initial ' } } /> ); Enter fullscreen mode Exit fullscreen mode Five stories beats a paragraph of “please handle edge cases.”
Happy path is still the demo. Empty / error / loading are the product. Put them in the prompt pack up front, or you’ll keep shipping demos.