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}/changesetswith"operations"as a string (the line form) or an array of objects (the JSON form). - The CLI:
livediagram edit "Auth flow" -f ops.txtsends a file (or stdin, with-f -) as one changeset, andlivediagram element add|set|rm|move|connect|insert|wrapsends one operation.livediagram guide editexplains the line form, andlivediagram schema view stickylists the fields a kind takes. - The MCP server:
update_documentinopsmode turns itsadd,updateandremoveintoadd,setandrm, 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
| Operation | Line form | What it does |
|---|---|---|
add | add <kind> [id=<id>] key=value… [<placement>] | Creates an element sized for its label |
set | set <selector> key=value… [all] | Changes only the fields named; key= with nothing after it unsets one |
rm | rm <selector> [all] [keep-arrows] | Removes elements, and the arrows pinned to them unless keep-arrows leaves those with free ends |
move | move <selector> <placement> [all] or move <selector> by=dx,dy [all] | Moves elements; pinned arrows follow, and a frame or lane carries its members |
connect | connect <a> -> <b> [id=<id>] [label=…] [line=…] [again] | Draws an arrow pinned at both ends, anchored on the sides facing each other |
rewire | rewire <arrow> from=<x> [to=<y>] or rewire <arrow> to=<y> | Moves one or both ends of an arrow to other elements |
insert | insert <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 |
wrap | wrap <selector…> in frame [id=<id>] key=value… [tidy] [absorb] [make-room] | Draws a frame (or in lane, a lane) around the members |
unwrap | unwrap <frame> | Removes a frame or lane and leaves its members where they are |
order | order <selector> front, back, above=<x> or below=<x> | Changes the stacking order; frames and lanes always stay behind their contents |
layout | layout <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 |
test | test <selector> key=value… | Refuses the whole changeset unless the element still holds these values |
A few rules hold for every operation:
- Kinds.
addandinserttake a shape kind (square,circle,diamond,cylinder,stadium,hexagonand the rest) ortext,sticky,table,annotationorlink-card. A shape kind it does not know is turned into the nearest one, with ashape_coercedwarning. Arrows are made withconnect; images, videos and drawn strokes are not made by name, but anaddin 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, asarrow,arrow-2). - Fields.
label,note,shape,fill,stroke,text-color,border,text(a size:sm,md,lgorscale),fontandline(straight,angledorcurved) 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 astheme), which follows a theme change; a hex colour works too, with acolour_overrides_themewarning. A sticky takes its own colour names instead. A number,trueorfalseis 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:
| Selector | Matches |
|---|---|
n3, 146b, orders | The element with that ref, id or unique start of an id |
"Orders service" | The element whose label is exactly that, ignoring case |
label~orders | Elements whose label contains orders |
type:sticky | Elements of that kind, as the outline prints it, or of that element type |
shape:diamond | Shapes of that shape kind |
in:f2 | The 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:n4 | Arrows pinned from, or to, an element |
downstream:n3 | Every element reachable from n3 by following arrows forwards (n3 itself left out) |
upstream:n4 | Every element that reaches n4 by following arrows |
selected | What 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 astarget_not_foundwith the nearest labels; several is refused astarget_ambiguous, listing them. wrapandlayoutact on many by nature. Each ref or quoted label you give names one element, and the other terms together add whatever they match.selectedwith 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:,wrapand 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,frontandbackare 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.
| Placement | Puts 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,y | At 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
| Flag | On | Means |
|---|---|---|
all | set, rm, move | Act on every element the selector matches, not only one |
keep-arrows | rm | Leave arrows pinned to the removed elements in place, with free ends, instead of removing them |
again | connect | Draw a second arrow from a to b when one already exists (otherwise arrow_exists) |
tidy | wrap | Lay the members out compactly before drawing the frame round them |
absorb | wrap | Let the new frame take in elements that are not members but fall inside it |
make-room | wrap | Move 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
| Mark | Means |
|---|---|
+ | 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 +verify | A 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
| Code | When |
|---|---|
parse_error | A line does not parse; names the column and what was expected |
unknown_operation | The first word is not an operation |
target_not_found | A selector matches nothing |
target_ambiguous | A selector matches several without all |
unknown_field | A field the kind does not have; lists the kind's fields |
invalid_value | A value outside what the field takes; lists what it takes |
id_taken | An id= already in use; suggests a free one |
arrow_exists | connect a -> b when an a→b arrow exists, without again |
not_connected | insert … between a b with no a→b arrow |
frame_captures | wrap would take in elements that are not members |
element_locked | The tab, a target or its layer is locked |
test_failed | A test does not hold; shows expected and actual |
changeset_conflict | A target changed since the base revision you sent |
elements_held | Someone has a target selected in the editor |
invalid_result | The result would not be a valid tab |
too_large | Over 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
| Code | Severity | Finding | Suggested fix |
|---|---|---|---|
box-overlap | error | Two boxes that arrows connect partly overlap | move b right-of:a |
arrow-dangling | error | An arrow is pinned to an element that is gone | rm <arrow> |
arrow-behind-box | warning | An arrow passes through a box it does not connect | set <arrow> line=angled |
edge-crossings | warning | More crossing arrow pairs than a quarter of the arrows | layout type:shape |
label-collision | warning | An arrow's label overlaps a box | A shorter arrow label |
label-overflow | warning | A label needs more lines than its box holds | text=sm, or a shorter label and a note |
node-isolated | warning | A shape with no arrows, on a tab that is otherwise a graph | connect <ref> -> a |
group-escape | warning | A member lies outside its frame | move a inside:<frame> |
group-split-edges | info | Most of a frame's arrows cross its border | unwrap <frame> |
duplicate-label | info | Two boxes share a label | Rename one |
flow-backwards | info | An arrow points against the drawing's flow | rewire <arrow> from=y to=x |
aspect-extreme | info | The drawing is more than 3 times wider than tall, or the reverse | layout type:shape direction=down |
colour-on-themed | info | A fill or stroke the theme would not paint, bound to no swatch | set 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?