Skip to content

Velloo documentation

Markdown for LLMs ↗

Screens and nodes

The node forms a screen tree is made of (component refs, snippet instances, params), plus stable ids and path addressing.

A screen is one file, one tree. Every position in that tree is a node, and a node is exactly one of three shapes, distinguished by which $-prefixed key it carries.

The three node forms

{
  "id": "landing",
  "name": "Landing",
  "tree": {
    "$ref": "Card",
    "props": { "className": "p-6 md:p-12 lg:max-w-4xl mx-auto" },
    "children": [
      { "$ref": "Heading", "props": { "level": 1, "children": "Welcome" } },
      { "$ref": "Text", "props": { "children": "Get started in seconds." } },
      { "$snippet": "feature-card", "args": { "title": "Fast", "body": "Snappy by default." } }
    ]
  }
}
FormShapeMeaning
Component{ "$ref": ComponentId, "$id"?, "$repo"?, "props"?, "children"? }A real component from the folder’s library, an extension, or — when it carries $repo — one of your app’s own
Snippet instance{ "$snippet": SnippetId, "$id"?, "args"?, "$extraClassName"?, "$overrides"? }An instantiation of a reusable subtree defined in snippets/
Param ref{ "$param": ParamName }A placeholder, valid only inside a snippet body, substituted at render time

Props are JSON literals. There are no fixtures, no data files, no expressions: the text on a button is a string in props, inline. Designs are static by design: click handlers, routing, and form state are all no-ops on the canvas.

Agents rarely write this JSON directly. They build trees with compose, which takes a restricted JSX string and compiles it into exactly these node forms; nothing executes. Tags resolve against the screen’s library, its extensions, and the folder’s snippets (by PascalCase name); quoted attributes become string props, {…} attributes must be JSON literals, and vellooId becomes $id:

{
  "screenId": "landing",
  "mode": "append",
  "parentPath": [],
  "jsx": "<Card className=\"p-6\" vellooId=\"welcome-card\"><Heading level={1}>Welcome</Heading><FeatureCard title=\"Fast\" body=\"Snappy by default.\" /></Card>"
}

mode: "append" inserts the subtree under parentPath (at index, or last); mode: "replace" swaps in a whole new screen tree. Later edits address nodes by path: update_props takes patches, each with a path plus a propPatch, a style payload in the screen’s native style channel (a className string for Tailwind, an object for sx / style), or both.

A component node may also carry one of two identity fields:

  • $repo marks a repository component — one of your app’s own. It records { importPath, exportName, member?, app?, proxy? } while $ref stays the JSX name, so the canvas mounts the app’s real component and emit_code prints its exact import. Because identity lives on the node, Mantine’s Button and the provider’s Button can coexist in one folder without either shadowing the other.
  • $emitAs is a host-component facade used by the scan/port flow: the canvas renders the node’s Velloo subtree, but emit_code emits the named component imported from your app instead. Prefer $repo for a component that can simply render; $emitAs is for a deliberately approximate preview. See Port a page.

You’ll rarely write either by hand.

Stable ids

Component and snippet-instance nodes may carry an optional $id, a stable anchor that survives sibling insertions and deletions.

  • Format: must match /^[a-zA-Z][a-zA-Z0-9_-]*$/, at most 64 characters. hero-cta, feature_card_3, nav are all fine; a leading digit is not (that would collide with path indices).
  • Uniqueness: ids are unique within a screen tree, validated at persist time. A collision surfaces as a typed IdConflict error carrying the id and every path it appears at.
  • Assignment: add a vellooId attribute to a tag when building with compose (<Button vellooId="hero-cta">Start</Button> stores $id: "hero-cta"), or name an existing node later with set_node_id (which also renames, and clears with id: null).

Param refs can’t carry ids; only components and snippet instances can.

Path addressing

Every path-accepting tool (update_props patch entries, move_node, remove_node, set_node_id, update_snippet_instance, inspect, compose’s parentPath, …) takes a locator in either of two forms:

  • Index array: integer indices from the tree root. [0, 2, 1] means: root’s first child, its third child, its second child. Cheap to serialize and unambiguous, but brittle. Insert a sibling above the target and every later index shifts; the path you computed two calls ago now points at the wrong node.
  • @id reference: the string "@hero-cta" resolves to whichever node carries $id: "hero-cta". Stable across insertions, deletions, and reorders. An unresolvable id returns a typed IdNotFound error rather than silently hitting a neighbor.

The practical rule: use index paths for one-shot edits on a tree you just read; assign an $id to anything you’ll reference more than once. Edits return the resolved path of the affected node, so chained operations don’t need a re-read. The empty array [] addresses the root itself. Tool-by-tool details are in Path addressing.

Snippet instances are opaque

A $snippet node has a path and may carry its own $id, but the structure rendered inside it is not addressable from the screen. Path navigation stops at the instance: from the screen’s point of view you can change its args, its $extraClassName, or its per-instance $overrides, all through update_snippet_instance, but you cannot descend into the body with a screen path.

That’s deliberate. The body belongs to the snippet definition, and editing it means editing every instance, so body edits go through the snippet’s own addressing space (the snippet:<id> virtual screen), not through any one instance. The full story is in Snippets.

Errors are typed

Mutations don’t throw strings at you. A bad componentRef returns UnknownComponent with Levenshtein-ranked suggestions; a stale path returns InvalidPath; a duplicate anchor returns IdConflict with the offending paths. compose reports JSX problems as a BadRequest whose issues carry a line and column, and an unknown tag comes with “did you mean” suggestions. Agents are expected to read the kind field and self-correct. The full catalogue is in the MCP reference.

Successful writes can carry advice too. When a change lands, the result may include diagnostics (invalid or undefined Tailwind classes, raw colors that won’t flip in dark mode, components that threw while rendering) and propWarnings for mistyped prop names or enum values outside a prop’s allowed set. Neither undoes the write; they tell the agent what to fix next. See Advisory prop warnings.

Next

  • The reusable form of a subtree: Snippets.
  • How nodes get styled per framework: Frameworks.