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." } }
]
}
}
| Form | Shape | Meaning |
|---|---|---|
| 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:
$repomarks a repository component — one of your app’s own. It records{ importPath, exportName, member?, app?, proxy? }while$refstays the JSX name, so the canvas mounts the app’s real component andemit_codeprints its exact import. Because identity lives on the node, Mantine’sButtonand the provider’sButtoncan coexist in one folder without either shadowing the other.$emitAsis a host-component facade used by the scan/port flow: the canvas renders the node’s Velloo subtree, butemit_codeemits the named component imported from your app instead. Prefer$repofor a component that can simply render;$emitAsis 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,navare 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
IdConflicterror carrying the id and every path it appears at. - Assignment: add a
vellooIdattribute to a tag when building withcompose(<Button vellooId="hero-cta">Start</Button>stores$id: "hero-cta"), or name an existing node later withset_node_id(which also renames, and clears withid: 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. @idreference: the string"@hero-cta"resolves to whichever node carries$id: "hero-cta". Stable across insertions, deletions, and reorders. An unresolvable id returns a typedIdNotFounderror 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.