How AI Agents Should Use Zindex

Zindex is agent-native diagram state infrastructure - a stateful diagram runtime for AI agents. It exposes the Diagram Scene Protocol (DSP): a structured way to create, patch, validate, diff, and render diagrams as durable scene state. Instead of emitting one-off SVG, Mermaid, or ad hoc JSON every time a diagram changes, agents apply typed operations to versioned scenes - so the diagram stays in sync with the system it describes.

This guide covers the default workflow for agents and the behaviour rules that produce good output. For diagram-type fit, see Supported Diagram Classes.

Integration

Use MCP when your host supports it - it provides direct tool access with structured I/O. See MCP Setup. The 8 tools are: dsp_create_scene, dsp_get_scene, dsp_apply_ops, dsp_validate_scene, dsp_normalize_scene, dsp_render_scene, dsp_diff_scene, dsp_list_revisions.

Use the HTTP API otherwise. See API Endpoints. Both expose the same model: persisted scenes with revisions for multi-step work, and stateless validate/normalize/render for drafts.

Ready-to-use agent instructions

If you just want the agent to start working, point it at https://zindex.ai/ with Accept: text/markdown. That single URL returns an imperative, opinionated onboarding response telling the agent how to install the MCP server, the five core rules, and where to go next. It’s the canonical agent front door.

Then ask the agent to draw a diagram (“create an architecture diagram for this service”, “build an ER diagram for this schema”) and it will follow the persist-first workflow automatically. For MCP, configure @zindex-ai/mcp in your host settings with ZINDEX_API_KEY set - see MCP Setup for the full config snippet.

The front door response is short by design - it links back to this page for the full reference. The rest of this page covers the same material in more depth.

Core concepts

Auto-layout: the default authoring mode

Default to omitting positions. Set layoutStrategy on the scene and let the engine place every node, route every edge, and place every label automatically. This is the recommended pattern for almost all agent-generated diagrams - agents are good at describing structure (nodes and edges) and bad at producing pixel coordinates. The Layout Engine handles the geometry.

{
  "schemaVersion": "0.1",
  "scene": { "id": "auth-flow", "canvas": { "width": 1100, "height": 500 } },
  "layoutStrategy": { "algorithm": "hierarchical", "direction": "LR" },
  "elements": [
    { "id": "client", "kind": "node", "nodeType": "service", "label": "Client" },
    { "id": "api",    "kind": "node", "nodeType": "service", "label": "API Gateway" },
    { "id": "db",     "kind": "node", "nodeType": "database", "label": "Postgres" },
    { "id": "e1", "kind": "edge", "from": { "elementId": "client" }, "to": { "elementId": "api" } },
    { "id": "e2", "kind": "edge", "from": { "elementId": "api" },    "to": { "elementId": "db" } }
  ]
}

layoutStrategy parameters:

FieldDefaultNotes
algorithm"hierarchical"Sugiyama-style layered layout, the production-ready choice.
directionper-family"TB", "BT", "LR", "RL". Most diagrams default to LR; org charts default to TB.
nodeSpacing30Minimum pixels between nodes in the same rank. Hard floor - the planner uses larger gaps when the canvas has slack to fill (see “Canvas size matters” below).
rankSpacing80Minimum pixels between adjacent ranks. Hard floor - same auto-spread rules apply.

Canvas size is a minimum, not a hard cap. Declared scene.canvas dimensions act as a lower bound. The engine handles the two cases automatically:

Auto-spread never compresses below nodeSpacing / rankSpacing (those are hard floors). If you want the declared size to remain authoritative for downstream consumers after auto-extension fires, persist the new dimensions back to your scene via updateScene.

Mixed mode is supported. Nodes WITH explicit layout are pinned where you put them. Nodes WITHOUT get auto-positioned around the pinned ones. Use this when one or two anchor nodes have a “right place” but you want the engine to arrange everything else.

Use explicit positions only when:

