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
- Scene - the canonical diagram document containing elements, styles, and constraints.
- Revision - each persisted scene has a revision number. Edits are applied against a specific
baseRevision. Conflicts return 409. - Operations - agents update diagrams by sending typed ops (
createNode,createEdge,moveElement, etc.) rather than replacing the whole scene. - Diagram family - optional declaration that activates family-specific node/edge types and rendering. See Diagram families below.
- Theme - pass
"theme"on render requests to control aesthetics without modifying the scene. See Rendering and themes below.
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:
| Field | Default | Notes |
|---|---|---|
algorithm | "hierarchical" | Sugiyama-style layered layout, the production-ready choice. |
direction | per-family | "TB", "BT", "LR", "RL". Most diagrams default to LR; org charts default to TB. |
nodeSpacing | 30 | Minimum 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). |
rankSpacing | 80 | Minimum 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:
- Declared canvas larger than required by node spacing → the engine auto-spreads nodes and ranks around their centroid to fill the available area. Pick a bigger canvas when you want more breathing room.
- Declared canvas smaller than the computed layout needs → the engine auto-extends
scene.canvas.{width,height}to fit content and emits aCANVAS_AUTO_EXTENDEDinfo diagnostic. The declared size is honoured as a floor; the engine never silently clips.
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:
- you need pixel-precise control for an editorial reason
- you’re editing an existing diagram with established positions you want to preserve
- the diagram has structural overrides the planner can’t infer (e.g. a swimlane where children must align across lanes)
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:
- Parallel processing paths or perimeter actors ->
workflow+pool/lane. When the diagram has 2+ horizontal or vertical bands of activity (frontend/backend/async, or actor-system-database), pool/lane frames make the parallelism visible and align messages across boundaries. Reach for this when you’d otherwise stack badges or decorators around one large central node. - Multi-step processes with decisions and joins ->
workflow. Diamond gateways and event-based intermediate nodes communicate “process logic” more clearly than generic nodes. - Data model with relationships ->
entityRelationship. Useextensions.columnsoner.entityfor PK/FK badges and crow’s-foot edges (one,many,zero-or-one,zero-or-many,one-or-many). - Time-ordered messages between actors ->
sequence. Auto-positions lifelines and orders messages by definition order, much less work than placing them yourself. - Reporting hierarchy ->
orgchart. Cards with name + title and optional accent stripe. - Screen navigation ->
uiflow. Title bar + description split via"label": "Title\nDescription".
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:
-
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" } } -
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:
| Property | Type | Description |
|---|---|---|
fill | color string | Background fill |
stroke | color string | Border/line color |
strokeWidth | number | Line thickness |
cornerRadius | number | Border radius |
fontFamily | string | Text font |
fontSize | number | Text size |
fontWeight | number or string | Weight (100-900, “normal”, “bold”) |
textColor | color string | Text color |
dash | number[] | Dash pattern (e.g. [6, 3]) |
labelBackground | color string | Background behind edge labels |
opacity | number (0-1) | Transparency |
accentColor | color string | Accent 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:
| Family | Use for | Common node types | Edge types |
|---|---|---|---|
architecture | Service architectures | service, database, queue, gateway | default |
workflow | BPMN processes | workflow.start, workflow.end, workflow.task, workflow.gateway (+ gatewayParallel/gatewayInclusive/gatewayEvent), workflow.subprocess, workflow.intermediateTimer/Message/Error, workflow.terminate, workflow.messageStart/messageEnd | workflow.sequenceFlow, workflow.messageFlow |
entityRelationship | ER / data models | er.entity, er.weakEntity | er.relationship, er.identifying |
sequence | UML sequence | sequence.actor, sequence.lifeline, sequence.activation, sequence.note | sequence.message, async, reply, create, destroy |
network | Network topology | network.server, router, switch, firewall, cloud, client | network.dataFlow, controlFlow |
orgchart | Org charts | org.person, org.role, org.department | org.directReport, org.dottedLine |
uiflow | UI navigation | uiflow.screen, modal, entry, exit | uiflow.navigate, back, action |
Family-specific rendering you should know about:
- Workflow - Full BPMN: filled start circle, bold end circle, gateway diamonds (x / + / circle / pentagon), subprocess
[+]marker, sequence flow with triangle arrowheads, message flow with open circle / open arrow. Pick edges deliberately: useedgeType: "workflow.sequenceFlow"for sequential workflow steps (auto-applies triangle arrowheads so the flow direction is visible) andedgeType: "workflow.messageFlow"for cross-pool message exchanges. PlainedgeType: "default"renders edges as lines with no arrowheads, which loses the direction indicator a reader needs to follow the flow. For decision diamonds without a BPMN-spec marker, usenodeType: "workflow.decision"- the label renders INSIDE the diamond (flowchart convention). The four marker-bearing gateway types (workflow.gateway,gatewayParallel,gatewayInclusive,gatewayEvent) use the BPMN external-label convention because their interiors carry a marker glyph. - ER - Set
extensions.columnsoner.entitywith{ name, type, pk?, fk?, unique?, nullable? }for compartmented entity boxes with PK/FK badges. Crow’s-foot cardinality glyphs (one,many,zero-or-one,zero-or-many,one-or-many) are auto-inferred from column metadata: PK column →one; FK column →manyby default, narrowing toone(unique),zero-or-many(nullable), orzero-or-one(unique + nullable). Setcardinalityon an endpoint only to override - author-set values are always preserved. Setendpoint.columnto anchor an FK edge at a named column row inside the entity (the dbdiagram.io / DBeaver convention). Whenendpoint.columnis undefined and the entity has a single PK, the endpoint anchors at the PK row automatically - so{ from: { elementId: "orders", column: "user_id" }, to: { elementId: "users" } }is enough; you don’t need to restatecolumn: "id"on the PK side. Self-FK edges (parent_id-style) work the same way and render as a U-bracket between the FK row and the PK row. If you don’t setendpoint.columnbut the edgelabelmatches a column name on either endpoint, the engine auto-promotes the edge and drops the label (column position then carries the FK identity); opt out withedge.style.forceLabel: trueto keep a label visible. - Sequence - The sequence resolver auto-positions lifelines and orders messages by their position in the elements array (definition order = visual order). You don’t need to specify positions for lifelines or messages. 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)"render without overflow - do NOT hand-tunelayout.widthon lifelines. 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, the platform draws them. Setscene.extensions.showLifelineFeet: falseto suppress foot bars. For UML annotations between messages (e.g. a “Postgres crashes” callout), usesequence.notewithextensions.anchor: "<id>"where<id>is the id of anysequence.lifelineorsequence.actor(note centres OVER that participant, PlantUML / Mermaid / UML 2.5 §17.4 convention), orextensions.anchor: ["<a>", "<b>"]for two or more participant ids (note spans across them, also centred-over semantics); place the note element between the two messages it should appear between - notes interleave with messages by document order. - Org chart - Use
"label": "Name\nTitle"for two-line cards. AddaccentColorfor a department stripe. - UI flow -
"label": "Title\nDescription"puts the first line in the title bar.
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:
- Read first when editing. Fetch the latest scene and revision before mutating. Fetch is token-efficient by default -
dsp_get_scene/GET /v1/scenes/:idreturns the scene without thecomputedsection (layout bounds, edge paths). PassincludeComputed=trueonly when you need computed layout data. - Create or build in small coherent op batches: nodes first, then edges, then containers/frames, then refinements. Use stable IDs for important elements.
- Apply against the latest
baseRevision. On 409 conflict, re-fetch and re-plan the patch - never replay stale ops. - Validate. Validation runs automatically on
applyOpsand scene creation. On 422, read the diagnostics, fix only the failing input, and retry. - 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. - Render. Pass
themefor 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:
- Attach a
messagetoapplyOpsdescribing the change (like a git commit message). dsp_list_revisions/GET /v1/scenes/:id/revisionsreturns timestamps, messages, and change summaries.dsp_diff_scene/GET /v1/scenes/:id/diff?from=N&to=Mreturns added, removed, and modified element IDs.- Rendered output includes
"Rev N - date"in the bottom-right by default. Suppress withshowRevision: false.
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.
| Theme | Background | Font | Description |
|---|---|---|---|
clean | White | Inter | Professional, crisp. Default when no theme specified. |
dark | Dark (#1a1a22) | Inter | Dark mode with warm amber and green-cyan accents. |
blueprint | Navy (#0d1b2a) | JetBrains Mono | Technical/engineering aesthetic with cyan strokes. |
sketch | Dark (#1a1a22) | Comic Neue | Hand-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):
- Set
textStyle: { wrap: "word" }on the node. - Set
layout.autoSize: "content"to compute height from text. - Use
setTextwith atextStyleparameter 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.
- Standalone text label - titles, captions, callouts, descriptive paragraphs that live OUTSIDE a node:
Text elements support{ "kind": "text", "text": "Section heading", "layout": { "x": 20, "y": 20, "width": 300, "height": 40 } }textStyle.wrap: "word"for line-wrapped paragraphs and accept\nfor explicit line breaks. - Bullet lists in text - put unicode bullets directly in the text content:
{ "kind": "text", "text": "Features:\n• Fast\n• Reliable\n• Cheap", "textStyle": { "wrap": "word" }, "layout": { ... } } - Horizontal or vertical separator line spanning the canvas - use a
guide:
Guides always render as dashed full-width / full-height lines. Perfect for dividing a diagram into labelled sections.{ "kind": "guide", "orientation": "horizontal", "position": 250 } - Dashed line on an edge or shape stroke - set
style: { dash: [4, 4] }(or any number array -[6, 3],[2, 2], etc):{ "kind": "edge", "from": { "elementId": "a" }, "to": { "elementId": "b" }, "style": { "dash": [4, 4], "stroke": "#cc0000" } } - Custom fill / stroke colors on individual elements - inline
style: { fill: "#cccccc", stroke: "#cc0000", strokeWidth: 2 }works on every element kind. For brand-wide colors, set scene-levelpaletteinstead so unstyled elements pick them up. - Image embed (logos, screenshots, custom diagrams) - use
{ "kind": "image", "assetRef": "...", "layout", "fit"? }. - Grouping for selection / movement only (no visible container) - use
{ "kind": "group", "children": [...] }. For a visible bordered container, use{ "kind": "frame", "containerType": "container", "children": [...] }.
If your design needs something not covered here, compose it from these primitives instead of concluding the platform can’t do it.
Anti-patterns
- Specifying pixel positions when auto-layout would work. This is the #1 mistake. Trust the layout engine.
- Full-scene regeneration for small edits. Use ops, not replacement.
- Writing without reading. Always fetch latest revision before mutating an existing scene.
- Ignoring validation. Don’t continue when diagnostics show blocking errors.
- Editing rendered SVG/PNG output directly. This is the most destructive anti-pattern. Rendered files are throwaway projections. Hand-edits are lost on the next render, cannot be diffed, cannot be audited, and break the scene/revision model entirely. Always edit the scene via
dsp_apply_ops(orPOST /v1/scenes/{id}/applyOps) and re-render. - Stripping the revision watermark by default. The
showRevision: truedefault stamps the scene ID and revision onto every render. Leave it on during iteration so every rendered image is traceable back to its scene and revision. Only passshowRevision: falsewhen the user explicitly asks for a clean final deliverable, and ideally only on the last render. - Blind retries. Don’t repeat a failed request without reading what failed.
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.