Velloo documentation
Markdown for LLMs ↗MCP: tree mutations
compose, update_props, move_node, remove_node, set_node_id, inspect, and update_viewport_presets, plus the diagnostics every tree write returns.
A screen has one tree. compose builds it from restricted JSX; the other operations on this page target nodes within it via locators: @id references or index paths. Except for compose, they also work on snippet bodies via the virtual screenId form snippet:<id> (see Editing a snippet body).
Successful writes can carry advisory propWarnings and diagnostics. For a guided workflow, see Design a screen.
compose
Append one subtree to a screen, or replace the screen’s whole tree, from restricted JSX. Tags resolve against the screen’s library, the folder’s extensions, and snippet names, so a whole feature card, or a whole page, lands in one call. No JavaScript executes.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId | string | yes | A screen id. snippet:<id> is not accepted |
mode | string | yes | "append" inserts the JSX root under parentPath; "replace" makes it the screen’s entire tree |
jsx | string | yes | Restricted JSX with a single root element |
parentPath | locator | no | Append only. Parent to insert under; default [], the screen root. Must be a component node |
index | number | no | Append only. Insertion index in the parent’s children; default: after the last child |
{
"screenId": "landing",
"mode": "append",
"parentPath": "@hero",
"jsx": "<Box vellooId=\"hero-actions\" className=\"mt-6 flex gap-3\"><Button vellooId=\"hero-cta\">Get started</Button><Button variant=\"outline\">Read the docs</Button></Box>"
}
Passing parentPath or index with mode: "replace" returns BadRequest. Rebuild a scanned placeholder screen with mode: "replace".
Restricted JSX
The compiler accepts a static subset of JSX and turns it into design nodes:
- One root. The source must contain exactly one root element. A fragment (
<>…</>) is allowed only when it wraps exactly one element. - Tags are component ids from
list_components:Button,Card,CardHeader, an extension id, or a snippet’s PascalCase tag. A dotted tag is a tag too, but only where the catalog has that id — a repository component’s compound part (<Tabs.List>) or a qualified name (<Mantine.Button>). Library ids stay flat, so shadcn’s is<CardHeader>, not<Card.Header>. An unknown tag is rejected with close-match suggestions; a tag that is both a component and a snippet is rejected as ambiguous. - Attribute values are quoted strings (
className="p-6", single or double quotes), JSON literals in braces (size={24},disabled={false},items={["A", "B"]},sx={{"p": 2}}), or an element (leftSection={<Icon name="Plus" />}), stored as a node-valued prop and rendered as the real element. A bare attribute (disabled) meanstrue. Identifiers, calls, template strings, functions, spread attributes, and child expressions such as{title}are rejected — passing a bare component type (icon={Plus}) is rejected with a message telling you to pass an element instead. - No behavior. Event handlers (
onClickand any otheron*attribute),ref,key, anddangerouslySetInnerHTMLare rejected. Duplicate attributes are rejected. - Text children become the component’s
childrenprop, with whitespace collapsed:<Button>Save</Button>. Settingchildrenboth as text and as an attribute is an error. When text sits beside elements (<Button><Icon name="Plus" />Add</Button>), each text run is wrapped in aTextnode automatically. vellooId="hero-cta"sets the node’s stable$id, addressable afterwards as"@hero-cta". It must be a string, start with a letter, and be unique on the screen. Assign one to anything you may touch again.
Snippets in JSX
A snippet is placed by its PascalCase tag, derived from the snippet’s id or name, as list_components reports it (pricing-tier becomes <PricingTier />):
- Each attribute is an argument and must name a declared param; an undeclared name is rejected.
className, unless the snippet declares aclassNameparam, becomes the instance’s extra class suffix, appended to the body root.- Text children, or exactly one element child for a
node-typed param, fill a param namedchildren; a snippet without achildrenparam rejects children. - Every required param (no default and not optional) must be present.
vellooIdsets the instance’s$id.
{
"screenId": "pricing",
"mode": "append",
"parentPath": "@tiers",
"jsx": "<PricingTier vellooId=\"tier-pro\" name=\"Pro\" price={19} featured className=\"md:scale-105\" />"
}
Per-instance interior overrides are not set at placement; apply them afterwards with update_snippet_instance.
Result and errors
An append returns { mode, path, root }, where path is the new node’s index path; a replace returns { mode, screenId, replacedRootRef, root }. root is { kind: "component" | "snippet", id }. Either can carry propWarnings and diagnostics.
A JSX problem returns BadRequest with issues: [{ message, offset, line, column }] pointing into the source. A host-app package that isn’t installed never blocks design; emit_code reports it later in componentsToInstall. Other errors: ScreenNotFound, InvalidPath, IdNotFound, IdConflict.
update_props
Patch props and native styling on one or more nodes in a single atomic write: one lock, one broadcast, one undo step.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId | string | yes | Screen id, or snippet:<id> for a snippet body |
patches | array | yes | One entry per node: { path, propPatch?, style? }. Length 1 for a single edit. Each entry needs propPatch, style, or both |
gesture | string | no | Opaque id (max 64 characters) of a continuous drag; writes sharing it collapse into one undo step |
Within an entry:
-
propPatchshallow-merges into the node’s props.nullremoves a key;classNameis a prop like any other. -
stylerestyles through the screen’s framework-native style channel, applied afterpropPatch:- shadcn and Tailwind no-framework folders: a Tailwind
classNamestring, which replaces the node’sclassName(an empty string removes it) - MUI: an
sxobject - no-framework, inline-style channel: a plain
styleobject, themed viavar(--...)tokens
Object channels merge shallowly and an inner
nulldrops that key;style: nullclears the channel’s prop entirely. A payload whose shape doesn’t fit the channel returnsBadRequestnaming the expected shape; this is how an agent discovers the active channel. All style payloads are validated before anything is written. - shadcn and Tailwind no-framework folders: a Tailwind
{
"screenId": "landing",
"patches": [
{ "path": "@hero-cta", "propPatch": { "children": "Start free", "size": "lg" } },
{ "path": "@hero", "style": "flex flex-col items-center gap-6 py-24" }
]
}
The top-level arguments are strict: a top-level path, propPatch, style, or props is rejected; wrap the edit in patches. A single entry object sent bare as patches is lifted into a one-element array.
Returns { paths }, the resolved index path of each entry in order. update_props patches component nodes only; targeting a snippet instance returns InvalidPath pointing at update_snippet_instance.
Patching inside a snippet instance
To change one node inside a single instance’s body, or an instance’s arguments or extra classes, use update_snippet_instance. To change a body node across every instance, use update_snippet with innerPatch.
| Operation | Scope |
|---|---|
update_props | A component node on a screen, or in a snippet body via snippet:<id> |
update_snippet_instance | One instance: its args, extra classes, or one node inside its body |
update_snippet with innerPatch | The shared definition: every instance at once |
move_node
Move a node to a new parent.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId | string | yes | Screen id, or snippet:<id> |
fromPath | locator | yes | Node to move |
toParent | locator | yes | Destination parent |
toIndex | number | no | Insertion index in the destination’s children; default: last |
Returns { newPath }. Errors: InvalidMove when moving the root, or moving a node into itself or one of its descendants; InvalidPath for a missing node or parent.
remove_node
Remove the node at a locator.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId | string | yes | Screen id, or snippet:<id> |
path | locator | yes |
Returns { removedRef } naming what was removed. The root itself is never removed: passing the root path [] clears all of the root’s children and leaves the root in place. To rebuild a whole screen, prefer compose with mode: "replace".
set_node_id
Set, rename, or clear a node’s stable $id anchor. Once set, the node is addressable as "@<id>" in any locator.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId | string | yes | Screen id, or snippet:<id> |
path | locator | yes | |
id | string or null | yes | A leading letter, then letters, digits, _, or -; null clears |
Returns { path, id }. Per-screen uniqueness is enforced; collisions return IdConflict. Targets component nodes and snippet instances; param refs can’t carry ids.
inspect
Return the server-rendered HTML, resolved className list, $ref, and resolved props for a node; use it instead of guessing rendered output.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId | string | yes | |
path | locator | yes | |
innerPath | string | no | For a snippet instance: "@id", a dotted index path, or "" for the body root (the default). Ignored for plain components |
computed | boolean | no | Also render the screen in a real browser and measure the node. Default false; it costs a render |
viewport | object | no | Render size for computed, { w, h } ({ width, height } is accepted). Default: the folder’s Desktop preset |
mode | string | no | Render variant for computed: "light", "dark", or "compare" |
theme | string | no | Named theme for computed. Default: the theme the screen’s board pins |
Returns { ref, resolvedProps, classes, bodyHtml, note? }. The HTML comes from Velloo’s bundled library; where the screen mounts the app’s own components (see component_status), the canvas and captures show those instead, so measure them with computed: true.
When path resolves to a snippet instance, the body is rendered with its args, $overrides, and $extraClassName applied; innerPath drills into one body node. Omitting it on an instance inspects the body root and returns a note on how to drill in.
With computed: true the result adds computed: { viewport, path, tag, class?, rect: { x, y, w, h }, style, children } (the node’s rendered box and resolved computed styles, plus its direct children’s boxes) and any mount diagnostics. computed is null with a computedNote when the node renders nothing measurable, or when innerPath points inside a snippet body (body nodes aren’t separately addressable in the render; measure the instance or use render_snippet).
update_viewport_presets
Replace the folder’s viewport presets: the sizes offered when adding a frame, and the source of the default screenshot viewport (the preset whose name contains “desktop”, else the first). Send the complete list in display order; names must be distinct and at least one is required. Frames store their own w/h, so this never resizes an existing frame.
| Arg | Type | Required | Description |
|---|---|---|---|
presets | array | yes | The complete list in display order: [{ name, w, h }], with positive integer w/h |
{
"presets": [
{ "name": "Desktop", "w": 1440, "h": 900 },
{ "name": "Mobile", "w": 390, "h": 844 }
]
}
Returns { presets }. At least one preset is required and names must be distinct (case-insensitive); violations return BadRequest. Frames store their own w/h, so this never resizes an existing frame.
Diagnostics on results
There is no separate audit or class-validation operation. Design checks run automatically and arrive as a diagnostics array on the result of the call that needs them, omitted entirely when there is nothing to fix. Each entry is { severity, code, path, message, suggestion? }, with path naming the affected node:
| Code | Severity | Meaning |
|---|---|---|
tailwind/invalid-class | warning | A class does not compile under the Tailwind JIT, which knows the folder’s theme tokens and custom CSS |
tailwind/undefined-var | warning | A class compiles but references a CSS variable the design system never declares, so it paints a fallback |
tailwind/v3 | warning | The host app is on Tailwind v3 and the class has v4 semantics; suggestion carries the v3 spelling when there is one |
theme/raw-color | warning | Color classes that won’t theme-flip in dark mode, with token suggestions. Listed for the first eight nodes, then summarized |
render/component-threw | error | The component threw while rendering and the canvas shows a placeholder; the message usually names the parent it must sit inside |
render/server-fallback | warning | On a screenshot, compare_to_url, or computed inspect: the capture renders Velloo’s bundled components rather than the app’s own, so don’t tune the design against it |
compose, update_props, update_snippet_instance, add_screen, add_snippet, update_snippet, and batch check what they wrote; screenshot and emit_code repeat the check over the complete screen, and emit_snippet over the snippet body. The class and color checks apply to Tailwind style channels; sx and inline-style folders get render diagnostics only.
Diagnostics never fail or roll back a write; treat them as a triage list, not a gate. A node that deliberately doesn’t flip (text over a photo scrim, a brand accent) is exempted from theme/raw-color with a truthy data-accent prop, such as data-accent="ok"; the exemption does not cascade to children.