Edit Operations Reference

The changeset vocabulary: operations, selectors, placement, flags, results and lint findings.

A changeset changes a tab with a short list of edit operations, applied together or not at all. This page lists the whole vocabulary: every operation, how to pick the elements it acts on, where new elements go, and what the answer and the lint tell you. Working with Documents shows how to send one.

The same vocabulary reaches a tab three ways:

  • The API: POST /documents/{id}/tabs/{tabId}/changesets with "operations" as a string (the line form) or an array of objects (the JSON form).
  • The CLI: livediagram edit "Auth flow" -f ops.txt sends a file (or stdin, with -f -) as one changeset, and livediagram element add|set|rm|move|connect|insert|wrap sends one operation. livediagram guide edit explains the line form, and livediagram schema view sticky lists the fields a kind takes.
  • The MCP server: update_document in ops mode turns its add, update and remove into add, set and rm, so removing a node removes its arrows too.

Two Forms

The line form is one operation a line, which is what an agent writes most easily. Values with spaces are quoted (label="Sign in"), and blank lines and lines starting with # are skipped:

# Rename the API box, add a database under it and join them
set n2 label="Orders API"
add cylinder id=db label="Orders DB" below:n2
connect n2 -> db label=writes

The JSON form is one object an operation, which is what a script produces. Any line of the text form that starts with { is read as JSON, so the two mix:

[
  { "op": "set", "target": "n2", "fields": { "label": "Orders API" } },
  {
    "op": "add",
    "kind": "cylinder",
    "id": "db",
    "fields": { "label": "Orders DB" },
    "place": { "rel": "below", "ref": "n2" }
  },
  { "op": "connect", "from": "n2", "to": "db", "fields": { "label": "writes" } }
]

One parser turns the line form into the JSON form, so both are checked the same way. A changeset holds at most 500 operations, and a line at most 16,384 characters.

Operations

OperationLine formWhat it does
addadd <kind> [id=<id>] key=value… [<placement>]Creates an element sized for its label
setset <selector> key=value… [all]Changes only the fields named; key= with nothing after it unsets one
rmrm <selector> [all] [keep-arrows]Removes elements, and the arrows pinned to them unless keep-arrows leaves those with free ends
movemove <selector> <placement> [all] or move <selector> by=dx,dy [all]Moves elements; pinned arrows follow, and a frame or lane carries its members
connectconnect <a> -> <b> [id=<id>] [label=…] [line=…] [again]Draws an arrow pinned at both ends, anchored on the sides facing each other
rewirerewire <arrow> from=<x> [to=<y>] or rewire <arrow> to=<y>Moves one or both ends of an arrow to other elements
insertinsert <kind> [id=<id>] key=value… between <a> <b>Puts a new element on the arrow from a to b, which becomes two arrows, and makes room for it
wrapwrap <selector…> in frame [id=<id>] key=value… [tidy] [absorb] [make-room]Draws a frame (or in lane, a lane) around the members
unwrapunwrap <frame>Removes a frame or lane and leaves its members where they are
orderorder <selector> front, back, above=<x> or below=<x>Changes the stacking order; frames and lanes always stay behind their contents
layoutlayout <selector> [style=flow] [direction=down]Lays out only the selection, keeping its top-left corner; style is flow, tree or mindmap, direction down or right
testtest <selector> key=value…Refuses the whole changeset unless the element still holds these values

A few rules hold for every operation:

  • Kinds. add and insert take a shape kind (square, circle, diamond, cylinder, stadium, hexagon and the rest) or text, sticky, table, annotation or link-card. A shape kind it does not know is turned into the nearest one, with a shape_coerced warning. Arrows are made with connect; images, videos and drawn strokes are not made by name, but an add in the JSON form can carry a whole element ({ "op": "add", "element": { … } }), which is how an uploaded image goes on a tab.
  • Ids. id= names a new element so later operations in the same changeset can use it. It is a short slug: a lowercase letter, then up to 23 lowercase letters, digits, - or _. Without one, the element takes the slug of its label (or of its kind, as arrow, arrow-2).
  • Fields. label, note, shape, fill, stroke, text-color, border, text (a size: sm, md, lg or scale), font and line (straight, angled or curved) work on the kinds that have them, beside any stored field of the element. fill= takes a colour slot of the tab's theme by name (such as theme), which follows a theme change; a hex colour works too, with a colour_overrides_theme warning. A sticky takes its own colour names instead. A number, true or false is read as JSON, and a value starting with [ or { is read as JSON as written.
  • Comments are not fields. Comment threads, votes and ideas travel live between people, so edit operations never change them; comments have their own endpoints.
  • Locks. A changeset on a locked tab, or one that would change a locked element or an element on a locked layer, is refused with element_locked.

Selectors

A selector picks the elements an operation acts on. It is a ref, a quoted label, or key:value terms joined by spaces, every one of which must match:

SelectorMatches
n3, 146b, ordersThe element with that ref, id or unique start of an id
"Orders service"The element whose label is exactly that, ignoring case
label~ordersElements whose label contains orders
type:stickyElements of that kind, as the outline prints it, or of that element type
shape:diamondShapes of that shape kind
in:f2The members of a frame or lane, at any depth (not arrows)
n3->n4, "Sign in"->"Pay"The arrow pinned from one element to the other
from:n3, to:n4Arrows pinned from, or to, an element
downstream:n3Every element reachable from n3 by following arrows forwards (n3 itself left out)
upstream:n4Every element that reaches n4 by following arrows
selectedWhat the token's owner has selected in an editor open on that tab

Terms combine: type:sticky in:f2 is the stickies inside frame f2, and from:n3 label~retry is the arrows leaving n3 whose label mentions retry.

  • A selector must match exactly one element unless the operation takes all. Nothing matching is refused as target_not_found with the nearest labels; several is refused as target_ambiguous, listing them.
  • wrap and layout act on many by nature. Each ref or quoted label you give names one element, and the other terms together add whatever they match.
  • selected with nothing selected is refused. The owner is whoever the API token belongs to, so an agent can act on "what I have selected" while you point at it.
  • Membership (for in:, wrap and moving a frame) is the smallest frame or lane holding an element's centre, the same rule the editor uses when you drag a frame.
  • The words selected, all, again, tidy, absorb, make-room, keep-arrows, between, in, frame, lane, front and back are always keywords. Reach an element named one of them by its quoted label or a longer ref.

Placement

add and move take one placement; insert places itself on its arrow.

PlacementPuts the element
right-of:<x>To the right of x
left-of:<x>To the left of x
above:<x>Above x
below:<x>Below x
after:<x>Along the flow of x's outgoing arrows
inside:<frame>In the next free slot inside a frame or lane
align:<x>Sharing x's centre, on the axis the element is already nearer
at:x,yAt that point, relative to the tab's content origin
gap:<n>Beside a side, after or align placement: the space to leave, 0 to 2,000

Without a placement, a new element goes beside the last one added, or to the right of everything. A spot already taken moves along the placement's direction until it is free. In the JSON form a placement is { "rel": "below", "ref": "n2", "gap": 40 } or { "rel": "at", "x": 0, "y": 200 }, and by=dx,dy is "by": [dx, dy].

What you did not ask to change stays put. set moves nothing except a box that grows around its centre to fit a longer label (reported). Only layout and a whole-tab replace lay out many elements, and only the ones they name.

Flags

FlagOnMeans
allset, rm, moveAct on every element the selector matches, not only one
keep-arrowsrmLeave arrows pinned to the removed elements in place, with free ends, instead of removing them
againconnectDraw a second arrow from a to b when one already exists (otherwise arrow_exists)
tidywrapLay the members out compactly before drawing the frame round them
absorbwrapLet the new frame take in elements that are not members but fall inside it
make-roomwrapMove those bystanders out of the way instead (without one of these two, wrap refuses with frame_captures)

In the JSON form, a flag is a member set to true: "all", "keepArrows", "again", "tidy", "absorb", "makeRoom".

insert always makes room: everything beyond the new element along the flow, within the same frame or lane (or the same connected group, outside them), shifts by its size and the gap, and those moves are reported. A frame holding shifted members grows with them.

The Answer

A changeset answers with one line per element, the same for a dry run (?dryRun=1, or --dry-run on the CLI) and a write:

~ n3  label "Login"→"Sign in" · shape square→stadium · widened 140→160
+ verify  square "Verify email" @45,300 131×120
~ a3  to n4→verify
+ arrow  verify→n4 (style of a3)
» n4 n5 n6  +0,+160 (make room)
f2  +verify
rev 41→42 · cs_8k2m4q7d1x · lint clean · revert: livediagram changeset revert 3f9c2a71-… cs_8k2m4q7d1x
MarkMeans
+Added
~Changed, each field before→after
-Removed, with the reason when it went with another
»Moved without being named (made room, carried by a frame)
f2 +verifyA frame or lane's membership changed
!A warning

The last line carries the tab's new revision, the changeset's id, the lint verdict and the command that reverts it. A changeset that changes nothing answers nothing changed.

Warnings do not stop a changeset: shape_coerced (an unknown shape kind became a known one), value_coerced, label_capped (a shape's label over 40 characters kept its heading and moved the full text into the note), colour_overrides_theme, arrows_freed (from keep-arrows), and no_base (sent without a base revision).

Refusals

A refused changeset applies nothing. It names the operation, the code and the way out. Lines that do not parse are all reported together (up to ten), so a batch of typos is fixed in one round:

error target_ambiguous · op 2 · set "Pay" fill=green
  "Pay" matches 3 elements:
    n7  square "Pay" in f2
    n9  sticky "Pay"
    a4  arrow n3→n7 "Pay"
  hint: use a ref, narrow with type:square, or add all
nothing was applied
CodeWhen
parse_errorA line does not parse; names the column and what was expected
unknown_operationThe first word is not an operation
target_not_foundA selector matches nothing
target_ambiguousA selector matches several without all
unknown_fieldA field the kind does not have; lists the kind's fields
invalid_valueA value outside what the field takes; lists what it takes
id_takenAn id= already in use; suggests a free one
arrow_existsconnect a -> b when an a→b arrow exists, without again
not_connectedinsert … between a b with no a→b arrow
frame_captureswrap would take in elements that are not members
element_lockedThe tab, a target or its layer is locked
test_failedA test does not hold; shows expected and actual
changeset_conflictA target changed since the base revision you sent
elements_heldSomeone has a target selected in the editor
invalid_resultThe result would not be a valid tab
too_largeOver 500 operations, or the tab's size cap

Lint Findings

Every changeset lints the whole tab it leaves behind, and GET /documents/{id}/tabs/{tabId}?view=lint (or livediagram tab lint) lists the findings, each ending with a fix you can send as a changeset:

5 crossings · 2 behind · 0 overlaps · 1317×1196 → 3 warnings, 6 info
W arrow-behind-box   orders→bus passes behind pay  fix: set orders->bus line=angled
CodeSeverityFindingSuggested fix
box-overlaperrorTwo boxes that arrows connect partly overlapmove b right-of:a
arrow-danglingerrorAn arrow is pinned to an element that is gonerm <arrow>
arrow-behind-boxwarningAn arrow passes through a box it does not connectset <arrow> line=angled
edge-crossingswarningMore crossing arrow pairs than a quarter of the arrowslayout type:shape
label-collisionwarningAn arrow's label overlaps a boxA shorter arrow label
label-overflowwarningA label needs more lines than its box holdstext=sm, or a shorter label and a note
node-isolatedwarningA shape with no arrows, on a tab that is otherwise a graphconnect <ref> -> a
group-escapewarningA member lies outside its framemove a inside:<frame>
group-split-edgesinfoMost of a frame's arrows cross its borderunwrap <frame>
duplicate-labelinfoTwo boxes share a labelRename one
flow-backwardsinfoAn arrow points against the drawing's flowrewire <arrow> from=y to=x
aspect-extremeinfoThe drawing is more than 3 times wider than tall, or the reverselayout type:shape direction=down
colour-on-themedinfoA fill or stroke the theme would not paint, bound to no swatchset a fill= to give it back to the theme

Boxes no arrow connects count as decoration and may overlap; the group checks judge frames, not lanes. Above 300 arrows the crossing check is skipped (the summary says so) and every other check still runs. The verdict on a changeset's last line reads lint clean or, say, lint 2 warnings, 1 info.

Send a changeset with ?dryRun=1 first while you learn the vocabulary: the answer and the lint are exactly what a write would give, and nothing changes.

Was this article helpful?