VizForDocs
Reference

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.

ParameterTypeAcceptsWhat it does
nodeIds *unknown[]The Blocks to move. The first one is the line the others come to, unless `to` says otherwise.
edge *enumleft | right | top | bottom | center-x | center-yWhich edge or centre line to share.
tostringAlign to this node id instead of the first listed, or to "canvas" for the canvas itself.
Put a stack of labels on one left margin
{
  "name": "block.align",
  "input": {
    "nodeIds": [
      "text-1",
      "text-2",
      "text-3"
    ],
    "edge": "left"
  }
}
Centre a card horizontally on the canvas
{
  "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.

ParameterTypeAcceptsWhat 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.
namestringA 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.
frameunknownThe 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.
two shapes that should move together
{
  "name": "block.group",
  "input": {
    "nodeIds": [
      "shape-1",
      "shape-2"
    ],
    "name": "Card"
  }
}
a run of centred lines, none of which declares a width
{
  "name": "block.group",
  "input": {
    "nodeIds": [
      "text-2",
      "text-3",
      "text-4"
    ],
    "name": "Copy block",
    "frame": "parent"
  }
}
a frame given explicitly, because the group is about to carry a fill
{
  "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.

ParameterTypeAcceptsWhat it does
parentId *unknownId 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`.
indexinteger (0 to 9007199254740991)Position among the parent's children. Omit to append. Position is paint order: later children draw on top.
node *objectThe Block to create: its element name, its attributes as strings, its text where the Block holds text, and its `children` where it holds Blocks.
a caption appended to a stack
{
  "name": "block.insert",
  "input": {
    "parentId": "group-0",
    "node": {
      "type": "vf-text",
      "params": {
        "role": "caption"
      },
      "text": "No account needed"
    }
  }
}
a whole footer region in one call, its parts placed in the group's own frame
{
  "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"
        }
      ]
    }
  }
}
a column of lines whose rhythm is a gap rather than three authored y values
{
  "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.

ParameterTypeAcceptsWhat it does
nodeId *unknownId 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 *unknownId 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.

ParameterTypeAcceptsWhat it does
nodeId *unknownId 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.

ParameterTypeAcceptsWhat it does
nodeId *unknownId 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 *enumfront | back | forward | backwardfront = draw on top of every sibling; back = behind them all; forward/backward = one step.
bring a badge in front of the card behind it
{
  "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.

ParameterTypeAcceptsWhat it does
nodeId *unknownId 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 *stringParameter name, exactly as it appears as an attribute.
valueunknownNew 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>.

ParameterTypeAcceptsWhat it does
nodeId *unknownId 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 *stringThe 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.

ParameterTypeAcceptsWhat it does
nodeId *unknownId 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`.
dissolve a group whose parts should be edited separately
{
  "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.

ParameterTypeAcceptsWhat it does
name *string^[a-z][a-z0-9-]*$Field name. Conventionally "intent", "audience", "constraint" or "rationale".
text *stringThe 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.

ParameterTypeAcceptsWhat it does
nodeId *unknownId 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 *booleantrue to hide, false to show.
park an alternative headline without losing it
{
  "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.

ParameterTypeAcceptsWhat it does
nodeId *unknownId 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 *booleantrue to lock, false to unlock. Unlocking is allowed; say so in the note that carries the batch.
protect a logo somebody placed by hand
{
  "name": "node.setLocked",
  "input": {
    "nodeId": "shape-2",
    "locked": true
  }
}
unlock before changing it, in the same batch as the change
{
  "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.

ParameterTypeAcceptsWhat it does
nodeId *unknownId 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 *stringUp to 64 characters on one line. Pass an empty string to clear the name.
name a group after the thing it depicts
{
  "name": "node.setName",
  "input": {
    "nodeId": "group-0",
    "name": "Phone mock"
  }
}
clear a name
{
  "name": "node.setName",
  "input": {
    "nodeId": "group-0",
    "name": ""
  }
}