Operations

DSP changes a diagram with typed operations. Send them as ops, in one batch, to diagram_edit or POST /v1/diagrams/{id}/revisions, with the baseRevision you read and a one-line message (Sending operations). An edit that changes the diagram makes its next revision. Field names match ops.schema.json, and examples elsewhere in the docs follow this page.

Node operations

createNode

Create a new node element.

{
  "op": "createNode",
  "id": "my-node",
  "nodeType": "service",
  "shape": "roundedRect",
  "label": "API Gateway"
}

Required: id, nodeType

Optional: layout, shape, label, textStyle, name, style, ports, icon, zIndex, locked, visible, metadata, and extensions (an ER entity’s columns go here). Instead of layout, the shorthand x, y, width and height gives the node an absolute layout at those values.

Without layout, the engine places the new node next to its neighbours, clear of the other nodes, and keeps that position. The other nodes stay where they are. With a layout that puts it on another node, both stay where they are, and the save’s warnings include NODE_OVERLAP_DETECTED. To place it yourself, give layout, such as { "mode": "absolute", "x": 50, "y": 100, "width": 180, "height": 70 }.

shape is optional and defaults per nodeType, such as database → cylinder; give one only for a different glyph (Element types: Shapes).

zIndex is optional: by default containers paint behind nodes, edges between and overlays on top. Set it only to change the paint order. A frame inside another frame always paints after it, whatever their zIndex.

icon takes a built-in key, such as lucide:database, or a URL (Element types: Icons).

updateNode

Update properties of an existing node.

{
  "op": "updateNode",
  "id": "my-node",
  "label": "Updated Label",
  "shape": "ellipse"
}

Required: id. At least one of the mutable properties (see Optional) must also be present.

Optional: any property from createNode except id and the x, y, width and height shorthand. icon: null removes the node’s icon. textStyle is merged into the node’s: only the fields you give change, as with setText.

style, layout, metadata, extensions and ports are replaced whole, not merged; only textStyle is merged. To add a column to an er.entity, read its extensions.columns, add the column, and send the complete array with updateNode; there is no column operation.

deleteElement

Remove any element from the scene.

{ "op": "deleteElement", "id": "my-node" }

Required: id

Deleting an element also deletes the edges with an end at it, and leaves nothing pointing at what went: the IDs leave every group’s and frame’s children; a constraint that names the element as its container, source or target is removed, and so is one left with fewer elements than it needs (two, or one for contain and centerIn), while any other constraint just loses it; and an element placed relative to it gets an absolute layout at the box it was last drawn with, so it stays where it is. The edit’s warnings list each constraint removed (CONSTRAINT_REMOVED) and each layout made absolute (RELATIVE_LAYOUT_FIXED). The members of a deleted group or frame stay where they are, unless one would then lie inside another frame that doesn’t hold it. The engine places that one again, clear of it, and the edit’s changed lists it.

moveElement

Move an element by a relative delta.

{ "op": "moveElement", "id": "my-node", "delta": { "dx": 50, "dy": -20 } }

Required: id, delta (with dx and dy)

Moving a frame or a group moves its members with it, and the members of frames and groups inside it, as containers move in other diagram tools. A member at x 0, y 0 isn’t placed yet and stays for the engine to place. A frame written without a layout is fitted around its moved members. A guide moves along the axis it marks.

moveElement refuses an edge, which the engine routes between its ends, and the nodes and frames of a sequence diagram, which it lays out from the messages (MOVE_NOT_SUPPORTED), and an element placed relative to another (MOVE_NOT_ALLOWED_FOR_RELATIVE_LAYOUT): change its layout with updateNode instead.

resizeElement

Change an element’s dimensions.

{ "op": "resizeElement", "id": "my-node", "size": { "width": 250, "height": 100 } }

Required: id, size (with width and height)

Optional: anchor ("top", "right", "bottom", "left") - the side that stays put. The element grows or shrinks away from that side and stays centred along it: with "bottom", the bottom edge keeps its place and the element grows upward. Without anchor, the top-left corner stays put.

A placed node made bigger pushes the nodes in its way, keeping the gap they had, at most the node spacing, and the edit’s changed lists them (Making room). A smaller one pulls nothing back.

Resizing a frame written without a layout gives it one: the box it was last drawn with, at the new size. From then on it keeps that box instead of fitting its members. An element placed relative to another keeps its placement and takes the new size as its preferredSize; leave anchor out for it.

Some resizes are refused, because the engine would undo them. A node at x 0, y 0 isn’t placed yet: the engine gives it the size its content needs when it places it, so resize it in a later edit, or create it with a layout that has a position. A frame written without a layout and created in the same edit hasn’t been drawn yet, so resize it in a later edit or create it with a layout (both RESIZE_NOT_PLACED). The engine sizes a sequence diagram’s nodes and frames from the messages, and edges, groups and guides have no size (RESIZE_NOT_SUPPORTED).

Edge operations

createEdge

