Back to Blog

How I Use a Graph to Evolve My UI Design in Claude Code

Graph engineering went viral this summer as a rename for agent orchestration. Here's how I actually use one, in Claude Code, to evolve a UI design over rounds.

Sean Robb
11 min read
#ai

In mid-July, a one-liner from Peter Steinberger split AI Twitter: “Are we still talking loops or did we shift to graphs yet?” Harrison Chase, who built the framework everyone points to, gave the skeptical read: “so i didn’t really know what graph engineering is, and i still don’t really… but it’s basically just langgraph?” Naming has been on a treadmill for years, prompt engineering to context engineering to loop engineering to graph engineering, and every rename earns eye-rolls from some and a scramble from others. What actually went viral underneath the label is orchestration: agents as nodes, dependencies between them as edges. He’s not wrong that most of it is a rename of patterns people already shipped, but the shape is worth showing, not arguing about.

I built a Claude Code skill called /options on exactly that shape, and the afternoon that tested it started with something as ordinary as asking for twelve versions of a landing-page hero.

Ask Claude Code to build twelve hero variants for a landing page and it hands them back before lunch. Twelve real routes, twelve real components, wired into the real app. Generating options used to be the hard part of a design pass. Now it’s the fast part.

Then I opened all twelve and started clicking through them, and by the sixth one I couldn’t tell you which was good and which was just familiar. My taste had evaporated somewhere around option four. I’d generated more work than I could actually judge, and doing it in an afternoon instead of a week didn’t make me any better at judging it.

AI made competent free. The judgment to use it well didn’t get cheaper.

The System

/options runs two different kinds of graph, and the difference between them is the whole reason this counts as graph engineering instead of a renamed pipeline.

The persistent layer is an org graph: a lead builder, one reviewer per persona, a best-practices researcher, all named roles that survive across rounds. The ephemeral layer is a work graph: variant builders that spin up for a single round, build exactly one option, and disappear the moment the round closes. An org graph with no disposable workers can’t generate twelve real routes before lunch. A pile of disposable workers with no persistent reviewers can’t tell you which of the twelve is worth keeping.

None of this is worth building for a single, well-scoped task with a clear pass/fail check. A loop is enough there, and a graph is overhead you don’t need. /options earns the extra shape because it’s judging taste across rounds, not passing a test once.

The work-graph half greps the codebase for how the component actually gets called, pulls the richest set of props off a real call site, and renders each variant inside production’s actual wrapper chain, so a variant can’t quietly render at a width the app could never ship. Every option is a real page at a real URL, wearing the site’s real CSS.

That solves generation. It does nothing about judgment, and judgment was the thing costing me time.

Half a graph — the builders fan out, every verdict lands on me
Me Lead Builders build 12 hero variants spawn 12 builders one variant each, then gone 12 tabs, no verdicts judge all 12 alone taste evaporates by #6

So before it hands variants back, the org-graph half assembles a review panel. It looks for persona docs already sitting in the repo, the ICP writeups that describe who actually buys the thing, and if none exist it asks for two personas with opposing failure modes: one that punishes burying the facts, one that punishes overselling. A single reviewer just tells you what it likes. Two reviewers who fail in opposite directions tell you where the actual tension is.

Each persona gets the same screenshots of every variant and the same manifest notes describing what each one was going for, and returns a strict verdict. LIKE means it would ship exactly as-is. Anything short of that is PASS, and every PASS carries exactly one flip change, the single highest-leverage edit that would move it to LIKE. Not a punch list. One thing.

{
  "by": "A",
  "verdict": "PASS",
  "flip_change": "surface the company + title above the quote, not below",
  "note": "I read the quote before I know who said it. Attribution should lead."
}

Every verdict folds straight into the manifest and renders inline next to its variant in the feed, no separate report to go read.

After — the graph runs the round
Me Lead Builders Reviewers per round /options round spawn builders each builds one variant variants + manifest send to personas judge independently LIKE / PASS verdicts verdicts fold in cull, keep champion

The Loop

A round is never one pass and done. Whatever gets zero likes gets killed, and deciding what should grow in its place is the researchers’ job, not the lead’s. I built that job one researcher at a time, and each addition fixed exactly the failure the last one exposed.

The first researcher I gave the loop was the feedback integrator. It checks the manifest for every outstanding flip-change and owner direction and makes sure a brief exists for each one. That’s all it does, so the loop could only ever build what the two reviewing personas had already asked for. It does exactly what it’s told, which means it can never propose anything nobody requested, and it converges on whatever the personas already wanted instead of on something good.

Adding the best-practices researcher fixed the badness and traded it for sameness. It grounds each proposal in a real source and moves a proven variant ten to twenty percent further, and that raised the floor. But it’s contractually capped at low or medium variance and has to parent something already working, so by construction it refines toward what’s already known to work. Everything a round produced came out competent and interchangeable. Best practices are, definitionally, what everyone else is already doing.

