Element Types
DSP supports 7 element kinds. Each has specific properties and behaviors.
Building an agent integration? See How AI Agents Should Use Zindex for the recommended workflow for scene creation, incremental edits, revision handling, validation, normalization, and rendering.
Node
The primary diagram element - a shape with a label.
{
"id": "my-node",
"kind": "node",
"nodeType": "service",
"shape": "roundedRect",
"label": "API Gateway",
"layout": {
"mode": "absolute",
"x": 50, "y": 100,
"width": 180, "height": 70
}
}
Node layout is optional when the scene has a layoutStrategy. Nodes without layout are positioned automatically by the layout engine.
Icons
Nodes can display an optional icon above the label. Set the icon field to a semantic key ({family}:{id}) or a URL:
{
"id": "db",
"kind": "node",
"nodeType": "database",
"shape": "roundedRect",
"label": "PostgreSQL",
"icon": "lucide:database",
"layout": { "mode": "absolute", "x": 50, "y": 50, "width": 180, "height": 100 }
}
Built-in icons (lucide family, ~51 icons): server, database, cloud, hard-drive, cpu, monitor, globe, smartphone, shield, lock, unlock, key, wifi, network, code, terminal, git-branch, git-merge, package, boxes, layers, file-code, table, bar-chart, filter, search, mail, message-square, bell, settings, wrench, refresh-cw, clock, calendar, zap, activity, user, users, user-check, check-circle, alert-triangle, x-circle, info, arrow-right, arrow-left, plus, minus, external-link, upload, download, trash
AWS service icons (aws family, ~50 icons): ec2, lambda, ecs, eks, fargate, elastic-beanstalk, auto-scaling, lightsail, s3, ebs, efs, rds, aurora, dynamodb, elasticache, redshift, documentdb, vpc, cloudfront, route53, elb, api-gateway, direct-connect, transit-gateway, iam, cognito, waf, shield, kms, sqs, sns, eventbridge, step-functions, cloudwatch, cloudformation, cloudtrail, systems-manager, kinesis, athena, glue, quicksight, sagemaker, codepipeline, codebuild, ecr, amplify, secrets-manager, certificate-manager, backup, budgets
AWS icons use their original brand color palette (orange for compute, green for storage, blue for database, purple for networking/analytics, red for security, pink for integration). They are NOT recolorable by themes.
Icons are embedded as base64 data URIs in rendered SVG/PNG - no external HTTP requests. Lucide icons automatically match the node’s text color for theme compatibility. URL fallback: "icon": "https://example.com/icon.svg".
Shapes
| Shape | Description |
|---|---|
rect | Rectangle |
roundedRect | Rounded rectangle |
ellipse | Ellipse / circle |
diamond | Diamond / rhombus |
cylinder | Database cylinder |
pill | Pill / stadium |
hexagon | Hexagon |
parallelogram | Parallelogram |
cloud | Cloud shape |
box3d | Isometric 3D box |
textBox | Text container (renders as rect) |
iconBox | Icon container (renders as rect) |
Node types
nodeType is a semantic label with no rendering effect.
Generic node types: generic, service, database, queue, actor, note, decision, document, process, container, externalSystem, storage, event, iconNode.
Family-specific node types use dot-namespaced values:
Workflow: workflow.start, workflow.end, workflow.task, workflow.decision, workflow.gateway, workflow.gatewayParallel, workflow.gatewayInclusive, workflow.subprocess, workflow.timer, workflow.error
workflow.decision renders the label INSIDE the diamond (the broader flowchart convention used by Mermaid, draw.io, Lucidchart, yEd, PlantUML). The four workflow.gateway* types render the label EXTERNALLY beside the marker glyph (BPMN convention) because their diamond interiors carry a marker (x, +, circle, pentagon) and there is no room for prose. Pick workflow.decision for a plain decision diamond; pick a workflow.gateway* type when the BPMN gateway semantics matter.
ER: er.entity, er.weakEntity, er.attribute, er.relationship
er.entity and er.weakEntity support compartmented rendering when extensions.columns is provided. Each column is an object with name (required), type (optional), pk (boolean), fk (boolean), unique (boolean), and nullable (boolean). The shape { name, type, key: "PK" | "FK", unique?, nullable? } is also accepted as an alias - the engine reads either form. The renderer produces a table-like box with a colored header bar (via accentColor), separator, and attribute rows with PK/FK badges and right-aligned types. Without extensions.columns, entities render as plain rectangles (backward compatible).
The unique and nullable flags don’t change the visible compartment but feed the auto-inference passes for column-row anchoring (see Column-row anchored edges) and Crow’s Foot cardinality glyphs (see Crow’s Foot cardinality).
Auto-sizing. When extensions.columns is set, the layout engine sizes the entity rect to fit the longest column row plus header and (rows × rowHeight) - author-declared layout.width / layout.height are treated as a minimum. If you set width: 100 on an entity whose longest column row measures 200px, the rect grows to 200px to prevent overflow; if you set width: 400 to leave breathing room, the rect stays at 400px. Auto-laid-out entities (no explicit layout) are sized from intrinsic measurements directly. This matches the canvas auto-extension semantics - declared dimensions are floors, not caps.
{
"id": "users",
"kind": "node",
"nodeType": "er.entity",
"shape": "rect",
"label": "Users",
"extensions": {
"columns": [
{ "name": "id", "type": "int", "pk": true },
{ "name": "email", "type": "varchar(255)" },
{ "name": "role_id", "type": "int", "fk": true }
]
}
}
Column-row anchored edges (auto-inference). When an edge’s label exactly matches the name of a column declared in either endpoint’s extensions.columns, the platform automatically promotes the edge to a column-row anchor: the FK edge terminates at the named column row inside the entity (matching the dbdiagram.io / DBeaver / DataGrip convention) and the edge label is dropped because the column position now carries the FK identity. Agents can also set endpoint.column directly:
{
"id": "fk_orders_users",
"kind": "edge",
"from": { "elementId": "orders", "column": "user_id" },
"to": { "elementId": "users", "column": "id" }
}
When the label matches a column on BOTH endpoints (the rare id-as-label case), both ends get the column anchor - the line goes column-to-column. When the label matches only one side, only that endpoint is column-anchored; the other terminates at the entity’s outer face.
The platform emits an EDGE_LABEL_SUPPRESSED_REDUNDANT info diagnostic listing every promoted edge ID and the unique label values dropped, so agents can audit the engine’s decision.
To keep a label visible on a specific edge (e.g. when the label is genuinely an annotation rather than the FK column name), set style.forceLabel: true on the edge:
{
"id": "fk_orders_users",
"kind": "edge",
"from": { "elementId": "orders" },
"to": { "elementId": "users" },
"label": "user_id",
"style": { "forceLabel": true }
}
When endpoint.column is set but the named column is not declared in the endpoint entity’s extensions.columns, the platform emits an EDGE_COLUMN_NOT_FOUND warning diagnostic and falls back to face-centre anchoring on that endpoint.
PK fallback for the unset side. When endpoint.column is undefined and the endpoint entity has a single PK column (pk: true or key: "PK"), the endpoint anchors at the PK row automatically. This means a typical FK edge can omit the column on the PK-side endpoint and still draw cleanly into the PK row inside the target entity:
{
"id": "fk_orders_users",
"kind": "edge",
"from": { "elementId": "orders", "column": "user_id" },
"to": { "elementId": "users" }
}
Self-FK edges (a parent_id-style column referencing the same entity’s PK) work the same way - the source side anchors at the FK row, the target side falls back to the PK row, and the renderer draws a U-bracket on the right face spanning the two rows.
Entities with no PK or composite PKs (multiple pk: true columns) skip the PK fallback and anchor at the entity face instead.
Crow’s Foot cardinality (auto-inference)
ER edges render Crow’s Foot glyphs at each endpoint based on the endpoint’s cardinality field. Valid values: one, many, zero-or-one, zero-or-many, one-or-many.
When cardinality is omitted on an endpoint and the endpoint resolves to a known column (via endpoint.column or the PK fallback above), the engine infers the value from the column metadata:
| Column metadata | Inferred cardinality |
|---|---|
PK column (pk: true or key: "PK") | one |
FK column, unique: true + nullable: true | zero-or-one |
FK column, unique: true | one |
FK column, nullable: true | zero-or-many |
| FK column (no other flags) | many |
Author-set cardinality is always preserved; auto-inference only fills in undefined endpoints. Self-loops still get inferred values, but the renderer skips Crow’s Foot glyphs on self-loops because the U-bracket geometry is too tight to fit two readable markers.
Sequence: sequence.lifeline, sequence.activation, sequence.actor, sequence.note
Lifeline headers auto-size to fit their labels (the 100 px width is a floor, not a cap), so multi-word participant names like "Compute Endpoint\n(Metadata)" no longer overflow the header rect. Each lifeline also renders a matching foot bar at the bottom of the diagram by default (UML / PlantUML / Mermaid convention) - do not author duplicate node shapes for the bottom row, and set scene.extensions.showLifelineFeet: false if you want to suppress the foot bars.
sequence.note is the UML annotation primitive for prose between messages - for example a “Postgres crashes” callout. Set extensions.anchor to the id of any sequence.lifeline or sequence.actor and the note centres OVER that participant (PlantUML / Mermaid / UML 2.5 §17.4 convention); pass a string array of two or more participant ids and the note spans across them (also centred-over semantics, leftmost to rightmost). Notes interleave vertically with messages by their position in the elements array; place the note element BETWEEN the two messages it should appear between. Renders as a rounded rect with a folded top-right corner, theme-aware soft yellow fill on light themes.
Network: network.server, network.router, network.switch, network.firewall, network.cloud, network.client
Org Chart: org.person, org.role, org.department
org.person and org.role render as structured person cards with bold name, muted title (parsed from label split on \n), left accent stripe (via accentColor style), and optional left-aligned icon (36px). org.department falls through to generic shape rendering.
Text style
Nodes accept an optional textStyle object that controls how the label text is rendered:
| Property | Values | Default | Description |
|---|---|---|---|
wrap | "none", "word", "character" | "none" | Text wrapping mode |
align | "left", "center", "right" | "center" | Horizontal text alignment |
verticalAlign | "top", "middle", "bottom" | "middle" | Vertical text alignment |
{
"id": "description-box",
"kind": "node",
"nodeType": "note",
"shape": "roundedRect",
"label": "This is a longer description that wraps across multiple lines within the node boundary.",
"textStyle": { "wrap": "word", "align": "left", "verticalAlign": "top" },
"layout": {
"mode": "absolute",
"x": 50, "y": 50,
"width": 220, "height": 120
}
}
Position shorthand
As a convenience, nodes (and other positioned elements) accept x, y, width, and height as top-level properties instead of nesting them inside a layout object:
{
"id": "quick-node",
"kind": "node",
"label": "Shorthand",
"x": 50, "y": 100,
"width": 180, "height": 70
}
This is equivalent to specifying layout: { mode: "absolute", x: 50, y: 100, width: 180, height: 70 }. The normalizer converts the shorthand into the full layout object automatically.
Auto-size
When layout.autoSize is set to "content" and textStyle.wrap is "word", the renderer automatically computes the node height from the text content. You only need to specify a width - the height adjusts to fit:
{
"id": "auto-node",
"kind": "node",
"label": "Height is computed from this text content automatically.",
"textStyle": { "wrap": "word" },
"layout": {
"mode": "absolute",
"x": 50, "y": 50,
"width": 200,
"autoSize": "content"
}
}
Edge
A connection between two elements. Endpoints reference any element by ID - typically nodes, but frame and group IDs are also valid targets. Edges to a frame terminate at the frame’s border using cardinal anchors, which is the right choice when the connection is semantically abstract (“this depends on Regional Services” or “uses the Customer Managed Resources cluster”) rather than tied to a specific member node. See agent usage: edges to a group as a whole for when to use this pattern.
{
"id": "e1",
"kind": "edge",
"from": { "elementId": "node-a", "portId": "right" },
"to": { "elementId": "node-b", "portId": "left" },
"router": "orthogonal",
"label": "requests"
}
Routers
| Router | Description |
|---|---|
straight | Direct line between endpoints |
orthogonal | Right-angle connectors |
polyline | Multi-segment straight lines |
Edge types
Edges can declare an optional edgeType for family-specific connection semantics: "default", "workflow.sequenceFlow", "workflow.messageFlow", "er.relationship", "er.identifying", "sequence.message", "sequence.reply", "sequence.create", "network.dataFlow", "network.controlFlow", "org.directReport", "org.dottedLine". org.dottedLine renders as a dashed line for matrix/secondary reporting.
Family-specific types also pick BPMN-correct arrowheads for you: workflow.sequenceFlow auto-applies a filled triangle on the end (so the flow direction is visible without reading labels) and workflow.messageFlow adds an open triangle plus a source-side open circle. Plain default renders an undirected line - prefer workflow.sequenceFlow for sequential workflow edges so a reader can follow direction at a glance.
Label typography
Edge labels matching the UML / SCXML / BPMN convention EVENT [guard] / action get multi-tier typography automatically: event in 12px standard, guard [...] in 10px muted, action / ... in 10px muted italic. Detection is content-based (any label containing balanced [...] OR a / separator) and family-agnostic - the same convention works on workflow, state-machine, sequence, and uiflow edges. Plain labels keep the existing single-tspan rendering. Multi-line labels (containing \n) skip the parse and use the existing line-wrapped path. See rendering reference: edge label typography for the full rule.
Endpoints
{
"endpoints": {
"startArrow": "none",
"endArrow": "triangle"
}
}
Endpoints can include an optional cardinality field for ER-style crow’s-foot notation:
| Value | Marker | Description |
|---|---|---|
one | | | Exactly one |
many | < | One or more (crow’s-foot) |
zero-or-one | ○| | Zero or one |
zero-or-many | ○< | Zero or more |
one-or-many | |< | One or more |
Group
A logical grouping of elements. No visual rendering by default.
{
"id": "group-1",
"kind": "group",
"children": ["node-a", "node-b"]
}
Frame
A visual container with a title bar and background.
{
"id": "frame-1",
"kind": "frame",
"title": "Backend Services",
"children": ["api", "db"],
"layout": {
"mode": "absolute",
"x": 20, "y": 20,
"width": 500, "height": 300
}
}
Container types
Frames can declare an optional containerType for semantic containment: "generic", "pool", "lane", "subprocess", "partition", "package", "boundary". Combined with laneDirection ("horizontal" or "vertical"), this enables swimlane-style layout.
laneDirection: "vertical" distributes lanes as horizontal bands (top-to-bottom). laneDirection: "horizontal" distributes as vertical bands (left-to-right).
Text
A standalone text block.
{
"id": "text-1",
"kind": "text",
"text": "System Architecture",
"layout": {
"mode": "absolute",
"x": 50, "y": 20,
"width": 300, "height": 40
}
}
Image
An image element referencing an asset.
{
"id": "img-1",
"kind": "image",
"assetRef": "logo",
"layout": {
"mode": "absolute",
"x": 50, "y": 50,
"width": 200, "height": 100
}
}
Guide
An alignment guide (horizontal or vertical line).
{
"id": "guide-1",
"kind": "guide",
"orientation": "horizontal",
"position": 200
}