The seven executable examples in the Examples library all use this position-less pattern; each one ships a canonical scene.json, ops.json (typed-operation envelope), workflow.json (agent recipe), prompt.md, diff.json (sample dsp_diff_scene response), github-actions.yml (runnable CI workflow), and rendered SVG. Three showcase examples (er-diagram-from-migrations, pr-architecture-diff, request-flow-from-handler) additionally ship a before.svg for the side-by-side revision-diff display. Browse the manifest to discover the per-example agent resources programmatically.

Refining auto-layout with constraints

Auto-layout handles most cases, but when you need specific spatial relationships, use constraints instead of switching to explicit positions:

Order constraints control which rank (layer) a node lands on and its position within a rank:

{
  "constraints": [
    { "type": "order", "source": "db", "target": "api", "relation": "rightOf" },
    { "type": "order", "source": "lb", "target": "api", "relation": "above" },
    { "type": "order", "source": "cache", "target": "db", "relation": "sameRank" }
  ]
}

Relations: leftOf, rightOf, above, below, sameRank. These are hard constraints the planner must satisfy - they’re interpreted relative to the layout direction.

Align constraints align nodes within containers or groups:

{ "type": "align", "axis": "x", "elements": ["memoryCache", "localDisk"] }

The engine also auto-aligns children within generic frames on the layout axis, so you often don’t need explicit align constraints for simple container layouts.

See the Constraints reference for all 10 constraint types.

When to reach for which family

Default to architecture for system-component diagrams. Switch family when the scene structure matches one of these patterns:

A scene with badges or decorators stacked around a slab-shaped central node almost always wants workflow + pool / lane instead. Splitting the work into bands renders cleaner than fanning many edges out of one oversized node.

Edges to a group as a whole

Edges may target a frame ID directly (not just nodes). The arrow then terminates at the frame’s border using cardinal anchors. Use this when the connection is semantically abstract - “this thing depends on Regional Services” or “uses the Customer Managed Resources cluster” - rather than tied to a specific service inside.

This matches the convention used by yEd, draw.io, Lucidchart’s AWS shapes, and the official AWS Reference Architecture diagrams: sidebar groups receive a single arrow to the group, not arrows to each service inside.

Two cases for using a frame as the target:

  1. Fan-out consolidation. When a source has multiple edges to siblings of a single parent frame and the connection is “this thing connects to the pool as a whole,” prefer ONE edge to the parent frame:

    // Consolidate (group-level fan-out):
    { "id": "e_efs_fs", "kind": "edge", "from": { "elementId": "efs_shared" }, "to": { "elementId": "file_systems" } }
    // NOT three edges efs_shared -> fsx_ontap / efs_customer / fsx_lustre
    
    // Keep individual when each connection is meaningful per-target:
    { "id": "e_ig_alb", "from": { "elementId": "internet_gateway" }, "to": { "elementId": "alb" } }
    { "id": "e_ig_nat", "from": { "elementId": "internet_gateway" }, "to": { "elementId": "nat_gateway" } }
  2. Abstract group dependency. When a single edge represents “this thing uses the X grouping” without specifying which member, target the frame directly:

    // Connect to the group as a whole (abstract dependency):
    { "id": "e_cmm_reg", "from": { "elementId": "web_application" }, "to": { "elementId": "regional_services" } }
    // NOT: web_application -> s3_regional (a specific service that happens to live inside it)

Why it matters: an edge targeting a node inside a frame routes through the frame’s border to reach the specific node, which visually reads as “the arrow trails off into the frame interior” - especially when the inner node is a borderless cloud-provider icon. Targeting the frame keeps the arrow tip on the frame’s clean dashed border.

Styling elements

The style field on every element accepts EITHER a named-style string OR an inline StyleValue object:

// Inline - put the style object directly on the element
{ "id": "n1", "kind": "node", ..., "style": { "fill": "#003366", "stroke": "#ffffff", "dash": [4, 4] } }

