Skip to content

Velloo documentation

Markdown for LLMs ↗

Design a screen

The full compose-and-verify loop. Discover the palette, build the tree in big strokes, check light and dark, then place frames on a board.

This is the core Velloo loop: discover, compose, verify, place. It’s written as a guide for a human directing an agent, but it’s equally readable by the agent itself, and the tool calls shown are the real MCP surface. If you’re wiring an agent for the first time, start at Connect an agent.

The examples assume a shadcn folder, where styling is Tailwind className strings. On a MUI folder the same loop applies, but the style channel of update_props carries sx objects instead of class strings; see Frameworks.

1. Discover

Before composing anything, learn what the folder already has. A handful of cheap calls:

{ "tool": "list_components", "args": {} }

The default index mode groups component families onto shelves (Actions, Forms & Inputs, Display, …) with their sub-pieces and usage notes, enough to plan with, without blowing the token budget. Then narrow: filter substring-matches tags, mode: "summary" adds prop names per component, and mode: "full" returns whole descriptors with a working example; pull those only for the components you’ll actually use.

If the result opens with Repo shelves, those are the app’s own components — prefer them over rebuilding the same thing from primitives, and run preview_status once to confirm they mount before you lean on them:

{ "tool": "preview_status", "args": {} }

A state of valid means a real component mounted in a headless browser. absent or failing means the canvas needs a preview entry first; the response’s next says exactly what to do, and set_preview_entry writes it.

{ "tool": "get_theme", "args": {} }

The active token tree: colors, typography (including the typeset), spacing, radius, plus the folder’s customCss. You’ll want this before picking any color class.

{ "tool": "list_components", "args": { "kind": "snippet", "mode": "summary" } }

Snippets share the component namespace. Each entry’s id is its PascalCase JSX tag and snippetId its persisted id. Reuse existing snippets before defining new ones: a folder that already has a feature-card snippet doesn’t need a second one under a different name.

{ "tool": "list_boards", "args": { "includeFrames": true } }

Boards host frames; frames point at screens. One call with includeFrames: true gives the whole layout picture. Archived boards are hidden unless you pass includeArchived: true.

For an existing screen, scan before you fetch:

{ "tool": "get_screen", "args": { "screenId": "landing", "mode": "outline" } }

Outline mode returns a stripped tree (the component ref or snippet, $id, a class snippet capped at 40 characters, children) so you can orient in a long screen without pulling the full JSON.

2. Compose

Create the screen. This does not place it on a board: placement is a separate, intentional step (step 4).

{ "tool": "add_screen", "args": { "name": "Landing", "id": "landing" } }

A new screen starts with a bare Card as its root.

Build in big strokes

compose takes restricted JSX (nested tags, literal text, quoted props, and JSON literals in braces; no JavaScript executes), so a whole hero section lands in one call instead of eight. mode: "replace" swaps the screen’s entire tree; mode: "append" adds one subtree under parentPath (default: the root) at an optional index.

{
  "tool": "compose",
  "args": {
    "screenId": "landing",
    "mode": "replace",
    "jsx": "<Box vellooId=\"hero\" className=\"flex flex-col items-center gap-6 bg-background px-6 py-24 text-center\"><Badge variant=\"secondary\">Now in beta</Badge><Heading level={1} className=\"max-w-3xl\">Ship the design you meant</Heading><Text className=\"max-w-xl text-lg text-muted-foreground\">Compose screens from your real components. Keep them local.</Text><Box vellooId=\"hero-actions\" className=\"flex items-center gap-3\"><Button vellooId=\"hero-cta\">Get started</Button><Button variant=\"outline\">Read the docs</Button></Box></Box>"
  }
}

Tags resolve against the screen’s library, the folder’s extensions, and snippet names, so a snippet goes in the same JSX as any component (<FeatureCard title="Fast" />), with its params as props and a className appended to its root. Compile errors come back with a line and column. Handlers (onClick and friends), ref, key, spreads, and {expressions} as children are rejected.

Two habits in that call worth naming:

  • Semantic tokens over raw palette. bg-background, text-muted-foreground, bg-primary, border-border all flip automatically under dark mode and survive a theme change. bg-zinc-900 and text-white render identically in both modes and go stale the day you re-theme. Reserve raw palette colors for accents that deliberately should not flip, and mark those nodes with data-accent="ok" so the theme diagnostic skips them (the exemption does not cascade to children).
  • @id anchors. vellooId="hero-cta" gives a node a stable $id. Every path-accepting tool takes "@hero-cta" in place of a positional path like [0, 3, 0], and unlike positional paths, ids survive sibling insertions. Assign one to anything you might touch again; set_node_id retrofits one later, and find_nodes locates nodes you didn’t name. See path addressing.

