Brilliant's Blueprint: teaching agents to speak design
The problem
Agents are extraordinarily good at code. It's the single largest bucket of training data they have. Give Claude a short syntax overview for any programming language and it will produce thousands of lines of accurate syntax without effort.
But design tools are not frontend code. They're not code at all, really.
Frontend code is optimized for being written once and running many times. Source goes in, rendered UI comes out, and the round trip from "I changed a color" to "I see the new color" is a build step. Design tools flip that. They're optimized for thousands of tiny interactions per session, each one updating a complex rendering graph in milliseconds, each one undoable, each one immediately reflected back on screen.
The data models reflect that difference. A design tool's model has auto-layout solvers that propagate on every property change, hit-test trees, selection state, snap guides, layer ordering that affects both stacking and z-order. A design isn't a static tree like a JSX document. It's a live graph built to support continuous direct manipulation.
And there's a second problem: verbosity.
To version-control a design, we need an on-disk format that agents and humans can review. Something that diffs cleanly in git. We landed on YAML. .design files are plain-text YAML, one property per line, legible at a glance.
elements:
- id: fb61011f9de5d310
type: frame
name: "Hero"
fills:
- id: bac3eb9a0caf3ac6
color: "#F8F8F8"
cornerRadius: 16But the underlying data is heavy. Every element has dozens of properties, many with sub-properties, many referenced by ID across the file. A single card with a few children runs to dozens of lines of YAML. Every line the agent writes costs tokens. Every line of response costs tokens. On a full design the agent ends up spending most of its context on field names rather than on decisions.
So: we need a representation that's readable enough for version control, fast enough to keep up with continuous manipulation, and light enough that agents don't burn their context describing what they want.
What we tried
Two attempts. Both taught us something.
Commands
The first try looked obvious from inside the codebase. Brilliant already had a robust command system: every user action the app can do (move, resize, fill, group, align, flatten, and several hundred more) is expressible as a typed Command. Why not let the agent call commands directly?
ChangeToolCommand
SetColorCommand
ResizeCommand
RotateCommand
GroupCommand
AlignCommandThe approach explodes fast. Creating one card with a heading is five or six command calls. The codebase carries several hundred typed Command classes, each with its own argument schema, and "which command for which thing" is a lookup the agent has to learn from scratch. Worse, the agent can't express relationships compactly. Every property goes through its own named operation. Token cost climbs linearly with the amount of design, and the agent's attention gets eaten by plumbing.
YAML diffs
The second attempt was the opposite direction. Since .design files already live as YAML, why not let the agent emit YAML directly and apply it to the canvas?
Similar to what agents do with code. They read the contents of a file, make edits, and write it back. The agent can express relationships directly through references. The app can diff the old and new YAML, figure out exactly which properties changed, and apply only those to the canvas.
Sounds fantastic, right? But what happens when you want to undo changes? Or when the agent writes something invalid? Does the user have to sit and wait while the whole file is re-parsed and re-applied on every turn? What about auto-layout rules that depend on multiple properties being set together? The agent would have to guess the right order of operations to avoid intermediate invalid states. The YAML would be verbose, and the agent would have to learn the entire schema.
Also: YAML that's nice to read is not YAML that's nice to write at scale. The example from the last section, the card with one fill and a corner radius, was eight lines. Add a few children and a couple of variants and you're back to dozens of lines per agent turn.
The revelation
At some point the framing clicked. We'd been trying to make the same representation work for two completely different audiences. Version control needs structure, labels, and explicitness. Agents want density, regularity, and anything that resembles a programming language they've seen before.
Human-readable and agent-readable are not the same thing.
We looked at what agents are genuinely good at. Code. Specifically, unfamiliar programming languages. Give Claude a page of syntax for a language it's never touched and it will produce working programs on first try. All programming languages share the same underlying machinery: tokens, expressions, scoping, references, nesting. Any reasonable syntax is just another skin on ideas the model has already internalized very deeply.
Here's the thing, though. No programming language exists for 2D vector design. yet.
Brilliant's Blueprint DSL
Blueprint is a programming language for drawing, optimized for what agents are good at. It's line-oriented, deeply abbreviated, and structurally regular. It has types (rectangles, circles, text, frames, groups, auto-layout containers, icons, vectors, components and instances), properties (position, size, fill, stroke, radius, effects, auto-layout), and references. The core grammar fits on a page or two. Each element is one line rather than a stanza of YAML.
Here's the same card from the YAML example, in Blueprint:
fr f[(#F8F8F8)] rd(16) "Hero"One line. Same element. Same semantics. About five times fewer characters, and eight lines of YAML collapsed to one. That ratio only widens as designs grow, because every element in Blueprint stays a single line while its YAML equivalent keeps sprouting nested keys.
The architecture
We built the compiler around in-context learning. Every time an agent sends a line, the compiler reads it, checks it, and builds diagnostic feedback. The feedback is designed to be read, not just obeyed.
There are three stages, and the first fuses two jobs.
1. Parse and validate, line by line. Each line is tokenized into a loose map, and the validator checks that map immediately, before anything touches the canvas. Wrong property for a type, missing required argument, unresolvable reference, an impossible auto-layout rule: each becomes a BlueprintDiagnostic with a severity (error, warning, info), a category (syntax, property, layout, reference, composition), a stable code (B101 and friends), a human-readable message, and a concrete suggestion. A fatal line error stops there; a single bad property token drops just that property and the rest of the block still applies.
2. Execute. Valid lines apply to the canvas as real, editable elements. Auto-layout recomputes, components propagate, the graph updates atomically.
3. Lint. After execution, the composition linter, the one genuinely separate post-canvas stage, inspects the real geometry for design-intent issues. Its diagnostics carry C-prefixed codes.
A validation diagnostic the agent actually receives looks like this:
B101: spaceBetween is only valid on the main axis (x for al(h)).
Move sb to x(): x(sb) y(c).Code, sentence, fix. Three beats. The agent reads it, adjusts the next line, and moves on.
Brilliant has an unfair advantage for this loop. Unlike frontend code, we don't need to spin up a browser to snapshot the result. Every response can also carry an instant visual export of the affected elements. The agent sees exactly what it made, next to the exact feedback on what went wrong.
And layers that look fine individually but don't work together? That's the composition linter. It catches things like:
Text with no fill, defaulting to white, rendered on a light background. Invisible text. (
C201)Elements that resolve to
0 x NorN x 0after layout. Collapsed. (C301)Sibling elements with the same name under the same parent. Duplicate Row, duplicate Row, duplicate Row, the agent got stuck. (
C101)Text that overflows its parent, or content that spills outside a clipping frame.
A block dropped far away from the rest of the work, stranded off in empty canvas.
Each maps to a diagnostic code, each runs only on the elements the agent just created or modified, and each comes back with a suggestion the agent can act on without another round trip.
Examples
Three real exchanges, to show how the loop feels. These run with no design system active, so colors are plain hex.
Invisible title
Agent sends:
fr f[(#FFFFFF)] rd(16) "Card"
t("Welcome",Manrope,24,sb) "Title" #titleBrilliant applies both lines. The frame renders, the text renders, and the composition linter catches the problem:
✓ 2 lines applied.
⚠ C201: "Title" (text) has no fill.
Defaults to white, may be invisible on light backgrounds.Along with the warning comes a rendered preview showing the empty-looking card. The agent sees both, amends the text line, and resends:
#title f[(#09090B)]This time the lint passes. The title renders black on white.
A malformed alignment
Agent sends:
al(h,y(sb),x(c),g(12),pad(16)) "Toolbar"The alignment axes in Blueprint are physical: x is horizontal, y is vertical, and they never flip with layout direction. sb (space-between) only makes sense on the main axis, which for a horizontal row is x. The validator rejects the line before anything touches the canvas:
✗ 0 lines applied (1 error).
B101 (line 1): spaceBetween is only valid on the main axis (x for al(h)).
Move sb to x(): x(sb) y(c).The agent applies the fix without guessing:
al(h,x(sb),y(c),g(12),pad(16)) "Toolbar"Applied cleanly.
A hug parent with a fill child
Agent sends:
al(v,g(8),pad(16)) s(hug,hug) "Card"
fr s(fill,120) f[(#FFFFFF)] rd(8) "Image Slot"Brilliant applies both lines. The image-slot frame resolves to 0 x 120, because a fill width inside a hug-width parent has nothing to stretch to. Execution succeeds, and the linter fires:
✓ 2 lines applied.
⚠ C301: "Image Slot" is 0×120.
fill inside a hug parent collapses to 0. Use a fixed size, or give an ancestor a fixed width on that axis.The agent gives the card a real width so the child has something to fill:
al(v,g(8),pad(16)) s(280,hug) "Card"The frame fills to 280 and renders correctly.
What the loop looks like over a long run
Three exchanges isn't much, but extend it. A real design is many Blueprint lines, arriving in a few streamed batches. Somewhere in there the agent will hit a warning it hasn't seen before. It gets the diagnostic, the suggestion, and the preview. Next time that same shape comes up, the agent writes the correct version on the first try. In-context learning plays out inside the session.
A model fluent in code but new to Blueprint stumbles on the first unfamiliar shape, reads the diagnostic, and writes it correctly the next time it comes up. The compiler closes the training-data gap one diagnostic at a time, without a single line of it living in the model's weights.
The result
A grammar that's extendable, concise, and token-efficient. A compiler that teaches the model which parts of the grammar it hasn't learned yet. A visual feedback loop that collapses the "did my code do what I thought" gap from "rebuild, deploy, reload" to "here's a PNG of what you just made".
Costs came down and design accuracy went up, for the same reason in both cases: the agent spends its tokens on decisions instead of field names, and it stops repeating mistakes the moment the compiler names them. Sessions that used to drift off-task now stay on-task, because every turn ends with concrete feedback instead of silence.
The bigger picture
Everything so far has been creation. New elements, new properties, new layouts. That's half of what a design tool does. The other half is manipulation: moving elements between containers, stretching them, duplicating them, retheming them, restructuring the tree. If the agent can't express those operations densely, all the token efficiency we worked for evaporates at edit time.
Blueprint has a compact vocabulary for manipulation, too.
Reparent in one line: #title parent(#new_card). The element keeps its properties, its overrides, and its ref. Only its place in the tree changes.
Clone for a standalone duplicate: clone(#card) p(320,0) "Card 2". Instance for a linked copy that follows its master: mark a source element as a component, then inst(#card) p(320,0) #pro. Instances can override specific children without breaking the link:
inst(#card) p(320,0) #pro
override(#title) t("Pro Plan")
override(#desc) t("For growing teams")Edit #card later and every instance updates, except the pieces the instance overrode. When the variation is discrete rather than free-form, a component set declares its axes up front: comp "Toggle" axes[state[on,off]], one variant frame per state, then inst(#toggle) at(state(on)) to place a configured copy.
Scale an element and everything inside it proportionally with #hero scaleTo(w,1200). Recolor every fill and stroke under a parent without touching the parent itself with #card cb(#0F172A). Nudge properties relatively with #hero p+(0,40) rd+(4) o+(-0.1), useful when the agent is reasoning in "a bit more" terms rather than absolutes.
And the everyday structural edits each land in one line: delete(#old_row), #hero front, #divider back(2), ungroup(#button_group). Nothing that couldn't be another API call, but the grammar makes them feel native.
There's one more audience Blueprint has to serve: a design system. When one is active, every color, size, and font slot is a $token that resolves through the current brand and mode rather than a bare value.
al(v,g($spacing.md),pad($spacing.lg)) s(280,hug) f[($color.surface)] rd($radius.lg) "Card"
t("Real-time sync",$font.family,$font.size.lg,b) f[($color.text.primary)]$color.surface and $color.text.primary aren't fixed colors; they're roles. Flip a subtree to dark with ds(, theme(dark)) and every token re-resolves: the surface goes dark, the ink goes light, and nothing in the layout changes. Palette stops run a presence ramp from $primary.hint to $primary.intense (loudness against the surface, not brightness), and spacing, radius, and font size follow a t-shirt scale from xs up. Change the brand and every reference in the file tracks the new values. This is what replaced the old ad-hoc seed variables: tokens that bind to a real design system instead of hard-coded hex.
Each of these maps through the same compiler: parse and validate against the current canvas, execute, lint. No special APIs, no order-of-operations dance between related properties, no stale references. The agent says "reparent", "clone", "scale", "override", and the canvas updates in one atomic step.
That's the point of the language. Not just creating elements, but restructuring and retheming them, at the same density the language was designed for.