Velloo documentation
Markdown for LLMs ↗MCP: lifecycle
Create, update, and remove screens, boards, frames, flow arrows, board notes, annotations, and extensions.
Lifecycle operations manage the resources a design folder is made of: screens, boards, frames, flow arrows, notes, annotations, and extensions. On the default guided surface, invoke an operation through call_velloo after checking its operation_schema; on the full surface the native names below are advertised directly. Every operation’s arguments are a strict object on either surface: an argument name the schema doesn’t declare is rejected with the valid keys rather than silently dropped.
Screens
add_screen
Create a new screen. Empty by default. Does not place the screen on a board; that’s a separate, intentional add_frame step.
| Arg | Type | Required | Description |
|---|---|---|---|
name |
string | yes | |
id |
string | no | Omit to auto-suffix a unique id derived from the name |
fromScreenId |
string | no | Clone an existing screen’s tree |
tree |
node | no | Supply a tree explicitly |
Returns { screenId, screen }, where screen carries the metadata and a nodeCount instead of the tree; read the tree with get_screen when you need it.
A route-scan init already scaffolds one placeholder screen per detected route (id = route slug). Don’t add_screen for those; it returns ScreenIdConflict with a hint. Rebuild the existing screen instead with compose mode "replace".
add_screen is not the only way to make a screen: a compose in mode "replace" on a screenId the folder doesn’t have creates the screen and places it on a board in one call.
update_screen
Sparse metadata patch of the screen’s name and the app route it stands for; the screen id stays stable. The fields can also be sent beside the screen, { screenId, name }, and are read as the patch.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId |
string | yes | |
patch |
object | yes | { name?, route? }. route is the app path, such as "/settings"; null clears it. |
Returns { screen } with the screen’s metadata and a nodeCount, not its tree.
remove_screen
Delete a screen from disk. Refuses to remove the last screen (LastScreen). Any frames referencing the screen on any board are removed alongside it; the result is { removedScreenId, removedFrames: [{ boardId, frameIds[] }] }. Undoable from the canvas.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId |
string | yes |
Boards
Boards are the canvases of a design folder; one folder has many. Each board owns its own frames and persists as boards/<id>.json. A folder with zero boards is a supported state.
add_board
Create a new empty board. Returns { boardId, board }.
| Arg | Type | Required | Description |
|---|---|---|---|
name |
string | yes | Max 80 characters |
id |
string | no | Derived from name if omitted (auto-suffixed on collision); an explicit id that already exists returns BoardIdConflict |
group |
string | no | Sidebar group to file the board under: an existing group’s id or name (matched case-insensitively), or a new name, which creates the group |
update_board
Sparse patch on a board’s metadata. Returns { board }.
| Arg | Type | Required | Description |
|---|---|---|---|
boardId |
string | yes | |
patch |
object | yes | { name?, theme?, archived?, group? }, see below |
Patch fields:
name: max 80 characters.themenames a theme (the stem oftheme/<name>.json) the board’s frames render with, the per-board look;nullclears back to the folder default.archived, whentrue, files the board away: hidden from the sidebar, fromlist_boards(unlessincludeArchived), and from a default publish, but kept and still editable.falseunarchives it.groupmoves the board to a sidebar group by existing id or name, or a new name (which creates one);nullfiles it back under Ungrouped.
{ "boardId": "explorations", "patch": { "archived": true } }
remove_board
Delete a board and its notes. There is no last-board rule; removing the only board is allowed.
The delete cascades. Screens that no other board places are deleted with the board; screens also framed on another board are kept. Snippets that only those deleted screens reached are deleted too; a snippet that was already unreferenced before the call is left alone. The result reports all of it: { removedBoardId, removedScreenIds, removedSnippetIds }.
This is permanent. To put a board away reversibly, use update_board with patch: { archived: true } instead.
| Arg | Type | Required | Description |
|---|---|---|---|
boardId |
string | yes |
reorder_boards
Set the left-sidebar display order of boards. Persists to config.boardOrder; the canvas drag-and-drop calls this too. Returns the persisted { order }.
| Arg | Type | Required | Description |
|---|---|---|---|
order |
string[] | yes | Board ids in the desired order. Unknown ids are ignored; omitted boards keep their current slots |
Frames
Frames are placements of screens on a chosen board. Multiple frames of the same screen always render the same tree at different sizes; that is the sync model. Different layouts per breakpoint are separate screens.
A frame’s group names a frame region on its board: an existing group’s id or name (matched case-insensitively), or a new name, which creates the group. Frames naming one new group in one write share it. Agents set or clear a frame’s group through add_frame and update_frame; an empty or over-long name is refused before anything is written. (This is unrelated to a board’s sidebar group, set via add_board / update_board.)
add_frame
Place a screen on a specific board at a chosen size and position. Returns { frame }.
| Arg | Type | Required | Description |
|---|---|---|---|
boardId |
string | yes | |
screenId |
string | yes | |
x |
number | no | Used only when y is also given; otherwise the frame is placed 80px right of the rightmost existing frame, at y: 0 |
y |
number | no | Paired with x |
w |
integer | no | Width in px; defaults to the folder’s Desktop viewport (else its first preset) |
h |
integer | no | Height in px. Omitted, the frame is sized to the screen and follows it |
label |
string | no | |
group |
string | no | Frame region on the same board: an existing group’s id or name, or a new name, which creates it |
id |
string | no | Defaults to <screenId>-<n>; an existing id returns FrameIdConflict |
Frames that follow their screen
A frame added without h carries fit: "content": its height follows the screen. Velloo measures the screen shortly after it changes, whoever changed it, an agent or a person on the canvas, and resizes the frames that follow it. A burst of edits is one measurement. add_frame waits for the first measurement, so the frame it returns has the fitted height. A fitted frame is never shorter than the viewport preset of the same width, and measuring needs the screenshot browser (velloo browser install).
Setting h to a new value with update_frame ends it, because a height someone chose stays theirs. Moving a frame does not. fit: "content" in a patch starts it again, and fit: null clears it. On the canvas this is Fit height to content in the frame’s menu, shown as a badge beside the height.
screenshot and compare_to_url stay reads. They never resize a frame, and framesShorterThanContent leaves out the frames that follow their screen.
update_frame
Move, resize, relabel, regroup, or pin the color scheme of frames on one board. One entry per frame in patches, applied in one atomic write (single persist, broadcast, and undo entry); length 1 for a single frame. Returns { frames }, the board’s full frame list. An unknown frame id fails the whole call with FrameNotFound.
| Arg | Type | Required | Description |
|---|---|---|---|
boardId |
string | no | Omit when exactly one board holds these frames |
patches |
array | yes | [{ frameId, patch }], at least one entry |
Each patch is { x?, y?, w?, h?, label?, group?, scheme?, fit? }. label, group, scheme, and fit accept null to clear; an omitted field is unchanged. scheme is "light" or "dark" and pins that frame’s render mode, a review affordance over the screen’s one shared tree, not a separate design variant. screenshot follows the pin when you omit mode. fit is "content": the frame’s height follows its screen.
{
"boardId": "main",
"patches": [
{ "frameId": "pricing-1", "patch": { "h": 2400 } },
{ "frameId": "pricing-2", "patch": { "scheme": "dark", "label": "Pricing · dark" } }
]
}
remove_frame
Remove a frame placement from a board. The underlying screen is untouched. Arrows that touched the frame go with it.
| Arg | Type | Required | Description |
|---|---|---|---|
boardId |
string | yes | |
frameId |
string | yes |
On the guided façade, remove_frame { frameId } without a boardId is read on the one board that has a frame of that id.
Flow arrows
Arrows between a board’s frames make the board a flow. Read one back with get_board, whose flow field groups the frames into steps.
connect_frames
Draw, edit, and erase arrows in one write: one persist, one broadcast, one undo entry.
| Arg | Type | Required | Description |
|---|---|---|---|
boardId |
string | no | Omit when exactly one board holds these frames |
edges |
array | yes | [{ from, to, label?, state?, remove? }], at least one entry |
Each entry:
| Field | Type | Description |
|---|---|---|
from |
string | Frame id the arrow leaves |
to |
string | Frame id the arrow points at |
label |
string or null | The trigger or condition in plain words (“Submit”, “Email already taken”), up to 120 characters. null clears |
state |
string or null | Makes to a state of from’s step: loading, empty, error, success, or any short name up to 32 characters, stored lower-case. Omit it for a transition to another step. null clears |
remove |
boolean | Erase the arrow between this pair |
A board holds one arrow per ordered pair of frames, so from and to name it. An entry for a pair that already has an arrow edits it, and an omitted field is unchanged. Entries apply in order and the result is checked as a whole, so one call can erase one arrow and draw another.
{
"edges": [
{ "from": "signup-1", "to": "signup-sending-1", "state": "loading", "label": "Submit" },
{ "from": "signup-sending-1", "to": "signup-taken-1", "state": "error", "label": "Email already taken" },
{ "from": "signup-sending-1", "to": "verify-1", "label": "Account created" },
{ "from": "signup-taken-1", "to": "signup-1", "label": "Try again" }
]
}
Returns { edges, readAsTransitions?, gaps?, unconnected? }. edges is every arrow on the board after the write. gaps and unconnected are what the drawing still leaves open, in the same shape get_board’s flow reports them.
A few guesses are read as meant:
- A state that can only be a transition. A way back to the step’s default frame or to the state it came from, a state its target already has, or
defaultis kept as the transition it is and listed inreadAsTransitions: [{ from, to, state }]. - An end that names a screen.
fromortomay be a screen id where exactly one frame shows that screen. - One arrow sent flat. On the guided façade,
{ from, to, label?, state? }with noedgesaround it is one arrow.
A real contradiction is refused with BadRequest and nothing is written: a second state for a frame that is already one, a loop of states, a frame pointing at itself, or remove for a pair with no arrow. An end that is not a frame on the board returns FrameNotFound.
Arrows are a spec for review and implementation. Nothing runs on the canvas, and emit_code emits nothing for them.
Canvas notes
Board-level markdown notes. A note is either free-floating in board coordinates (the same space as frame x/y) or attached to a node inside a frame, in which case the canvas shows it as a marker on that node that opens the note on hover. A free note fits its text unless someone resized it on the canvas, which records an optional height; the text then scrolls inside it. Use them for guidance that belongs next to frames: tour steps, review remarks, handoff context. list_notes is on the discovery page.
add_note
Returns { note }.
| Arg | Type | Required | Description |
|---|---|---|---|
boardId |
string | no | Default: the first board |
x |
number | no | Board coordinates; place beside frames, not on them |
y |
number | no | |
width |
number | no | Pixel width; default 240 |
body |
string | yes | Markdown-lite |
attachment |
object | no | { frameId, screenId, locator }: anchor the note to a node instead of free board space |
A note that names no place goes to the left of the board’s frames, below any note already standing there. A design with no board yet has nowhere to put one, and the call returns BadRequest saying so. For an attachment, frameId must be a frame on this board, that frame must show screenId (otherwise InvalidPath), and locator (@id or a path array) must resolve in that screen.
{
"boardId": "main",
"body": "Swap this for the real pricing table before handoff.",
"attachment": { "frameId": "pricing-1", "screenId": "pricing", "locator": "@plans" }
}
update_note
Returns { note }; an unknown noteId returns CanvasNoteNotFound.
| Arg | Type | Required | Description |
|---|---|---|---|
boardId |
string | yes | |
noteId |
string | yes | |
patch |
object | yes | { x?, y?, width?, body? } |
remove_note
Returns { removedId }.
| Arg | Type | Required | Description |
|---|---|---|---|
boardId |
string | yes | |
noteId |
string | yes |
Annotations
Annotations are markdown pinned to a specific node; the canvas draws a connector to that node in every frame showing it. Authorship matters: designer-authored annotations are the designer’s channel to the agent: read them via list_annotations, act on them, never edit or delete them. Agent-authored annotations (questions for the designer, review remarks) are created, edited, and removed with the three operations below.
add_annotation
Pin agent-authored markdown to a node. Created with author: "agent" and position: "auto". Returns { annotation }. A node holds one annotation; pinning a second to the same node returns AnnotationConflict with the existingId.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId |
string | yes | |
path |
locator | yes | @id or path array of the target node |
body |
string | yes | Markdown |
collapsed |
boolean | no |
update_annotation
Edit one of your own annotations. Pass body, collapsed, or both; neither is a BadRequest. Refuses user-authored annotations; answer those with your own annotation instead. Returns { annotation }.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId |
string | yes | |
annotationId |
string | yes | |
body |
string | no | Replacement markdown |
collapsed |
boolean or null | no | null clears the override |
remove_annotation
Remove an agent-authored annotation. Refuses user-authored ones. Returns { removedId }; an unknown id returns AnnotationNotFound.
| Arg | Type | Required | Description |
|---|---|---|---|
screenId |
string | yes | |
annotationId |
string | yes |
Extensions
Extensions declare user-owned custom components (a bespoke DataTable, a brand Hero, a real chart) that aren’t part of any library’s registry. The canvas renders them as labelled placeholders (or client-mounts the real component with render: "live"); emit_code emits a real import from importPath. Concepts: Extensions.
Each prop entry is { name, type, optional, control, defaultValue?, enumValues? } where optional is a boolean, defaultValue a string, and control one of boolean, number, string, color, enum, icon. control: "enum" requires a non-empty enumValues. A malformed entry returns InvalidExtensionProp naming the prop.
add_extension
Register a custom component so it can appear in screen trees. Returns { id, extension }, plus shadowedLibraryComponent when the id matches a library component and liveResolveWarning when a render: "live" import path can’t be resolved from the host app (the canvas shows the placeholder until it’s fixed). An id that is already registered returns ExtensionIdConflict.
| Arg | Type | Required | Description |
|---|---|---|---|
id |
string | yes | Component id; shadows a library component with the same id |
importPath |
string | yes | Emitted verbatim by codegen; use the alias your app actually uses (@/components/data-table, @acme/charts) |
props |
array | yes | Prop schema (shape above) |
category |
string | no | "ui" (default) or "typography" |
description |
string | no | |
render |
string | no | "static" (default, placeholder card) or "live": bundle the real component from the host app and client-mount it in the canvas; falls back to the placeholder on bundle/render failure |
app |
string | no | Monorepos only: which host app the component lives in, a key of config.hostApps (e.g. "web", "admin"). The live island bundles from that app’s root and node_modules. Omit for the default host app; an unknown key is a BadRequest listing the known ones |
fit |
string | no | Live-island sizing: "aspect-video" (default, locks a 16:9 box) or "content" (a fixed-height chart or an absolute-inset overlay drives its own height) |
{
"id": "RevenueChart",
"importPath": "@/components/revenue-chart",
"props": [
{ "name": "range", "type": "\"7d\" | \"30d\"", "optional": true, "control": "enum", "enumValues": ["7d", "30d"] }
],
"render": "live",
"fit": "content"
}
Don’t use extensions for compositions of existing components (use snippets) or for variations of an existing component (use props or class overrides).
update_extension
Patch an existing extension: add or remove props, change the importPath after a refactor, switch render or fit, move it to another host app. Sparse: unlisted fields keep their values. To rename, remove_extension then add_extension; renaming would break tree references. Returns { id, extension } (plus liveResolveWarning when applicable); an unknown id returns ExtensionNotFound.
| Arg | Type | Required | Description |
|---|---|---|---|
id |
string | yes | |
patch |
object | yes | { importPath?, props?, category?, description?, render?, app?, fit? }, same value rules as add_extension; props replaces the whole prop list |
remove_extension
Remove an extension. Refuses with ExtensionInUse while any screen or snippet tree still references it; the error carries the offending references: [{ screenId, path }] so the agent can remove or replace them first. Returns { id }.
| Arg | Type | Required | Description |
|---|---|---|---|
id |
string | yes |
Folder format upgrade
upgrade_design_folder
Not part of the normal catalogue. When velloo mcp (stdio) starts against a design folder whose on-disk format is older than the installed velloo, the design tools can’t load. Instead of exiting, the server serves a minimal session: its instructions explain the mismatch, and upgrade_design_folder is the only tool. It takes no arguments and runs the same in-place migration as velloo upgrade <design>, returning the applied steps and rewritten files. Reconnect the MCP server afterwards to load the design tools.
A folder written by a newer velloo can’t be migrated backwards, so that session carries only the notice: update the velloo CLI, then reconnect.