Create a connection between two elements, usually nodes. A frame or group can be an end: an edge to a frame ends at its border.

{
  "op": "createEdge",
  "id": "e1",
  "from": { "elementId": "node-a" },
  "to": { "elementId": "node-b" },
  "label": "connects to",
  "router": "straight"
}

Required: id, from, to

Optional: label, router ("straight", "orthogonal", "curved", "polyline"), style, endpoints, waypoints. The engine routes every edge itself: waypoints are kept with the edge but not drawn through, and polyline draws like orthogonal.

Optional: edgeType - family-specific edge type (e.g. "workflow.sequenceFlow", "er.relationship"). An ER edge’s column and cardinality work as they do in a scene: an omitted cardinality is inferred from the columns, and follows them when they change.

Without endpoints, the edge gets the arrowheads of its edgeType, as an edge in a scene does: a filled triangle at the target for default. See Element Types: Edge types.

updateEdge

Update properties of an existing edge.

{
  "op": "updateEdge",
  "id": "e1",
  "label": "new label",
  "router": "orthogonal"
}

Required: id

Optional: any property from createEdge except id

Style and text

setStyle

Apply a named style or inline style overrides to an element.

{ "op": "setStyle", "id": "my-node", "style": "highlight" }

Or with inline overrides:

{ "op": "setStyle", "id": "my-node", "styleOverride": { "fill": "#ff0000", "strokeWidth": 2 } }

Required: id

Optional: style (named style token), styleOverride (inline StyleValue object)

Send one of the two. style replaces the element’s style. styleOverride merges onto the element’s current look: if the element uses a named style, its values from scene.styles are copied in first, so only the properties you send change.

setText

Set the label of a node or an edge, or the text of a text element. Optionally update text styling at the same time.

{ "op": "setText", "id": "my-node", "text": "New Label" }

With textStyle:

{
  "op": "setText",
  "id": "my-node",
  "text": "Centered Label",
  "textStyle": { "wrap": "word", "align": "center", "verticalAlign": "middle" }
}

Required: id, text

Optional: textStyle - an object with any combination of:

When textStyle is provided, it is merged with the element’s existing textStyle. Only the fields you include are updated; omitted fields keep their current values. This works on nodes and text elements; an edge has no textStyle, and ignores one.

Grouping

groupElements

Group multiple elements together.

{
  "op": "groupElements",
  "id": "group-1",
  "children": ["node-a", "node-b", "e1"]
}

Required: id, children (at least one element ID)

Optional: title, style, layout, name, zIndex, metadata, extensions

ungroupElements

Dissolve a group, releasing its children.

{ "op": "ungroupElements", "id": "group-1" }

Required: id, which names a group. Naming any other kind is the error OP_TARGET_KIND_MISMATCH; remove other elements with deleteElement.

createFrame

Create a visual frame container.

{
  "op": "createFrame",
  "id": "frame-1",
  "title": "Backend Services",
  "children": ["api", "db"]
}

Required: id, children (at least one element ID)

Optional: title, style, layout. Without layout, the engine sizes the frame around its children on every save. Children already placed keep their positions, unless the frame fitted round them would overlap another frame or take in a node that isn’t its child. Then the engine places them again, together, clear of the other frames and nodes, and the edit’s changed lists them. Children without a position are placed inside it. Give layout ({ "mode": "absolute", "x": 20, "y": 20, "width": 500, "height": 300 }) to fix its position and size yourself.

Optional: containerType ("generic", "pool", "lane", "subprocess", "partition", "package", "boundary", "fragment"), laneDirection ("horizontal", "vertical")

Constraints

addConstraint

Add a spatial constraint.

{
  "op": "addConstraint",
  "constraint": {
    "type": "align",
    "axis": "y",
    "elements": ["node-a", "node-b", "node-c"]
  }
}

Required: constraint (object with type and type-specific fields)

Axis values: "x", "y"

Constraints has every type and its fields. An order constraint added to a saved diagram takes effect in the same edit: when its two nodes don’t already stand in that order, the engine places both again, in order, around the nodes that keep their positions; nodes already in order stay where they are. The other types apply on every save.

removeConstraint

Remove a constraint by its ID.

{ "op": "removeConstraint", "id": "c1" }

Required: id

Layout

autoLayout

Have the engine place nodes again, and keep where it puts them. Nodes keep their positions after the first save, even when nodes around them are added or deleted. They move only when a node that grows needs the room; when an edit adds or deletes a frame round them and keeping them would leave frames overlapping or a frame holding a node that isn’t its member (createFrame, deleteElement); when an order constraint you add finds its two nodes the wrong way round (addConstraint); or when updateScene changes or removes layoutStrategy or diagramFamily. So autoLayout is how to get a fresh arrangement.

{ "op": "autoLayout", "elements": ["node-a"] }