// Named - define once in scene.styles, reference by key
{ "styles": { "brand": { "fill": "#003366" } }, "elements": [{ ..., "style": "brand" }] }

Use setStyle to change an element’s style after creation. Valid StyleValue properties:

PropertyTypeDescription
fillcolor stringBackground fill
strokecolor stringBorder/line color
strokeWidthnumberLine thickness
cornerRadiusnumberBorder radius
fontFamilystringText font
fontSizenumberText size
fontWeightnumber or stringWeight (100-900, “normal”, “bold”)
textColorcolor stringText color
dashnumber[]Dash pattern (e.g. [6, 3])
labelBackgroundcolor stringBackground behind edge labels
opacitynumber (0-1)Transparency
accentColorcolor stringAccent stripe (org charts, ER headers)

Automatic text contrast: When you set a node fill without an explicit textColor, the engine picks a contrast-appropriate text color (light text on dark fills, dark text on light fills). This prevents illegible text when using custom fills or palette colors. To override, set textColor explicitly on the element or in a named style.

For scene-wide color defaults, use palette instead of styling every element individually.

Diagram families

Always declare scene-level diagramFamily on every scene. The field is technically optional in the schema, but it is effectively load-bearing: it gates family-specific behaviour the engine and downstream tooling rely on (sequence-family exemption from same-label fan-in collapse, ER-family auto-promote-fk-labels and column-row anchoring, workflow-family BPMN conventions, family-namespaced node types). Omitting it forces the engine into the generic-flowchart base case and triggers a MISSING_DIAGRAM_FAMILY info diagnostic on the render response. The element-types reference has the full list per family - this table covers the common cases:

FamilyUse forCommon node typesEdge types
architectureService architecturesservice, database, queue, gatewaydefault
workflowBPMN processesworkflow.start, workflow.end, workflow.task, workflow.gateway (+ gatewayParallel/gatewayInclusive/gatewayEvent), workflow.subprocess, workflow.intermediateTimer/Message/Error, workflow.terminate, workflow.messageStart/messageEndworkflow.sequenceFlow, workflow.messageFlow
entityRelationshipER / data modelser.entity, er.weakEntityer.relationship, er.identifying
sequenceUML sequencesequence.actor, sequence.lifeline, sequence.activation, sequence.notesequence.message, async, reply, create, destroy
networkNetwork topologynetwork.server, router, switch, firewall, cloud, clientnetwork.dataFlow, controlFlow
orgchartOrg chartsorg.person, org.role, org.departmentorg.directReport, org.dottedLine
uiflowUI navigationuiflow.screen, modal, entry, exituiflow.navigate, back, action

Family-specific rendering you should know about:

Swimlanes (workflow): Use frames with containerType: "pool" and "lane". Set laneDirection: "vertical" for horizontal bands or "horizontal" for vertical bands. Note that swimlane children currently need explicit layout with shared x-coordinates so they align across lanes - this is the one diagram type where auto-layout doesn’t work cleanly today.

Workflow

The default sequence works for both new diagrams and edits:

  1. Read first when editing. Fetch the latest scene and revision before mutating. Fetch is token-efficient by default - dsp_get_scene / GET /v1/scenes/:id returns the scene without the computed section (layout bounds, edge paths). Pass includeComputed=true only when you need computed layout data.
  2. Create or build in small coherent op batches: nodes first, then edges, then containers/frames, then refinements. Use stable IDs for important elements.
  3. Apply against the latest baseRevision. On 409 conflict, re-fetch and re-plan the patch - never replay stale ops.
  4. Validate. Validation runs automatically on applyOps and scene creation. On 422, read the diagnostics, fix only the failing input, and retry.
  5. Normalize when needed. Normalize before render only if you need to inspect computed positions, you used relative layout (rightOf, below), or you’re debugging routing. For most flows, the render call normalizes implicitly.
  6. Render. Pass theme for aesthetics. The scene is the source of truth - rendered SVG/PNG is a projection.

