Commands
Every command that can change a document, with its input schema — generated from the command registry the editor, the assistant and the MCP endpoint all share.
Nothing mutates a document except a command. The editor UI, the built-in assistant and a third party's agent are three clients of one registry, which is what makes an edit made in one place undoable, attributable and diffable in the others.
Three properties hold for every command here:
- A batch is atomic. Commands are submitted together and applied as one unit: if any of them fails, none is applied. A rejected batch is safe to repair and retry.
- Every command is invertible. Applying and then inverting restores the previous tree — the property that makes undo and revert possible at all.
- A command validates against the vocabulary before it produces a change. A parameter no Block declares is refused rather than written and silently dropped by the serializer.
An agent submits these through the one mutation tool, vf_apply, which takes an
array of { name, input } objects and a note — one line, in the owner's
language, that becomes their history entry.
block.align
Put several Blocks on one edge, in one command, instead of computing each coordinate by hand. Aligns on "left", "right", "top", "bottom", "center-x" or "center-y"; the line to align to is the first node listed, or the canvas when "to" is "canvas". Writes absolute pixels — it is a calculator, not a layout engine. Refused, by name, on a Block whose box the edge needs and the document does not declare: a <vf-text> has no width, so its right edge and its horizontal centre are not numbers, and "left" or "top" is the alignment that exists for it.
| Parameter | Type | Accepts | What it does |
|---|---|---|---|
| nodeIds * | unknown[] | — | The Blocks to move. The first one is the line the others come to, unless `to` says otherwise. |
| edge * | enum | left | right | top | bottom | center-x | center-y | Which edge or centre line to share. |
| to | string | — | Align to this node id instead of the first listed, or to "canvas" for the canvas itself. |
{
"name": "block.align",
"input": {
"nodeIds": [
"text-1",
"text-2",
"text-3"
],
"edge": "left"
}
}{
"name": "block.align",
"input": {
"nodeIds": [
"shape-0"
],
"edge": "center-x",
"to": "canvas"
}
}block.group
Wrap Blocks in a <vf-group> so they move, rotate, scale and clip as one thing. The nodes must be siblings, and a contiguous run of them — grouping across a gap cannot preserve what draws between them. `frame` decides the group's box: the default computes the union of the children's boxes and needs every one of them to sit at a pixel coordinate with a pixel size, while "parent" spans the parent's whole box and so wraps anything at all — a percentage coordinate, a <vf-text> with no declared width — because nothing about the children has to be measured for it. Reach for "parent" to make a region addressable, and for a union or an explicit box when the group is going to carry a fill, a radius or a clip that needs to sit tight around it. Coordinates are rebased automatically and a percentage the frame would re-base is refused by name, so the picture is never changed.
| Parameter | Type | Accepts | What it does |
|---|---|---|---|
| nodeIds * | unknown[] | — | The nodes to wrap, as a contiguous run of siblings. Order in this list does not matter; paint order is kept. |
| name | string | — | A readable name for the new group, as node.setName would set. Worth passing every time: an unnamed group makes the layer panel deeper without making it easier to read, which is a loss for the person it is for. |
| frame | unknown | — | The group's box in the parent's coordinates. "union" (the default) is the union of the children's boxes and needs every child to have pixel x, y, w and h. "parent" is the parent's own box — the canvas, or an enclosing group — which preserves every coordinate verbatim and refuses nothing. An explicit box is refused for any child carrying a percentage the box would re-base. |
{
"name": "block.group",
"input": {
"nodeIds": [
"shape-1",
"shape-2"
],
"name": "Card"
}
}{
"name": "block.group",
"input": {
"nodeIds": [
"text-2",
"text-3",
"text-4"
],
"name": "Copy block",
"frame": "parent"
}
}{
"name": "block.group",
"input": {
"nodeIds": [
"shape-1",
"shape-2"
],
"name": "Badge",
"frame": {
"x": 80,
"y": 400,
"w": 520,
"h": 640
}
}
}block.insert
Insert a new Block under a parent, with everything inside it. `children` nests, up to three levels deep, so a whole region — a <vf-group> and its parts, a <vf-stack> and its lines — is one call and not one call per Block. Write a child's x/y in its parent's frame when that parent is a <vf-group>: the group is the coordinate space, so composing the region here means there is nothing to rebase later. Every Block it creates is reported in `created`, parents before children.
| Parameter | Type | Accepts | What it does |
|---|---|---|---|
| parentId * | unknown | — | Id of the parent the Block goes inside. Copy it from vf_get_doc exactly. Every id is a short lowercase name — "text-0", "group-1", "hero-card". The leading word is the Block's type, there to be read: it is not a rule, and never a way to build an id by counting ("text-2"). Use the one vf_apply reported in `created`. |
| index | integer | — (0 to 9007199254740991) | Position among the parent's children. Omit to append. Position is paint order: later children draw on top. |
| node * | object | — | The Block to create: its element name, its attributes as strings, its text where the Block holds text, and its `children` where it holds Blocks. |
{
"name": "block.insert",
"input": {
"parentId": "group-0",
"node": {
"type": "vf-text",
"params": {
"role": "caption"
},
"text": "No account needed"
}
}
}{
"name": "block.insert",
"input": {
"parentId": "canvas-0",
"node": {
"type": "vf-group",
"params": {
"x": "0",
"y": "1560",
"w": "1080",
"h": "120"
},
"children": [
{
"type": "vf-text",
"params": {
"role": "caption",
"x": "48",
"y": "30"
},
"text": "T. 0592-6666 8888"
},
{
"type": "vf-text",
"params": {
"role": "caption",
"x": "1032",
"y": "34",
"anchor": "top-right"
},
"text": "One city, four seasons"
}
]
}
}
}{
"name": "block.insert",
"input": {
"parentId": "canvas-0",
"node": {
"type": "vf-stack",
"params": {
"direction": "column",
"gap": "28",
"align": "center",
"x": "50%",
"y": "150",
"anchor": "top"
},
"children": [
{
"type": "vf-text",
"params": {
"role": "title"
},
"text": "Beginning of Autumn"
},
{
"type": "vf-text",
"params": {
"role": "subtitle"
},
"text": "Summer lingers, autumn has begun"
}
]
}
}
}block.move
Move a Block to a new parent and position. Also the way to change paint order within one parent, since paint order is document order.
| Parameter | Type | Accepts | What it does |
|---|---|---|---|
| nodeId * | unknown | — | Id of the target node, as reported by vf_get_doc. Copy it from vf_get_doc exactly. Every id is a short lowercase name — "text-0", "group-1", "hero-card". The leading word is the Block's type, there to be read: it is not a rule, and never a way to build an id by counting ("text-2"). Use the one vf_apply reported in `created`. |
| parentId * | unknown | — | Id of the destination parent. Copy it from vf_get_doc exactly. Every id is a short lowercase name — "text-0", "group-1", "hero-card". The leading word is the Block's type, there to be read: it is not a rule, and never a way to build an id by counting ("text-2"). Use the one vf_apply reported in `created`. |
| index * | integer | — (0 to 9007199254740991) | Position among the destination's children. |
block.remove
Remove a Block and everything inside it.
| Parameter | Type | Accepts | What it does |
|---|---|---|---|
| nodeId * | unknown | — | Id of the target node, as reported by vf_get_doc. Copy it from vf_get_doc exactly. Every id is a short lowercase name — "text-0", "group-1", "hero-card". The leading word is the Block's type, there to be read: it is not a rule, and never a way to build an id by counting ("text-2"). Use the one vf_apply reported in `created`. |
block.reorder
Move a Block through the paint order of its own parent, without naming an index. Paint order is document order — later siblings draw on top — so "front" is last and "back" is first. Already at the end it asks for is accepted and changes nothing. Refused on the regions directly under <vf-doc>, whose order is fixed by the format.
| Parameter | Type | Accepts | What it does |
|---|---|---|---|
| nodeId * | unknown | — | Id of the target node, as reported by vf_get_doc. Copy it from vf_get_doc exactly. Every id is a short lowercase name — "text-0", "group-1", "hero-card". The leading word is the Block's type, there to be read: it is not a rule, and never a way to build an id by counting ("text-2"). Use the one vf_apply reported in `created`. |
| to * | enum | front | back | forward | backward | front = draw on top of every sibling; back = behind them all; forward/backward = one step. |
{
"name": "block.reorder",
"input": {
"nodeId": "shape-3",
"to": "front"
}
}block.setParam
Set one parameter on one Block. Omit `value` to clear it back to the Block's default — clearing and setting to an empty string are different documents.
| Parameter | Type | Accepts | What it does |
|---|---|---|---|
| nodeId * | unknown | — | Id of the target node, as reported by vf_get_doc. Copy it from vf_get_doc exactly. Every id is a short lowercase name — "text-0", "group-1", "hero-card". The leading word is the Block's type, there to be read: it is not a rule, and never a way to build an id by counting ("text-2"). Use the one vf_apply reported in `created`. |
| param * | string | — | Parameter name, exactly as it appears as an attribute. |
| value | unknown | — | New value, written the way it would appear in the attribute. Omit to clear. |
block.setText
Replace the text content of a Block whose content model is text, such as <vf-text>.
| Parameter | Type | Accepts | What it does |
|---|---|---|---|
| nodeId * | unknown | — | Id of the target node, as reported by vf_get_doc. Copy it from vf_get_doc exactly. Every id is a short lowercase name — "text-0", "group-1", "hero-card". The leading word is the Block's type, there to be read: it is not a rule, and never a way to build an id by counting ("text-2"). Use the one vf_apply reported in `created`. |
| text * | string | — | The new text content. |
block.ungroup
Dissolve a <vf-group>, lifting its children into its parent where the group sat and putting their coordinates back into the parent's frame. Refused when the group carries something its children cannot absorb — a rotation, a scale, a clip, an opacity, a blur, a radius, a shadow or a background — because lifting them out would change the picture while reporting success. Remove that parameter first if dissolving the group is really what you want.
| Parameter | Type | Accepts | What it does |
|---|---|---|---|
| nodeId * | unknown | — | Id of the target node, as reported by vf_get_doc. Copy it from vf_get_doc exactly. Every id is a short lowercase name — "text-0", "group-1", "hero-card". The leading word is the Block's type, there to be read: it is not a rule, and never a way to build an id by counting ("text-2"). Use the one vf_apply reported in `created`. |
{
"name": "block.ungroup",
"input": {
"nodeId": "group-0"
}
}brief.setField
Record or update one field of the design's brief — the intent, constraints and rationale behind it. Never rendered; travels with the Design and is read back on every later turn. Creates the brief region if it does not exist yet.
| Parameter | Type | Accepts | What it does |
|---|---|---|---|
| name * | string | ^[a-z][a-z0-9-]*$ | Field name. Conventionally "intent", "audience", "constraint" or "rationale". |
| text * | string | — | The field's content. Pass an empty string to remove the field. |
node.setHidden
Show or hide a node. A hidden node stays in the document, keeps its parameters and its place in paint order, and simply is not drawn — so this is how to park a variation rather than delete it. Hiding a group hides everything in it. Refused on <vf-doc>, <vf-canvas>, <vf-brief> and <vf-field>, none of which draws anything.
| Parameter | Type | Accepts | What it does |
|---|---|---|---|
| nodeId * | unknown | — | Id of the target node, as reported by vf_get_doc. Copy it from vf_get_doc exactly. Every id is a short lowercase name — "text-0", "group-1", "hero-card". The leading word is the Block's type, there to be read: it is not a rule, and never a way to build an id by counting ("text-2"). Use the one vf_apply reported in `created`. |
| hidden * | boolean | — | true to hide, false to show. |
{
"name": "node.setHidden",
"input": {
"nodeId": "shape-1",
"hidden": true
}
}node.setLocked
Lock or unlock a node. A locked node refuses every other command — including delete, and including edits to anything inside it — until it is unlocked. This is the one command a locked node accepts, so unlocking is always a deliberate, separately recorded act. Locking a group locks everything in it.
| Parameter | Type | Accepts | What it does |
|---|---|---|---|
| nodeId * | unknown | — | Id of the target node, as reported by vf_get_doc. Copy it from vf_get_doc exactly. Every id is a short lowercase name — "text-0", "group-1", "hero-card". The leading word is the Block's type, there to be read: it is not a rule, and never a way to build an id by counting ("text-2"). Use the one vf_apply reported in `created`. |
| locked * | boolean | — | true to lock, false to unlock. Unlocking is allowed; say so in the note that carries the batch. |
{
"name": "node.setLocked",
"input": {
"nodeId": "shape-2",
"locked": true
}
}{
"name": "node.setLocked",
"input": {
"nodeId": "shape-2",
"locked": false
}
}node.setName
Give a node a readable name, the way a layer is named in a design tool. Names are for people and for you: a document of five <vf-shape> nodes is unreadable in the layers panel and unaddressable in conversation without them. Names need not be unique — a name is a label, not an identity; vf-id is the identity.
| Parameter | Type | Accepts | What it does |
|---|---|---|---|
| nodeId * | unknown | — | Id of the target node, as reported by vf_get_doc. Copy it from vf_get_doc exactly. Every id is a short lowercase name — "text-0", "group-1", "hero-card". The leading word is the Block's type, there to be read: it is not a rule, and never a way to build an id by counting ("text-2"). Use the one vf_apply reported in `created`. |
| name * | string | — | Up to 64 characters on one line. Pass an empty string to clear the name. |
{
"name": "node.setName",
"input": {
"nodeId": "group-0",
"name": "Phone mock"
}
}{
"name": "node.setName",
"input": {
"nodeId": "group-0",
"name": ""
}
}Blocks
Every Block in the core vocabulary, its content model, its parameters and the worked examples the registry carries — generated from the registry itself.
Channels, presets and Checks
The destinations a design can be made for, the exact pixel sizes and safe areas they publish, and every rule in the compliance gate.