Optional: elements, a list of node IDs. Without it, the engine lays the diagram out again, as a new diagram, and sizes the canvas to fit. With it, the engine places only the listed nodes again, next to their neighbours and clear of the other nodes, which stay where they are. That is the fix for NODE_OVERLAP_DETECTED. elements names nodes only: frames and edges are redrawn from the nodes on every save, so listing one is an error.

Without elements, in architecture, workflow, entity-relationship and UI flow diagrams and diagrams without a family, every frame but a pool, lane or fragment drops its own layout and is laid out with its members, fitted round them. A pool, lane or fragment with its own layout keeps its size, and its members stay where they are; so does every frame with its own layout in an org chart or network, whose layouts place nodes without regard to frames. The engine then places the other nodes around them without resizing the canvas. With elements, a frame with its own layout keeps it, and its listed members are placed again inside it. One that doesn’t fit lands outside, and the edit’s warnings include NODE_OUTSIDE_FRAME.

Nodes with a relative layout follow the node they’re placed against, and the engine lays sequence diagrams out from their messages on every save, so autoLayout doesn’t move either. An edit that lands every node where it already was makes no new revision (noop). To move a node yourself, send updateNode with a new layout. Changing or removing layoutStrategy or diagramFamily with updateScene places every node again too, the same way.

updateScene

Update scene-level metadata (title, brief, palette, styles registry, layoutStrategy, diagramFamily, canvas) on an existing scene. Fields set to null are removed; unspecified fields are left unchanged. Styles are merged additively; canvas is partially merged.

{
  "op": "updateScene",
  "title": "Updated Architecture",
  "palette": { "nodeFill": "#003366", "edgeStroke": "#004488" },
  "styles": { "highlight": { "fill": "#ffcc00", "stroke": "#cc9900" } },
  "layoutStrategy": { "direction": "LR" },
  "diagramFamily": "architecture",
  "canvas": { "width": 1400, "height": 800 }
}

Optional: title, brief (up to 1,000 characters, or null to remove), palette (or null to remove), styles (merged additively with the existing registry), layoutStrategy (or null to remove), diagramFamily (or null to remove), canvas (partially merged - omitted fields keep current values).

All fields are optional - include only the metadata you want to change. Omitted fields are preserved. Use this operation to modify scene-wide settings without recreating the scene. A new layoutStrategy or diagramFamily, or removing one, lays the diagram out again as autoLayout without elements does, under the new value: every frame but a pool, lane or fragment drops its own layout, except in org charts and networks, and only the members of a frame that keeps its layout stay where they are. The other fields leave the elements as they are.

History

restoreRevision

Make the next revision equal to an earlier one. History is never rewritten: the old content becomes a new revision, and every revision in between stays readable.

{ "op": "restoreRevision", "revision": 3 }

Required: one of revision and asOf. restoreRevision must be the only operation in its edit. Restoring the content the diagram already has makes no new revision (noop: true).

To restore by date, give the edit a restoreRevision argument instead of ops: { "restoreRevision": { "asOf": "2026-09-26" } } on diagram_edit or POST /v1/diagrams/{id}/revisions restores the revision that was current at that instant. asOf is a date (the end of that day in UTC) or an ISO 8601 timestamp with an offset, such as 2026-09-26T14:00:00Z. restoreRevision: 3 works there too. The operation takes asOf as well, in place of revision: { "op": "restoreRevision", "asOf": "2026-09-26" } restores the same revision.

Sending operations

An edit looks like this:

{
  "baseRevision": 1,
  "message": "Add the cache in front of the database",
  "requestId": "6f1c2e7a-4b0d-4c55-9a51-2f1f8f7c9d10",
  "ops": [{ "op": "createNode", "id": "cache", "nodeType": "database", "label": "Cache" }]
}

Send it as the arguments of diagram_edit, with the diagram’s id beside them, or as the body of POST /v1/diagrams/{id}/revisions, which takes the diagram’s ID or key from the path.

Fields:

Every edit is all or nothing. If any operation has an error, or the revision the edit would make breaks a rule the diagram didn’t break before, such as an edge to a port that isn’t there, the call returns 422 VALIDATION_FAILED with the errors and saves nothing. Operations run in order, but their order only matters for the element an operation changes, which must exist by then: an edge, a group, a frame, a style, a placement or a constraint can name an element that a later operation in the same edit creates. A saved edit returns warnings for the new revision: the diagram family’s rules it breaks, what drawing it noticed, and notes on what a deleteElement changed besides the element it named (a constraint it removed, or an element placed relative to the deleted one), each with a code, the element when there is one, and a one-line fix. Other changes an edit makes on its own get no note; changed lists every element it added, updated or removed. Validation rules lists the codes.

An edit that leaves the diagram unchanged makes no revision: the response has noop: true and the current revision. A retry without a requestId is also safe in the common case: if the diagram already equals what the edit asked for, the call returns noop: true instead of a conflict.

ops.schema.json also lists schemaVersion and sceneId. The API sets them itself and ignores them in a request: the diagram comes from the path or id.