Iterate with bulk patches

update_props takes a patches array: many nodes, one atomic write, one undo entry. Each entry carries a path plus a propPatch, a style, or both:

{
  "tool": "update_props",
  "args": {
    "screenId": "landing",
    "patches": [
      { "path": "@hero-cta", "propPatch": { "size": "lg" } },
      { "path": "@hero", "style": "flex flex-col items-center gap-8 bg-background px-6 py-28 text-center" }
    ]
  }
}

Prop patches merge shallowly and null removes a key. style restyles through the screen’s native channel (a className string here, an sx object on MUI), and style: null clears it. Successful calls may carry advisory propWarnings: a typo’d prop name, an enum value outside the declared set. Treat a warning as “this will probably render wrong” and fix it in the same turn.

For multi-step sequences, batch runs { tool, args } entries in one atomic round-trip: on the first error every touched resource rolls back and the result reports rolledBack: true. It accepts add_screen, remove_screen, add_board, add_frame, remove_frame, update_frame, update_props, remove_node, move_node, set_node_id, add_snippet, update_snippet, remove_snippet, update_snippet_instance, and add_note; compose runs as its own call. For repeated structure (feature cards, pricing tiers, nav rows), define a snippet once with add_snippet and place it by tag in compose instead of copy-pasting subtrees.

3. Verify

Designs are static (no handlers, no routing), so verification is visual, backed by automatic diagnostics.

Tree-building writes (compose, update_props, add_screen, the snippet operations, and batch) return a diagnostics array when something needs fixing: invalid Tailwind utilities, undefined CSS variables, Tailwind v3 incompatibilities, and raw colors that won’t theme-flip, each naming the node path. They’re triage signals, not gates: the write already landed, and a deliberate brand accent may be correct. The exception is render/component-threw (severity error): the component at that path threw while rendering, so the canvas shows a placeholder there, and the message usually names the parent it needs.

{ "tool": "screenshot", "args": { "screenId": "landing", "mode": "compare", "scale": 0.5 } }

Compare mode renders light and dark side-by-side in one PNG (the fastest signal that the design actually adapts rather than merely existing) and reruns the diagnostics over the whole screen. With mode omitted, the render follows the hosting frame’s pinned scheme. Screenshots default to fullPage: true, so tall screens aren’t clipped, and render at the folder’s Desktop preset unless you pass viewport: { "w": 390, "h": 844 }. While iterating, diff: true compares against your previous capture: zero change costs no image at all; a small change returns a highlight crop naming the changed nodes.

When you need to know exactly what one node resolved to:

{ "tool": "inspect", "args": { "screenId": "landing", "path": "@hero-cta" } }

Returns the server-rendered HTML, the resolved class list, the $ref, and the resolved props: ground truth for “did that prop actually land.” That HTML comes from Velloo’s bundled library; where the screen mounts your app’s own components, add computed: true to measure the node’s real geometry and computed styles in a browser.

4. Place

A screen has one tree. Viewport size belongs to the frame, the placement of a screen on a board, not to the screen. So a desktop view and a mobile view of the same page are two frames pointing at one screen, and every edit syncs to both. Both placements fit in one batch:

{
  "tool": "batch",
  "args": {
    "calls": [
      { "tool": "add_frame", "args": { "boardId": "marketing", "screenId": "landing", "w": 1440, "h": 900, "label": "Desktop", "id": "landing-desktop" } },
      { "tool": "add_frame", "args": { "boardId": "marketing", "screenId": "landing", "w": 390, "h": 844, "label": "Mobile", "id": "landing-mobile" } }
    ]
  }
}

The tree renders responsively at each frame’s size; write your Tailwind breakpoints (md:, lg:) as you would in the app. When two viewports need genuinely different structure, that’s a separate screen with its own frames, not a variant.

Frames can also carry a group (the id of a frame region on that board, as listed under groups by list_boards { includeFrames: true }) and a light/dark scheme pin for review. Set either on add_frame or later with update_frame; there is no agent operation for creating the regions themselves, which are drawn in the canvas.

One nuance: a frame’s w/h is board layout, a fixed window, while a screenshot renders at its own viewport. The screenshot result’s contentHeight, and its framesShorterThanContent list (each entry naming the board, frame, frameHeight, and overflowBy for frames at the rendered width) tell you when content runs past a frame’s bottom edge. Fix it with update_frame:

{
  "tool": "update_frame",
  "args": {
    "boardId": "marketing",
    "patches": [{ "frameId": "landing-desktop", "patch": { "h": 1680 } }]
  }
}

Next