pub enum Op {
Show 40 variants
SetTextAlign {
node: String,
align: String,
},
MoveForward {
node: String,
},
MoveBackward {
node: String,
},
MoveToFront {
node: String,
},
MoveToBack {
node: String,
},
SetFill {
node: String,
fill: String,
},
SetStroke {
node: String,
stroke: String,
},
SetStrokeWidth {
node: String,
stroke_width: String,
},
SetVisible {
node: String,
visible: bool,
},
SetLocked {
node: String,
locked: bool,
},
SetGeometry {
node: String,
x: Option<f64>,
y: Option<f64>,
w: Option<f64>,
h: Option<f64>,
rotate: Option<f64>,
},
SetPoints {
node: String,
points: Vec<OpPoint>,
},
AddNode {
parent: String,
position: Position,
source: String,
},
RemoveNode {
node: String,
},
SetOpacity {
node: String,
opacity: f64,
},
ReplaceText {
node: String,
spans: Vec<OpSpan>,
},
DuplicateNode {
node: String,
new_id: String,
},
DuplicatePage {
page: String,
new_id: String,
id_suffix: String,
},
Group {
node_ids: Vec<String>,
group_id: String,
},
Ungroup {
group_id: String,
},
Reparent {
node: String,
new_parent: String,
position: Position,
},
AlignNodes {
node_ids: Vec<String>,
align: String,
anchor: String,
},
SetTextOverflow {
node_id: String,
overflow: String,
},
AddPage {
id: String,
w: String,
h: String,
background: Option<String>,
index: Option<usize>,
},
DeletePage {
page: String,
},
ReorderPages {
order: Vec<String>,
},
AddAsset {
id: String,
kind: String,
src: String,
sha256: Option<String>,
},
SetAsset {
node_id: String,
asset_id: String,
},
DistributeNodes {
node_ids: Vec<String>,
axis: String,
},
CreateToken {
id: String,
token_type: String,
value: String,
},
UpdateTokenValue {
id: String,
value: String,
},
SetStyleProperty {
style_id: String,
property: String,
value: String,
},
SetTextDirection {
node: String,
direction: String,
},
FindReplaceText {
find: String,
replace: String,
node: Option<String>,
},
SetPageSize {
page: String,
w: String,
h: String,
},
AlignToEdge {
node: String,
edge: String,
margin: f64,
},
CreateRecipe {
id: String,
kind: String,
seed: Option<i64>,
generator: Option<String>,
bounds: Option<String>,
detached: Option<bool>,
},
UpdateRecipe {
id: String,
kind: String,
seed: Option<i64>,
generator: Option<String>,
bounds: Option<String>,
detached: Option<bool>,
},
DeleteRecipe {
id: String,
},
DetachPattern {
node: String,
},
}Expand description
A single operation within a Transaction.
The op field in JSON is the snake_case tag, e.g. "set_text_align".
Variants§
SetTextAlign
Set the align property on a text node.
Valid values: start, center, end, justify.
MoveForward
Move a node one sibling position toward the end (front/top of z-order).
Has no effect if the node is already last in its parent’s children.
MoveBackward
Move a node one sibling position toward the beginning (back/bottom of z-order).
Has no effect if the node is already first in its parent’s children.
MoveToFront
Move a node to the topmost position (last child) in its parent’s children.
Has no effect if the node is already the last sibling (frontmost/topmost).
MoveToBack
Move a node to the bottommost position (first child) in its parent’s children.
Has no effect if the node is already the first sibling (backmost/bottommost).
SetFill
Set the fill property on a node that supports fill.
The fill value is a token id (e.g. "color.accent"); the engine
wraps it as PropertyValue::TokenRef(fill). Post-validation rejects
unknown token ids automatically.
Supported nodes: rect, ellipse, text, polygon, polyline.
Unsupported: line, frame, group, image — yields
tx.unsupported_property.
Fields
SetStroke
Set the stroke (outline color) property on a node that supports stroke.
The stroke value is a token id (e.g. "color.rule"); the engine wraps it
as PropertyValue::TokenRef(stroke). Post-validation rejects unknown token
ids automatically.
Supported nodes: rect, line, polygon, polyline.
Unsupported: ellipse (fill-only), text, frame, group, image —
yields tx.unsupported_property.
Fields
SetStrokeWidth
Set the stroke-width property on a node that supports stroke.
The value is a dimension token id (e.g. "size.stroke"), stored as
PropertyValue::TokenRef. A token (not a raw number) is required because
v0 stroke-width only resolves through dimension tokens at compile time;
post-validation rejects unknown token ids automatically.
Supported nodes: rect, line, polygon, polyline.
Unsupported: ellipse, text, frame, group, image — yields
tx.unsupported_property.
Fields
SetVisible
Show or hide a node by setting its visible property.
All known node variants except Unknown support this property.
Fields
SetLocked
Lock or unlock a node by setting its locked property.
All known node variants except Unknown support this property.
Fields
SetGeometry
Move and/or resize a bbox node by updating its x, y, w, h
geometry fields, and optionally set its rotate angle. All five fields
are optional — only the fields present in the JSON payload are changed;
omitted fields are left untouched.
Values are in document pixels ((px) unit) for x/y/w/h.
rotate is in degrees ((deg) unit at storage; pass a raw f64 here).
Supported nodes for x/y/w/h: rect, ellipse, frame, image,
text, code, group, field.
Supported nodes for rotate: rect, ellipse, frame, image, text,
code, group, polygon, polyline.
Unsupported for rotate: line, instance, field, footnote,
unknown — yields tx.unsupported_property.
If all five fields are omitted, an advisory tx.noop is emitted and no
node is recorded as affected.
JSON example (partial — only x, w, and rotate change):
{"op":"set_geometry","node":"r","x":10,"w":200,"rotate":45}Fields
SetPoints
Replace the entire vertex list of a polygon or polyline node.
Post-validation rejects automatically if the new point count falls
below the node’s minimum (polygon needs ≥ 3, polyline needs ≥ 2).
Supported nodes: polygon, polyline.
Unsupported: all other variants — yields tx.unsupported_property.
JSON example:
{"op":"set_points","node":"poly","points":[{"x":0,"y":0},{"x":100,"y":0},{"x":50,"y":80}]}Fields
AddNode
Construct a new node from a .zen source fragment and insert it into a
container (a page, group, or frame) at a chosen position.
source is a single .zen node fragment, e.g.
rect id="box" x=(px)10 y=(px)10 w=(px)100 h=(px)80 fill=(token)"color.accent".
It is parsed through the canonical KDL parser, so every node kind, nested
children (for group/frame), tokens, and properties are supported with no
per-field mapping. Exactly one top-level node must be present.
Post-validation rejects an incomplete/invalid node automatically (missing required geometry, duplicate id, unknown token/asset ref, too few points, …).
Fields
RemoveNode
Remove a node (and its subtree) by id from whatever container holds it.
Rejects with tx.unknown_node if no node with that id exists.
SetOpacity
Set the opacity of a node (0.0 = fully transparent, 1.0 = fully opaque).
The value is clamped to [0.0, 1.0] before being stored.
Supported nodes: all concrete variants (rect, ellipse, line, text,
code, frame, group, image, polygon, polyline).
Unsupported: unknown — yields tx.unsupported_property.
Fields
ReplaceText
Replace the entire span list of a text node with a new set of spans.
The spans vec fully replaces TextNode.spans. Replacing with an empty
vec is valid and clears all text content. fill and font_weight in each
OpSpan are token ids wrapped as PropertyValue::TokenRef; post-validation
rejects unknown token ids automatically (same as set_fill).
Supported nodes: text, and shape (replaces the shape’s owned label
spans, which use the same span model as a text node).
Unsupported: all other variants — yields tx.unsupported_property.
Fields
DuplicateNode
Duplicate a leaf node, assigning it a new id, and insert the clone immediately after the original in the same parent’s children.
v0 scope — leaf nodes only. Duplicating a container (frame or
group) is rejected with tx.unsupported_property. A deep-clone would
copy all descendant ids, producing duplicate ids throughout the subtree;
re-id’ing an entire subtree is deferred to a future version.
Post-validation catches a new_id that collides with an existing node
id via the id.duplicate diagnostic (same as Op::AddNode).
Rejects with tx.unknown_node if node does not exist in the document.
JSON example:
{"op":"duplicate_node","node":"box","new_id":"box-copy"}Fields
DuplicatePage
Duplicate an entire page (and its full subtree), inserting the copy immediately after the source page in the document body.
Unlike Op::DuplicateNode (leaf-only, v0), this performs a deep clone:
the new page gets new_id, and every descendant node id in the copy
is suffixed with id_suffix so all ids stay unique. Any page-level
safe_zones[].id is suffixed the same way.
duplicate_page only creates new content and never mutates the source,
so it is exempt from lock enforcement.
Rejects with tx.unknown_node if no page with id page exists.
Post-validation rejects the transaction if id_suffix fails to keep ids
unique (e.g. an empty suffix) via the id.duplicate diagnostic — that is
the safety net; an empty suffix also emits a helpful advisory.
JSON example:
{"op":"duplicate_page","page":"page.x","new_id":"page.x2","id_suffix":".v2"}Fields
Group
Wrap a set of sibling nodes inside a new group node.
All node_ids must be direct siblings under the same parent
(a page, group, or frame). If any id is not found, or if the ids
do not all share one common parent, the op is rejected with
tx.invalid_parent.
The new group is inserted at the position of the earliest (lowest index) member, preserving z-order. The grouped nodes are transferred into the new group in their original relative order.
Post-validation catches a group_id that collides with an existing
node id via the id.duplicate diagnostic.
v0 note: the group is created with x/y = None (no translation
offset). Children keep their authored coordinates; any visual shift must
be handled by the caller by adjusting child geometry separately.
JSON example:
{"op":"group","node_ids":["rect1","rect2"],"group_id":"grp-new"}Fields
Ungroup
Dissolve a group node, moving its children up to the group’s parent.
The group is replaced in-place by its children (spliced at the group’s original index), preserving source order.
Rejects with tx.unknown_node if group_id is not found.
Rejects with tx.unsupported_property (“not a group”) if the node is
not a group variant.
v0 limitation: the group’s own x/y translation is NOT applied
to children on ungroup (children keep their authored coordinates). If the
group had a non-zero x/y offset, the rendered positions of children
may shift after ungroup. An advisory is emitted in that case.
JSON example:
{"op":"ungroup","group_id":"grp1"}Reparent
Move a node to a different container (page, group, or frame).
Rejects with tx.unknown_node if node is not found.
Rejects with tx.invalid_parent if new_parent is not a container
(page, group, or frame), or if new_parent is node itself or a
descendant of node (cycle detection).
position controls where in the new parent’s children the node is
inserted; defaults to Position::Last (top of z-order).
JSON example:
{"op":"reparent","node":"rect1","new_parent":"grp1","position":{"at":"last"}}Fields
AlignNodes
Align a set of nodes to a common edge or centre along one axis.
align controls the alignment target:
- Horizontal:
"left","hcenter","right" - Vertical:
"top","vcenter","bottom"
anchor controls the reference rectangle:
"selection"(default): the union bounding box of all alignable nodes."page": the page that contains the nodes (0,0 to page w/h).- a node id: the bbox of that node.
- an explicit dimension like
"(px)120": align the chosen edge of every listed node to that absolute page coordinate. For the horizontal edges (left,hcenter,right) the value is an X coordinate; for the vertical edges (top,vcenter,bottom) it is a Y coordinate.
Only nodes supported by set_geometry (rect, ellipse, frame,
image) with resolvable x/y/w/h in px/pt are alignable. Any node
that lacks full geometry is skipped with a tx.geometry_unresolved
warning; the rest are still aligned.
An unknown align value is rejected with tx.unsupported_property.
An unknown anchor value is rejected with tx.unsupported_property.
A "(px)…" anchor whose dimension cannot be parsed is rejected with
tx.invalid_value.
Fewer than one alignable node emits tx.noop.
JSON example:
{"op":"align_nodes","node_ids":["a","b","caption"],"align":"left","anchor":"(px)120"}Fields
SetTextOverflow
Set the overflow property of a text or code node.
Valid values: "fit", "clip", "visible". Any other value is rejected
with tx.invalid_value.
Supported nodes: text, code.
Unsupported: all other variants — yields tx.wrong_node_type.
A missing node yields tx.unknown_node.
JSON example:
{"op":"set_text_overflow","node_id":"body","overflow":"visible"}Fields
AddPage
Create a new EMPTY page (no children) and insert it into the document
body at index (0-based) or, when index is None, append it at the
end.
w and h are canonical dimension strings like "(px)1800" / "(pt)90"
(the same (unit)value form parsed by other ops). background, when
present, is a token-ref id (e.g. "color.bg") stored as
PropertyValue::TokenRef — exactly like Op::SetFill.
Rejects with tx.duplicate_id if a page (or any node) already uses id.
Rejects with tx.invalid_value if w/h fail to parse as a dimension.
Rejects with tx.out_of_range if index is past the end of the page list.
The new page carries no children, safe-zones, folds, margins, or bleed — it is a blank canvas. Post-validation still runs over the whole document.
JSON example:
{"op":"add_page","id":"page.new","w":"(px)1800","h":"(px)1200","index":1}Fields
DeletePage
Remove the page whose id == page (and its entire subtree) from the
document body.
Rejects with tx.unknown_node if no page with that id exists.
JSON example:
{"op":"delete_page","page":"page.old"}ReorderPages
Reorder the document body’s pages to match order.
order must be a permutation of the existing page ids: the same set,
with no duplicates and nothing missing or extra. On success the pages are
rearranged so their ids follow order exactly.
Rejects with tx.invalid_value if order is not a permutation (an id is
missing, extra, duplicated, or unknown).
JSON example:
{"op":"reorder_pages","order":["page.b","page.a","page.c"]}AddAsset
Declare a new asset in the document’s assets block.
kind must be one of "image", "svg", or "font". src is a relative
path to the asset file. sha256 is an optional content-integrity digest.
Rejected immediately with tx.duplicate_id if an asset with id already
exists. Post-validation catches asset.invalid_src (absolute paths, ../
components, URLs) and asset.invalid_kind (unrecognized kinds).
JSON example:
{"op":"add_asset","id":"asset.logo","kind":"image","src":"images/logo.png","sha256":"abc123"}Fields
SetAsset
Set the asset reference on an image node.
The asset_id must reference a declared asset. An unknown asset_id is
permitted here (post-validation catches it via asset.unknown_reference).
An asset of kind font is eagerly rejected with tx.invalid_value because
image nodes require an image or svg asset.
Rejected with tx.unknown_node if node_id is not found.
Rejected with tx.wrong_node_type if node_id is not an image node.
JSON example:
{"op":"set_asset","node_id":"pic","asset_id":"asset.hero"}Fields
DistributeNodes
Evenly distribute a set of nodes along one axis so the gaps between consecutive nodes are equal, keeping the first and last node’s outer edges fixed (standard “distribute spacing” semantics).
The nodes are ordered by their current position on the chosen axis
before distributing. Requires ≥ 3 alignable nodes; fewer than three
emits tx.noop (consistent with align_nodes’ degenerate-input
convention) and leaves the document unchanged.
Only nodes supported by set_geometry (rect, ellipse, frame,
image, text, code, group) with resolvable x/y/w/h are
distributable. A listed node that is missing yields tx.unknown_node;
a node found but lacking resolvable geometry yields a
tx.unsupported_property warning and is skipped.
An unknown axis value is rejected with tx.unsupported_property.
JSON example:
{"op":"distribute_nodes","node_ids":["p1","p2","p3"],"axis":"horizontal"}Fields
CreateToken
Create a new design token in the document’s tokens block.
token_type is one of "color", "dimension", "number",
"fontFamily", "fontWeight". value is the literal in string form:
a color/family string ("#e11d48", "Inter"), a dimension string
("(px)40"), or a number ("700", "1.05").
Eagerly rejected with tx.duplicate_id if a token with id already
exists. Gradient/shadow/unknown types are rejected with
tx.invalid_value (v0: scalar literal token types only; gradient/shadow
tokens must be authored in source).
JSON example:
{"op":"create_token","id":"color.brand","type":"color","value":"#e11d48"}Fields
UpdateTokenValue
Replace the literal value of an existing token, preserving its declared type.
value is parsed against the token’s existing token_type; a value
that does not parse for that type is rejected with tx.invalid_value.
Rejected with tx.unknown_token if no token with id exists.
Gradient/shadow tokens cannot be updated via this op → tx.invalid_value.
JSON example:
{"op":"update_token_value","id":"color.brand","value":"#3b82f6"}Fields
SetStyleProperty
Set one recognized visual property on a named style to a token reference.
property is a style property key (fill, stroke, stroke-width,
font-family, font-size, font-weight, line-height, radius,
padding, gap, stroke-alignment); underscore spellings are accepted
and canonicalized. value is a token id, stored as
PropertyValue::TokenRef.
Rejected with tx.unknown_style if no style with style_id exists, and
tx.unsupported_property if property is not a recognized style key.
Unknown/incompatible token refs are caught by post-validation
(token.unknown_reference / token.incompatible_property).
Fields
SetTextDirection
Set the direction property on a text node. Valid values: "ltr", "rtl".
Any other value is rejected with tx.invalid_value. A missing node yields
tx.unknown_node; a non-text node yields tx.wrong_node_type.
Fields
FindReplaceText
Literal find-and-replace across text node spans and shape label spans,
preserving per-span formatting. find is a literal substring (NOT a
regex); all occurrences within each span’s text are replaced. When node
is given, only that text node or shape is scoped; when omitted, ALL text
nodes and shape labels in the document are scanned.
find must be non-empty (tx.invalid_value otherwise). A scoped node
that is missing yields tx.unknown_node; a scoped node that is neither a
text node nor a shape yields tx.wrong_node_type. If no occurrence is
found anywhere in scope, an advisory tx.noop is emitted and no node is
recorded as affected.
Locked nodes: a scoped locked node is guarded by the normal lock check
(rejected unless allow_locked). In document-wide mode, locked text nodes
and locked shapes are SKIPPED and reported via an advisory
tx.locked_skipped (warning) that names them — they are never silently
mutated.
Fields
SetPageSize
Resize a page (artboard). w/h are canonical dimension strings like
"(px)794" (same form parsed by add_page). Rejected with tx.unknown_node
if no page with id page exists, and tx.invalid_value if w/h fail to
parse or are not finite and > 0.
NOTE: child node coordinates are NOT reflowed — after shrinking a page,
children may fall outside the new bounds and trigger off_canvas advisories
at validation. Repositioning children is a separate concern (set_geometry).
Fields
AlignToEdge
Snap a single node’s edge (or center) to the boundary of the page that contains it, with an optional margin inset.
edge: "left", "right", "top", "bottom", "hcenter", "vcenter".
margin (default 0) insets the node from that page edge (ignored for the
center edges). For left/top/hcenter/vcenter margin is measured from
the low edge; for right/bottom it is measured from the high edge.
Computes: left → x = margin; right → x = page_w - node_w - margin; top → y = margin; bottom → y = page_h - node_h - margin; hcenter → x = (page_w - node_w)/2; vcenter → y = (page_h - node_h)/2.
Rejected with tx.unknown_node if the node is missing, tx.unsupported_property
if edge is not one of the six values or the node has no resolvable x/y/w/h
geometry. (Composable: issue two ops — e.g. right + bottom — to snap to a corner.)
Fields
CreateRecipe
Create a new recipe entry in the document’s recipes block.
Appends a new RecipeDef with the given scalar fields and empty
params, palette, expanded, and unknown_props; source_span is
None. Eagerly rejected with tx.duplicate_id if a recipe with id
already exists.
JSON example:
{"op":"create_recipe","id":"recipe.scatter","kind":"scatter","seed":42}Fields
UpdateRecipe
Replace the scalar fields of an existing recipe, preserving its
params, palette, expanded, and unknown_props.
The fields kind, seed, generator, bounds, and detached are
replaced with the op’s values. None for any Option field makes that
field absent on the recipe. Rejected with tx.unknown_recipe if no
recipe with id exists.
JSON example:
{"op":"update_recipe","id":"recipe.scatter","kind":"scatter","detached":true}Fields
DeleteRecipe
Remove a recipe from the document’s recipes block by id.
Rejected with tx.unknown_recipe if no recipe with id exists.
JSON example:
{"op":"delete_recipe","id":"recipe.scatter"}DetachPattern
Materialize a pattern node into an editable group of native shapes —
the “detach to native” path.
The pattern is replaced in place by a group with the same id and the
pattern’s x/y/w/h bounds. The group’s children are clones of the
pattern’s motif, one per instance position computed by
pattern_positions, each placed at its instance offset within the group.
Because the group translates its children by x/y exactly as the scene
places live pattern instances, the detached group renders identically to
the live pattern (same instance positions). Child ids are
<pattern-id>.0, <pattern-id>.1, … in render order.
Rejected with tx.unknown_node if no node with node exists.
Rejected with tx.not_a_pattern if node is not a pattern.
Rejected with tx.pattern_unresolved_bounds if the pattern’s w/h
cannot be resolved to a positive pixel size.
Rejected with tx.pattern_not_expandable if the layout yields no
instances (unknown kind or a missing required parameter).
JSON example:
{"op":"detach_pattern","node":"dots"}