Updating scene metadata. Use the updateScene operation to change title, palette, styles, layoutStrategy, diagramFamily, or canvas on an existing scene without recreating it. Fields set to null (palette, layoutStrategy) are removed; unspecified fields are left unchanged. Styles are merged additively, and canvas is partially merged (e.g. updating only width preserves the existing height).

For drafts without persistence, use the stateless endpoints: POST /v1/scenes/validate, POST /v1/scenes/normalize, POST /v1/scenes/render. No scene ID, no revision tracking. Promote to a persisted scene only when the draft structure is ready.

Revisions

Every applyOps call creates an immutable revision. Useful tools:

Rendering and themes

Pass "theme" in the render request body to control the visual style. The theme changes colors, typography, and rendering mode without modifying the scene data.

ThemeBackgroundFontDescription
cleanWhiteInterProfessional, crisp. Default when no theme specified.
darkDark (#1a1a22)InterDark mode with warm amber and green-cyan accents.
blueprintNavy (#0d1b2a)JetBrains MonoTechnical/engineering aesthetic with cyan strokes.
sketchDark (#1a1a22)Comic NeueHand-drawn aesthetic with wobbly lines.

HTTP API - pass theme in the render request body:

POST /v1/scenes/my-diagram/render
{ "format": "svg", "theme": "blueprint" }

For stateless renders:

POST /v1/scenes/render
{ "scene": { ... }, "format": "svg", "theme": "blueprint" }

MCP - pass theme to dsp_render_scene:

dsp_render_scene({ "sceneId": "my-diagram", "theme": "blueprint" })

Themes are render-time only. The same scene renders differently with each theme, and you can switch themes without modifying the scene. See the Rendering reference for full details on formats, visual diff, and diagnostics.

Pair canvas.background with the matching theme. When you set scene.canvas.background to a dark color (e.g. #06080d, #1a1a22) and do NOT pass theme, the renderer auto-selects the dark theme so element strokes and labels stay legible against the dark canvas. Without this, a clean-theme #333 stroke on a near-black canvas is nearly invisible. Explicit theme: "clean" always wins. scene.palette.background does NOT trigger auto-selection - it controls the painted rect color only; set canvas.background to express dark-mode intent.

Scene palette

Set palette at the scene root to override theme defaults for all unstyled elements. Agents define colors once and every node, edge, and container inherits them - no need to style each element individually.

{
  "schemaVersion": "0.1",
  "scene": { "id": "branded", "canvas": { "width": 800, "height": 400 } },
  "palette": {
    "nodeFill": "#003366",
    "nodeStroke": "#001a33",
    "edgeStroke": "#004488",
    "foreground": "#ffffff",
    "background": "#f0f4f8"
  },
  "layoutStrategy": { "algorithm": "hierarchical", "direction": "LR" },
  "elements": [
    { "id": "a", "kind": "node", "nodeType": "service", "label": "Frontend" },
    { "id": "b", "kind": "node", "nodeType": "service", "label": "API" },
    { "id": "e1", "kind": "edge", "from": { "elementId": "a" }, "to": { "elementId": "b" } }
  ]
}

Palette sits between the theme and element-level styles in the resolution cascade: element style > scene palette > theme > fallback. Elements with explicit style still win. Palette keys match ThemePalette: background, foreground, nodeFill, nodeStroke, edgeStroke, containerFill, containerStroke, accentPrimary, accentSecondary, mutedForeground.

Validation

Validation rejects invalid input with HTTP 422 plus a list of diagnostics. The recovery loop is: read diagnostics -> fix the failing element -> resubmit. Don’t blindly retry the same payload.

validate: false is available for intermediate draft states or batch imports where you validate once at the end. Always re-enable validation before final render or persistence.

After render, check the response’s diagnostics array for TEXT_OVERFLOW warnings (report requiredHeight and availableHeight so you can resize an element in one operation), CANVAS_AUTO_EXTENDED info diagnostics (report declaredWidth/declaredHeight/width/height/extendedDimensions - scene.canvas was auto-extended to fit the computed layout; declared canvas dimensions are a minimum, not a hard cap, and the engine never silently clips; persist the new dimensions via updateScene if you want the declared size to remain authoritative), EDGE_LABEL_SUPPRESSED_REDUNDANT info diagnostics (report suppressedEdgeIds and suppressedLabels - an edge label that exactly matches a column declared in either endpoint’s extensions.columns was auto-promoted to a column-row anchor; the FK edge now terminates at the named column row inside the entity, matching the dbdiagram.io / DBeaver convention, and the label is dropped because the column position carries the FK identity; set edge.style.forceLabel: true via updateEdge to keep a label visible on a specific edge, or set endpoint.column directly to anchor at a column row without going through the auto-inference), EDGE_LABEL_SUPPRESSED_FANIN info diagnostics (report suppressedEdgeIds and a groups[] array of {targetId, label, electedEdgeId, suppressedEdgeIds} - 2+ edges share an exact label string AND a target endpoint, so the platform renders the label once on the lowest-id edge per group and drops it from the others; set edge.style.forceLabel: true via updateEdge to keep a label on a specific edge, or rename one of the duplicates if the convergence wasn’t intentional; sequence diagrams are exempt because same-label messages are temporally distinct), LABEL_DUPLICATION_DETECTED warnings (now only fire for the shared-SOURCE case - one node emitting 2+ edges with the same label to different targets; the shared-target case is handled silently by EDGE_LABEL_SUPPRESSED_FANIN above; report labelText, sharedEndpoint, and edgeIds so you can rename one of the source-shared duplicates via updateEdge), and LAYOUT_ABSOLUTE_AT_ORIGIN info diagnostics (report affectedElementIds and allAtOrigin - 1+ nodes declare layout.mode: "absolute" with default coordinates {x:0, y:0}, which is incoherent input; when allAtOrigin: true the engine fell back to auto-layout for the whole scene, when false the named nodes were pinned at (0,0) and likely overlap other elements; remove the layout block on those nodes and use layoutStrategy at scene level for clean auto-layout instead of authoring layout.mode: "absolute" with placeholder zeros).

Text-heavy nodes

For multi-line content (cards, comparison rows, info boxes):

  1. Set textStyle: { wrap: "word" } on the node.
  2. Set layout.autoSize: "content" to compute height from text.
  3. Use setText with a textStyle parameter to update content and styling in a single op.

Don’t use a node + text overlay - the wrap option exists specifically to replace that pattern.

Icons

Set icon to a semantic key: "lucide:database", "aws:lambda", etc. ~101 built-in icons across two packs (Lucide ~51 generic, AWS ~50 service). Lucide icons match the node’s text color; AWS icons use brand colors. All embedded as base64 data URIs - no external requests. URL fallback: "icon": "https://example.com/custom.svg". See Element Types: Icons for the full list.

When to reach for which element kind

Before declaring “Zindex can’t do X”, check this list. The most common false-limitation reports come from agents that didn’t try text, guide, or the dash style.

If your design needs something not covered here, compose it from these primitives instead of concluding the platform can’t do it.

Anti-patterns

For system prompts: use the front door

If you want a ready-to-paste onboarding block for an LLM system prompt, fetch the canonical agent front door:

curl -H "Accept: text/markdown" https://zindex.ai/

Or, if your host can’t set headers, paste from https://zindex.ai/_agent-front-door.md directly.

The front door is ~1.2 KB of imperative instructions covering the persist-first workflow, auto-layout default, anti-patterns, and element-kind cheatsheet. It is the single canonical source of agent onboarding - any update to platform behavior that affects agent usage lands there first, so a paste from the front door always reflects the current state of the platform. Everything else on this page is the long-form explanation behind those rules.