The bold designer put the variance back. It’s the only lane allowed high variance, the only one that can start from nothing instead of from a parent, and every proposal it makes has to name the premise it’s rejecting. It’s also the only one permitted to challenge the reviewing personas themselves rather than the design, and it’s the only agent in the loop running on Opus. It has no web access on purpose, so its divergence comes from principles instead of from looking at what competitors already shipped. Feedback alone builds what it’s told, best practices builds what everyone already does, and bold is the only seat that can build something nobody asked for.

Continuity is the optimization everywhere else in this loop, but that one seat inverts it. The best-practices researcher persists so its research compounds across rounds, while the bold designer gets spawned fresh every round, because a persistent one starts anchoring on its own prior swings. The builder that implements its brief is spawned fresh too, so a premise-rejection can’t inherit the parent builder’s frame. It’s a permanent seat filled by a new occupant every time. The feedback integrator still runs after both and can’t close the round while a single gap remains.

The next round breeds from whatever the researchers hand back, and that lineage is the actual graph the design travels along. Every variant carries a parent, and when two survivors get combined into one, a merge_of:

{ "id": "v9", "parent": "v3", "merge_of": ["v3", "v5"] }

That pointer is why round four looks like it grew out of round one instead of just being a different set of guesses.

Each round breeds from whatever survived the last one
Me Lead Researchers Builders Reviewers every round v2 and v6: zero likes kill v2, v6 what's worth trying next? 1 bold swing, 2 refinements build the set v9, parent v3 judge the new batch champion moves to v9 star migrates pin it, or run it again

The page itself is also a control surface. I get my own Like, Pass, Kill, and Champion buttons right on each card, and the terminal folds my calls into the manifest as a third reviewer alongside the personas. But mine never counts toward the persona tally, and it can’t be outvoted by one either. The personas can push a champion star around all afternoon. The pin that decides what actually ships is mine.

Try It Yourself

The full skill is more machinery than most people need: framework detection, a production wrapper chain measured at multiple viewports, a manifest schema, a kill/regen cycle, an org graph and a work graph both running at once. If your app isn’t already on Astro or Next, or you’re not ready to add a React island just to run a design loop, that overhead isn’t worth taking on for one page.

The part worth stealing is two things, not one: the reviewer setup, and the three researcher tempers that decide what gets tried next. Two personas with opposing failure modes, a strict LIKE or PASS with no in-between, exactly one flip change per PASS. Then feedback integration, best practices, and the bold swing, covering what was asked for, what’s proven, and what nobody thought to ask for. Neither half needs a graph. Both run on a screenshot you paste into a chat just as well as they run on a scaffolded route.

You are running a compressed version of a two-step design loop: review, then decide what to build next. Do this in two steps, in order. STEP 1: REVIEW Play two independent design reviewers judging the same set of variants. Keep both personas in this single response, clearly separated.
You are running a compressed version of a two-step design loop: review, then decide what to build next. Do this in two steps, in order. STEP 1: REVIEW Play two independent design reviewers judging the same set of variants. Keep both personas in this single response, clearly separated. Persona A: [PERSONA_A_BRIEF, e.g. "a cautious buyer who punishes buried facts and vague credibility claims"] Persona B: [PERSONA_B_BRIEF, e.g. "a design-literate skeptic who punishes overselling and decoration"] Here are the variants, as [screenshots attached / URLs / pasted markup]: [VARIANT_LIST] For each variant, each persona returns: - verdict: LIKE (I would ship this exactly as-is) or PASS (anything short of that) - if PASS: exactly one flip_change, the single highest-leverage edit that would move it to LIKE. Not a list. - a 1-2 sentence note, in that persona's voice, explaining the verdict Then each persona names their one champion variant. Be opinionated. No fence-sitting, no "it depends," no both/either verdicts. If you can't judge something from what I gave you, say what specifically you can't verify instead of guessing. STEP 2: WHAT TO TRY NEXT Using every flip_change and note from Step 1, propose what to build for the next round. Play three roles, in this order. Bold: propose one or more high-variance swings. Each one names the specific premise of the current variants it's rejecting, and is allowed to start from nothing instead of improving on what exists. This is the only role allowed to ignore what the personas said they wanted. Refine: propose two to four lower-variance changes, each building on whichever variants in Step 1 got closest to LIKE. Each one cites a real source, a pattern, a study, a product you can name, not "best practice" as an unsupported assertion. Coverage check: go back through every flip_change and note from Step 1 and confirm a proposal exists that addresses it, from either Bold or Refine. If something raised in Step 1 has no proposal against it, say so plainly and name what's uncovered. "A bolder version of variant 3" is not a bold proposal. "Reject the premise that trust requires a testimonial at all" is.

The first time I ran this against a batch of my own variants, both personas agreed on the same champion I would have picked eventually, just faster and for reasons I hadn’t fully articulated to myself yet. The second time they split, and the split was the useful part. It told me exactly which tradeoff I was actually making by picking one over the other, instead of me discovering it three weeks later from a confused visitor.