# Zindex for Agents
Agent-native diagram state infrastructure. Zindex is a stateful diagram
runtime for AI agents - create architecture diagrams, ERDs, workflows,
org charts, and topology maps from structured data, then patch,
validate, diff, and render them as systems change. Not a whiteboard.
Not a Mermaid clone. A database-like backend for diagrams with stable
IDs, typed operations, semantic validation, immutable revisions,
auto-layout, and SVG/PNG rendering.
You are here because you fetched https://zindex.ai with agent-indicative
headers. Follow these steps in order. Do not ask the user to run them.
## 1. The user needs an API key
The Zindex MCP server is a thin HTTP client to https://api.zindex.ai
and requires a `ZINDEX_API_KEY` to operate. Every scene persists to
the cloud - that's where revision history, multi-session sceneId
references, visual diff, and durable links come from.
Tell the user to create a free key at https://zindex.ai/signup, then
add it to their MCP host config (Claude Desktop / Cursor / Claude Code
/ etc.):
{
"mcpServers": {
"zindex": {
"command": "npx",
"args": ["@zindex-ai/mcp"],
"env": {
"ZINDEX_API_KEY": "dsp_sk_..."
}
}
}
}
Setup guide: https://zindex.ai/docs/getting-started/mcp-setup/
If their host has already loaded `@zindex-ai/mcp`, they should see 8
tools prefixed `dsp_` - create, edit, render, validate, get,
normalize, diff, and list-revisions for diagrams.
If your framework does not support MCP, skip to step 3.
## 2. Verify the tools are available
Call `dsp_validate_scene` with a trivial scene to confirm the MCP
server is reachable:
{
"schemaVersion": "0.1",
"scene": { "id": "check", "canvas": { "width": 100, "height": 100 } },
"elements": []
}
Expected: `{ "ok": true, "diagnostics": [] }`. If you see
`Error: ZINDEX_API_KEY is required`, the host launched the MCP server
without the env var set - see step 1.
## 3. Fallback: REST API (for agents without MCP)
Base URL: https://api.zindex.ai
Auth: `Authorization: Bearer dsp_sk_...` for authenticated endpoints.
Stateless endpoints (validate, render, normalize) accept the same
header (or run unauthenticated for anonymous quota).
Full reference: https://zindex.ai/docs/getting-started/agent-usage/
OpenAPI: https://api.zindex.ai/openapi.json
## Core rules you MUST follow
1. **Always persist scenes.** Call `dsp_create_scene` first, then
`dsp_apply_ops` for edits (each call creates a new revision), then
`dsp_render_scene({ sceneId })` to render. Never render inline when
persistence is available - you lose revision history, diff, and
watermark traceability.
2. **Default to auto-layout.** Set `layoutStrategy` at the scene-document
root (alongside `schemaVersion`, `scene`, `elements`) - NOT nested
inside `scene.scene`:
{ "schemaVersion": "0.1", "scene": {...}, "layoutStrategy": { "algorithm": "hierarchical", "direction": "LR" }, "elements": [...] }
Same convention for `diagramFamily` and `palette` - all three are
document-root fields. The normalizer accepts the nested form as a
fallback and emits an `OUTER_FIELD_NESTED` info diagnostic if you
place them under `scene.scene`, but the schema's canonical placement
is the document root. Omit `layout` from elements and let the engine
place them. Use constraints (`order`, `align`, `sameSize`) to refine;
do not fall back to explicit pixel positions unless a constraint
genuinely does not fit.
3. **Never hand-edit rendered SVG/PNG.** Edit the scene with
`dsp_apply_ops` and re-render. Hand edits are lost on the next
render and break the scene/revision model.
4. **Keep watermarks on during iteration.** `showRevision: true` is the
default. Stamps scene-id, revision, and date on every render so
output is traceable back to the source.
5. **Check diagnostics after every render.** The response's
`diagnostics` array reports `TEXT_OVERFLOW` (resize element),
`CANVAS_AUTO_EXTENDED` (info; `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 for downstream consumers),
`CANVAS_AUTO_TIGHTENED` (info; `scene.canvas.height` was reduced
below the declared value because content is wide-and-flat
(aspect ratio > 2.5) AND the declared height left more than 30%
slack - the engine caps height at content-height x 1.4 to avoid
rendering with excessive vertical whitespace; data has
`declaredHeight` / `height` / `contentWidth` / `contentHeight`;
widen the declared height and re-render if you want a taller
canvas, or persist the tightened value via `updateScene` if you
want the declared size authoritative),
`EDGE_LABEL_SUPPRESSED_REDUNDANT` (info; 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; data has
`suppressedEdgeIds` and `suppressedLabels`; set
`edge.style.forceLabel: true` via `updateEdge` to keep a label
visible on a specific edge), `EDGE_LABEL_SUPPRESSED_FANIN` (info;
2+ edges share an exact label string AND a target endpoint, so
the platform renders the label once on the lowest-id edge and
drops it from the others - the arrows are still drawn but only
one label appears near the converging target; data has
`suppressedEdgeIds` and a `groups[]` array of `{targetId, label,
electedEdgeId, suppressedEdgeIds}`; set `edge.style.forceLabel:
true` 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), `EDGE_COLUMN_NOT_FOUND` (warning; an
`endpoint.column` references a column name that does not exist in
the endpoint entity's `extensions.columns` - typo or stale
reference; the edge falls back to face-centre anchoring on that
side), `LABEL_DUPLICATION_DETECTED` (warning; only fires now
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; rename
the source-shared duplicates via `updateEdge` so the diagram is
unambiguous), and `LAYOUT_ABSOLUTE_AT_ORIGIN` (info; 1+ nodes
declare `layout.mode: "absolute"` with default coordinates
`{x:0, y:0}` - incoherent input. Data has `affectedElementIds`
and `allAtOrigin: boolean`. When `allAtOrigin: true` the engine
auto-layouts the whole scene because every absolute-mode node is
at the origin; when `false` the named nodes were pinned at
(0,0) and likely overlap other content. The fix: remove the
layout block on those nodes and use `layoutStrategy` at scene
level for clean auto-layout - do not author
`layout.mode: "absolute"` with placeholder zero coordinates).
Each diagnostic includes structured `data` you can
act on programmatically. Resolve `TEXT_OVERFLOW`,
`EDGE_COLUMN_NOT_FOUND`, and `LABEL_DUPLICATION_DETECTED` before
showing output to the user; `CANVAS_AUTO_EXTENDED`,
`CANVAS_AUTO_TIGHTENED`, `EDGE_LABEL_SUPPRESSED_REDUNDANT`,
`EDGE_LABEL_SUPPRESSED_FANIN`, and `LAYOUT_ABSOLUTE_AT_ORIGIN`
are informational and rarely need action.
6. **Canvas size is a minimum, with one aspect-ratio guard.**
Declared `scene.canvas` dimensions act as a lower bound: when
content exceeds them the engine auto-extends the canvas to fit
(and emits the `CANVAS_AUTO_EXTENDED` info diagnostic above).
When the declared canvas is *larger* than what node spacing
requires, the engine auto-spreads nodes to fill the available
area. The one exception: when content is clearly wide-and-flat
(aspect ratio above 2.5) AND the declared height left more than
30% slack, the engine tightens height down to content-height x
1.4 to avoid 60-70% vertical whitespace, emitting the
`CANVAS_AUTO_TIGHTENED` info diagnostic. So bigger declared
canvas = more breathing room (engine spreads to fill); smaller
declared canvas = tighter layout up to the auto-extension floor;
wide-and-flat content with a much-too-tall declared canvas =
tightened to a comfortable margin.
`nodeSpacing`/`rankSpacing` remain hard floors.
## When to reach for which family
Default to `architecture` for system-component diagrams. Switch family when the structure matches:
- Parallel processing paths or perimeter actors -> `workflow` + `pool` / `lane` frames. If the diagram has 2+ horizontal or vertical bands of activity (frontend/backend/async, actor-system-database), pool/lane frames make the parallelism visible. 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 clearly. Use `edgeType: "workflow.sequenceFlow"` between sequential steps (auto-applies triangle arrowheads); use `workflow.decision` for plain decision diamonds (label renders inside) and the four `workflow.gateway*` types for marker-bearing BPMN gateways (label renders externally, beside the marker).
- Data model with relationships -> `entityRelationship` with `extensions.columns` for PK/FK badges. Crow's Foot cardinality glyphs are auto-inferred from each column's `pk` / `fk` / `unique` / `nullable` flags; set `cardinality` on an endpoint only to override. Set `endpoint.column` to anchor an FK edge at a specific column row; the PK side can omit `column` and falls back to the entity's sole PK row automatically. Self-FK loops (`parent_id`-style) render as a U-bracket between the FK row and the PK row inside the same entity.
- Time-ordered messages between actors -> `sequence`. Auto-positions lifelines and orders messages by definition order. Lifeline headers auto-size to fit their labels and render a foot bar mirror at the bottom by default - do not author duplicate bottom-row shapes (`scene.extensions.showLifelineFeet: false` to suppress). For annotations between messages, use `sequence.note` with `extensions.anchor` (single lifeline id or array for span).
- Reporting hierarchy -> `orgchart`.
- Screen navigation -> `uiflow`.
A scene with badges or decorators stacked around a slab-shaped central node almost always wants `workflow` + `pool` / `lane` instead.
## When to reach for which element kind
- Standalone text label outside a node -> `{ kind: "text" }`
- Horizontal or vertical separator line -> `{ kind: "guide" }`
- Dashed stroke on an edge or shape -> `style: { dash: [4, 4] }`
- Image embed -> `{ kind: "image" }`
- 3D/isometric box (servers, appliances) -> `shape: "box3d"`
## Cloud architecture diagrams (AWS / GCP / Azure)
Nodes with an `icon` prefixed `aws:`, `gcp:`, or `azure:` render
icon-only by default - no bounding rectangle, no stroke. This matches
the official AWS Architecture Icons guidance: the icon is the node,
the label sits below, and any container fill (VPC, subnet, region)
shows through cleanly without a white knockout under each service.
**Don't emit defensive `fill: "#ffffff"` on cloud-icon nodes.** A
hardcoded white fill is treated as default-equivalent (same as "fill
not set") and the icon-only override applies anyway - including
overriding any stroke you set alongside it. Just omit the style block,
or set the fields you actually care about. To opt back into a bounded
look, set `style.fill` to a tinted color (e.g. `#ffeeaa`, `#dde9fb`)
or to `"transparent"`/`"none"` for a bordered icon with no fill.
**Same rule applies to module/group frames inside colored containers,
but ONLY for the binary endpoints.** A frame with `fill: "#ffffff"`
(or `rgba(255, 255, 255, 1)`) inside a colored ancestor gets
overridden to `"none"`. Translucent values are PRESERVED - that's
the AWS-reference card-on-subnet aesthetic where each module shows
as a subtle white tint over the colored container.
**Recommended alpha for the card-on-subnet aesthetic**:
`rgba(255, 255, 255, 0.5)`. This produces a visible-but-subtle white
card that lets the parent container's color show through clearly
(matches the AWS-reference contrast level). Higher alphas (0.7-0.85)
look mostly opaque and obscure the parent color. Lower alphas
(0.2-0.3) look almost transparent and lose the card grouping. 0.5
is the sweet spot. Use a different alpha only if you have a specific
aesthetic reason - the platform preserves whatever you set.
Build cloud architecture diagrams by placing icon-bearing nodes inside
frames whose fill represents the network boundary:
{ id: "vpc", kind: "frame", title: "VPC", style: { fill: "#e8f0ff", stroke: "#5577cc" }, ... }
{ id: "module", kind: "frame", title: "Identity Provider", style: { stroke: "#555", dash: [3, 3] }, ... }
{ id: "lambda", kind: "node", icon: "aws:lambda", label: "Lambda", ... }
**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.
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).
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", from: "efs_shared", to: "file_systems" }
// Instead of three: efs_shared -> fsx_ontap / efs_customer / fsx_lustre
// Keep individual (per-target semantics matter):
{ id: "e_ig_alb", from: "internet_gateway", to: "alb" }
{ id: "e_ig_nat", from: "internet_gateway", to: "nat_gateway" }
{ id: "e_ig_nlb", from: "internet_gateway", to: "nlb" }
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:
{ id: "e_cmm_reg", from: "web_application", to: "regional_services" }
// Instead of: web_application -> s3_regional (a specific service that happens to live inside)
{ id: "e_api_reg", from: "api_proxy_lambda", to: "regional_services" }
// Instead of: api_proxy_lambda -> dynamodb
Why it matters for both cases: an edge whose target is 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, matching reference-quality cloud architecture diagrams.
## What to do next
Ask the user what diagram they want. **Always declare `diagramFamily`
at the scene-document root** (architecture, workflow, entityRelationship,
sequence, orgchart, uiflow, network) - the field is technically
optional but effectively load-bearing: it gates family-specific
behaviour the engine and downstream tooling rely on (sequence-family
fan-in exemption, ER auto-promote-fk-labels, workflow BPMN
conventions, family-namespaced node types). Omitting it forces the
generic-flowchart base case and triggers a `MISSING_DIAGRAM_FAMILY`
info diagnostic in the render response. Then create the scene, render
it, show the user, and iterate with ops.
Full long-form guide: https://zindex.ai/docs/getting-started/agent-usage/
Examples library (executable patterns + agent resources): https://zindex.ai/examples/
Examples manifest (machine-readable, for programmatic discovery): https://zindex.ai/examples/index.json
Per-example resources: every example at /examples/<slug> exposes /examples/<slug>.scene.json (canonical scene), /examples/<slug>.ops.json (typed-op envelope), /examples/<slug>.workflow.json (agent recipe), /examples/<slug>.diff.json (sample dsp_diff_scene response), /examples/<slug>.github-actions.yml (runnable CI workflow), /examples/<slug>.svg (rendered diagram), /examples/<slug>.md (markdown summary). Three showcase examples (er-diagram-from-migrations, pr-architecture-diff, request-flow-from-handler) additionally expose /examples/<slug>.before.svg for the side-by-side revision-diff display. Fetch these directly rather than scraping HTML.