← Examples

Living architecture diagram from a repository scan

Let an agent scan a repository and keep a service architecture diagram current as the codebase evolves. Frame-grouped services, databases, queues, and external systems with edge labels.

architecture Codebase / configuration files scangenerateupdate

What this example shows

A modern SaaS platform rendered as a frame-grouped architecture diagram - clients, edge, API layer, services, async, data plane, observability, and external systems each visually separated with labelled edges showing the dependencies between them. The same diagram an agent maintains for the team rather than a one-shot artwork that drifts the moment a service is added.

When to use it

When your architecture documentation matters but no one wants to maintain it by hand. The agent reads source code, configuration, and infrastructure-as-code; identifies services, databases, queues, and external systems; and applies typed operations to keep a persisted diagram current. Run it on every push to main, on a nightly schedule, or as a manual refresh - the diagram is always behind by at most one CI run.

What the agent does

The agent scans the repository and identifies the components: services from package.json workspaces, databases from connection strings or ORM configs, queues from worker definitions, external systems from outbound HTTP / SDK imports. It fetches the persisted scene, applies createNode for new components, updateNode when properties change, createEdge for new dependencies, and deleteElement for removed components. Then it validates structurally, renders the result, and stores the new revision.

Because Zindex preserves stable element IDs across runs, frame groupings stay intact and small changes produce small visual diffs. New engineers see the live architecture; reviewers see exactly what each PR added or removed.

What the output includes

  • A frame-grouped architecture diagram with seven logical layers (clients, edge, API, services, async, data plane, observability) plus an external-systems sidebar.
  • Lucide icons on every component for quick visual recognition.
  • Edge labels showing the protocol or relationship (HTTPS, queries, publishes, charges, emails).
  • Dashed edges for asynchronous flows; solid for synchronous request/response.
  • Frame-level edges (e.g. services → observability) to express “the whole layer emits telemetry” without drawing N redundant arrows.

Rendered diagram

Living architecture diagram from a repository scan — rendered diagram

Built by Zindex from the canonical scene below. Open in Playground to swap themes (clean / dark / blueprint / sketch), or POST the scene to /v1/scenes/render with format: "png" for a rasterised version.

Scene JSON

Raw

The canonical DSP scene used to render the diagram above. Drop into the Playground or POST to /v1/scenes/render to reproduce.

{
  "schemaVersion": "0.1",
  "diagramFamily": "architecture",
  "scene": {
    "id": "saas-platform",
    "title": "Modern SaaS Platform",
    "units": "px",
    "canvas": {
      "width": 1400,
      "height": 1200
    }
  },
  "elements": [
    {
      "id": "clients",
      "kind": "frame",
      "title": "Clients",
      "containerType": "generic",
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 40,
        "y": 30,
        "width": 1080,
        "height": 170
      },
      "children": [
        "web",
        "mobile",
        "api_consumer"
      ]
    },
    {
      "id": "web",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Web App",
      "icon": "lucide:monitor",
      "layout": {
        "mode": "absolute",
        "x": 230,
        "y": 75,
        "width": 180,
        "height": 110
      }
    },
    {
      "id": "mobile",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Mobile App",
      "icon": "lucide:smartphone",
      "layout": {
        "mode": "absolute",
        "x": 490,
        "y": 75,
        "width": 180,
        "height": 110
      }
    },
    {
      "id": "api_consumer",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Public API",
      "icon": "lucide:code",
      "layout": {
        "mode": "absolute",
        "x": 750,
        "y": 75,
        "width": 180,
        "height": 110
      }
    },
    {
      "id": "edge",
      "kind": "frame",
      "title": "Edge",
      "containerType": "generic",
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 40,
        "y": 220,
        "width": 1080,
        "height": 170
      },
      "children": [
        "cdn",
        "waf"
      ]
    },
    {
      "id": "cdn",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "CDN",
      "icon": "lucide:globe",
      "layout": {
        "mode": "absolute",
        "x": 330,
        "y": 265,
        "width": 200,
        "height": 110
      }
    },
    {
      "id": "waf",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "WAF",
      "icon": "lucide:shield",
      "layout": {
        "mode": "absolute",
        "x": 630,
        "y": 265,
        "width": 200,
        "height": 110
      }
    },
    {
      "id": "api",
      "kind": "frame",
      "title": "API Layer",
      "containerType": "generic",
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 40,
        "y": 410,
        "width": 1080,
        "height": 170
      },
      "children": [
        "gateway",
        "auth"
      ]
    },
    {
      "id": "gateway",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "API Gateway",
      "icon": "lucide:network",
      "layout": {
        "mode": "absolute",
        "x": 330,
        "y": 455,
        "width": 200,
        "height": 110
      }
    },
    {
      "id": "auth",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Auth Service",
      "icon": "lucide:lock",
      "layout": {
        "mode": "absolute",
        "x": 630,
        "y": 455,
        "width": 200,
        "height": 110
      }
    },
    {
      "id": "services",
      "kind": "frame",
      "title": "Services",
      "containerType": "generic",
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 40,
        "y": 600,
        "width": 1080,
        "height": 170
      },
      "children": [
        "users",
        "catalog",
        "orders",
        "payments",
        "notifs"
      ]
    },
    {
      "id": "users",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Users",
      "icon": "lucide:users",
      "layout": {
        "mode": "absolute",
        "x": 110,
        "y": 645,
        "width": 140,
        "height": 110
      }
    },
    {
      "id": "catalog",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Catalog",
      "icon": "lucide:boxes",
      "layout": {
        "mode": "absolute",
        "x": 310,
        "y": 645,
        "width": 140,
        "height": 110
      }
    },
    {
      "id": "orders",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Orders",
      "icon": "lucide:package",
      "layout": {
        "mode": "absolute",
        "x": 510,
        "y": 645,
        "width": 140,
        "height": 110
      }
    },
    {
      "id": "payments",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Payments",
      "icon": "lucide:layers",
      "layout": {
        "mode": "absolute",
        "x": 710,
        "y": 645,
        "width": 140,
        "height": 110
      }
    },
    {
      "id": "notifs",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Notifications",
      "icon": "lucide:bell",
      "layout": {
        "mode": "absolute",
        "x": 910,
        "y": 645,
        "width": 140,
        "height": 110
      }
    },
    {
      "id": "async",
      "kind": "frame",
      "title": "Async",
      "containerType": "generic",
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 40,
        "y": 790,
        "width": 1080,
        "height": 170
      },
      "children": [
        "event_bus",
        "workers",
        "scheduler"
      ]
    },
    {
      "id": "event_bus",
      "kind": "node",
      "nodeType": "queue",
      "shape": "roundedRect",
      "label": "Event Bus",
      "icon": "lucide:zap",
      "layout": {
        "mode": "absolute",
        "x": 230,
        "y": 835,
        "width": 180,
        "height": 110
      }
    },
    {
      "id": "workers",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Workers",
      "icon": "lucide:cpu",
      "layout": {
        "mode": "absolute",
        "x": 490,
        "y": 835,
        "width": 180,
        "height": 110
      }
    },
    {
      "id": "scheduler",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Scheduler",
      "icon": "lucide:clock",
      "layout": {
        "mode": "absolute",
        "x": 750,
        "y": 835,
        "width": 180,
        "height": 110
      }
    },
    {
      "id": "data",
      "kind": "frame",
      "title": "Data Plane",
      "containerType": "generic",
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 40,
        "y": 980,
        "width": 1080,
        "height": 170
      },
      "children": [
        "postgres",
        "redis",
        "search",
        "storage"
      ]
    },
    {
      "id": "postgres",
      "kind": "node",
      "nodeType": "database",
      "shape": "cylinder",
      "label": "PostgreSQL",
      "icon": "lucide:database",
      "layout": {
        "mode": "absolute",
        "x": 170,
        "y": 1010,
        "width": 160,
        "height": 130
      }
    },
    {
      "id": "redis",
      "kind": "node",
      "nodeType": "database",
      "shape": "cylinder",
      "label": "Redis",
      "icon": "lucide:database",
      "layout": {
        "mode": "absolute",
        "x": 390,
        "y": 1010,
        "width": 160,
        "height": 130
      }
    },
    {
      "id": "search",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Search Index",
      "icon": "lucide:search",
      "layout": {
        "mode": "absolute",
        "x": 610,
        "y": 1025,
        "width": 160,
        "height": 110
      }
    },
    {
      "id": "storage",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Object Storage",
      "icon": "lucide:hard-drive",
      "layout": {
        "mode": "absolute",
        "x": 830,
        "y": 1025,
        "width": 160,
        "height": 110
      }
    },
    {
      "id": "observability",
      "kind": "frame",
      "title": "Observability",
      "containerType": "generic",
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 1160,
        "y": 30,
        "width": 220,
        "height": 550
      },
      "children": [
        "logs",
        "metrics",
        "tracing"
      ]
    },
    {
      "id": "logs",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Logs",
      "icon": "lucide:terminal",
      "layout": {
        "mode": "absolute",
        "x": 1190,
        "y": 90,
        "width": 160,
        "height": 110
      }
    },
    {
      "id": "metrics",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Metrics",
      "icon": "lucide:bar-chart",
      "layout": {
        "mode": "absolute",
        "x": 1190,
        "y": 240,
        "width": 160,
        "height": 110
      }
    },
    {
      "id": "tracing",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Tracing",
      "icon": "lucide:activity",
      "layout": {
        "mode": "absolute",
        "x": 1190,
        "y": 390,
        "width": 160,
        "height": 110
      }
    },
    {
      "id": "external",
      "kind": "frame",
      "title": "External",
      "containerType": "generic",
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 1160,
        "y": 600,
        "width": 220,
        "height": 540
      },
      "children": [
        "stripe",
        "sendgrid"
      ]
    },
    {
      "id": "stripe",
      "kind": "node",
      "nodeType": "externalSystem",
      "shape": "roundedRect",
      "label": "Payment\nProvider",
      "icon": "lucide:external-link",
      "layout": {
        "mode": "absolute",
        "x": 1190,
        "y": 700,
        "width": 160,
        "height": 110
      }
    },
    {
      "id": "sendgrid",
      "kind": "node",
      "nodeType": "externalSystem",
      "shape": "roundedRect",
      "label": "Email\nProvider",
      "icon": "lucide:mail",
      "layout": {
        "mode": "absolute",
        "x": 1190,
        "y": 880,
        "width": 160,
        "height": 110
      }
    },
    {
      "id": "e_web_cdn",
      "kind": "edge",
      "from": {
        "elementId": "web"
      },
      "to": {
        "elementId": "cdn"
      },
      "label": "HTTPS",
      "router": "orthogonal"
    },
    {
      "id": "e_mob_cdn",
      "kind": "edge",
      "from": {
        "elementId": "mobile"
      },
      "to": {
        "elementId": "cdn"
      },
      "label": "HTTPS",
      "router": "orthogonal"
    },
    {
      "id": "e_api_cdn",
      "kind": "edge",
      "from": {
        "elementId": "api_consumer"
      },
      "to": {
        "elementId": "cdn"
      },
      "label": "HTTPS",
      "router": "orthogonal"
    },
    {
      "id": "e_cdn_waf",
      "kind": "edge",
      "from": {
        "elementId": "cdn"
      },
      "to": {
        "elementId": "waf"
      },
      "router": "orthogonal"
    },
    {
      "id": "e_waf_gw",
      "kind": "edge",
      "from": {
        "elementId": "waf"
      },
      "to": {
        "elementId": "gateway"
      },
      "router": "orthogonal"
    },
    {
      "id": "e_gw_auth",
      "kind": "edge",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "auth"
      },
      "label": "validates",
      "router": "orthogonal"
    },
    {
      "id": "e_gw_users",
      "kind": "edge",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "users"
      },
      "router": "orthogonal"
    },
    {
      "id": "e_gw_catalog",
      "kind": "edge",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "catalog"
      },
      "router": "orthogonal"
    },
    {
      "id": "e_gw_orders",
      "kind": "edge",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "orders"
      },
      "router": "orthogonal"
    },
    {
      "id": "e_orders_payments",
      "kind": "edge",
      "from": {
        "elementId": "orders"
      },
      "to": {
        "elementId": "payments"
      },
      "label": "triggers",
      "router": "orthogonal"
    },
    {
      "id": "e_users_db",
      "kind": "edge",
      "from": {
        "elementId": "users"
      },
      "to": {
        "elementId": "postgres"
      },
      "label": "queries",
      "router": "orthogonal"
    },
    {
      "id": "e_catalog_search",
      "kind": "edge",
      "from": {
        "elementId": "catalog"
      },
      "to": {
        "elementId": "search"
      },
      "label": "indexes",
      "router": "orthogonal"
    },
    {
      "id": "e_orders_db",
      "kind": "edge",
      "from": {
        "elementId": "orders"
      },
      "to": {
        "elementId": "postgres"
      },
      "router": "orthogonal"
    },
    {
      "id": "e_orders_redis",
      "kind": "edge",
      "from": {
        "elementId": "orders"
      },
      "to": {
        "elementId": "redis"
      },
      "label": "cache",
      "router": "orthogonal"
    },
    {
      "id": "e_orders_evt",
      "kind": "edge",
      "from": {
        "elementId": "orders"
      },
      "to": {
        "elementId": "event_bus"
      },
      "label": "publishes",
      "router": "orthogonal",
      "style": {
        "dash": [
          4,
          4
        ]
      }
    },
    {
      "id": "e_evt_workers",
      "kind": "edge",
      "from": {
        "elementId": "event_bus"
      },
      "to": {
        "elementId": "workers"
      },
      "label": "consumes",
      "router": "orthogonal",
      "style": {
        "dash": [
          4,
          4
        ]
      }
    },
    {
      "id": "e_sched_workers",
      "kind": "edge",
      "from": {
        "elementId": "scheduler"
      },
      "to": {
        "elementId": "workers"
      },
      "router": "orthogonal"
    },
    {
      "id": "e_workers_notifs",
      "kind": "edge",
      "from": {
        "elementId": "workers"
      },
      "to": {
        "elementId": "notifs"
      },
      "label": "triggers",
      "router": "orthogonal",
      "style": {
        "dash": [
          4,
          4
        ]
      }
    },
    {
      "id": "e_workers_storage",
      "kind": "edge",
      "from": {
        "elementId": "workers"
      },
      "to": {
        "elementId": "storage"
      },
      "label": "writes",
      "router": "orthogonal"
    },
    {
      "id": "e_payments_stripe",
      "kind": "edge",
      "from": {
        "elementId": "payments"
      },
      "to": {
        "elementId": "stripe"
      },
      "label": "charges",
      "router": "orthogonal"
    },
    {
      "id": "e_notifs_sendgrid",
      "kind": "edge",
      "from": {
        "elementId": "notifs"
      },
      "to": {
        "elementId": "sendgrid"
      },
      "label": "emails",
      "router": "orthogonal"
    },
    {
      "id": "e_services_obs",
      "kind": "edge",
      "from": {
        "elementId": "services"
      },
      "to": {
        "elementId": "observability"
      },
      "label": "telemetry",
      "router": "orthogonal"
    }
  ]
}

Agent workflow

Keep a service-architecture diagram of an entire codebase current by scanning the repository on a schedule (or on every default-branch commit), deriving services / databases / queues / external dependencies from configuration files, and patching the persisted Zindex scene incrementally so the diagram never drifts more than a single commit behind reality.

Inputs

  • Repository checkout (full clone, default branch)
  • Convention map: which directory layouts mean 'this is a service' (e.g. services/*/Dockerfile, apps/*/package.json, internal/cmd/*)
  • Detector list: regexes / dep-graph patterns that identify databases (postgres URL strings), queues (Kafka topics, SQS queue names), external systems (Stripe SDK imports, Sendgrid API keys)
  • Existing Zindex scene id (long-lived; one per repository)
  • Zindex API key

Outputs

  • Updated persisted scene with frames for each tier (clients / edge / API / services / data / async / observability / external) and one node per detected component
  • Rendered SVG suitable for embedding in the repository's README or docs site
  • Revision history that doubles as an architecture changelog (each revision message describes what shipped that day / week)
  • Optional: weekly Slack digest summarising newly-detected components
  1. 01

    Fetch the persisted scene

    Architecture scenes are long-lived (revisions stretch across months). Fetch the current state before deriving anything new.

    dsp_get_scene GET /v1/scenes/${SCENE_ID}
  2. 02

    Scan the repository for components

    Walk the directory tree applying the convention map: directories matching `services/*/Dockerfile` become service nodes, `apps/*/package.json` become app nodes. Apply the detector list against every source file: database URL strings → database nodes; queue topic / queue-name imports → queue nodes; SDK package names → external-system nodes. Group everything into the canonical eight-tier frame layout (clients, edge, API, services, data, async, observability, external).

  3. 03

    Derive edges from imports and config

    For each component, derive its outbound dependencies. Service A imports service-B's SDK → edge A→B. Service A reads from queue Q → edge A→Q (publishes); service B reads queue Q → edge Q→B (consumes), styled dashed for async. Service A's config contains a database URL → edge A→database. Service A imports Stripe SDK → edge A→stripe (external). Reuse stable element ids derived from directory paths so renames don't churn the graph.

  4. 04

    Compute the operation diff

    Compare the parsed (component, edge) tuple against the persisted scene. New components → createNode (placed in the appropriate tier frame via `children` updates on that frame). Removed components → deleteElement. New edges → createEdge. Changed labels → updateEdge.

  5. 05

    Apply the operation batch

    One applyOps batch with `errorPolicy: "allOrNothing"`. The `revisionMessage` should describe what shipped: 'add notifications service; consume orders.placed event' is more useful in the revision history than 'sync from main'.

    dsp_apply_ops POST /v1/scenes/${SCENE_ID}/ops
  6. 06

    Validate the architecture scene

    Resolve `LABEL_DUPLICATION_DETECTED` warnings (two services connecting to the same database with the same generic 'queries' label benefit from differentiating). `CANVAS_AUTO_EXTENDED` is informational - the canvas grows as the architecture grows; this is correct.

    dsp_validate_scene POST /v1/scenes/validate
  7. 07

    Render the updated diagram

    Render to SVG. Tier frames stay translucent so the colour of each tier is visible behind the components. Embed the rendered SVG in the repository README via a stable URL (`/examples/living-architecture-docs.svg` or the project's own `/architecture.svg`).

    dsp_render_scene POST /v1/scenes/${SCENE_ID}/render
  8. 08

    Optional: weekly Slack digest

    On weekly cadence, call `dsp_diff_scene` with `from=` the revision recorded a week ago and `to=` current. Post a short Slack message summarising the diff - new services, retired services, new external dependencies. This turns the architecture diagram into an ambient awareness signal rather than a thing engineers only consult when onboarding.

    dsp_diff_scene GET /v1/scenes/${SCENE_ID}/diff?from=${WEEK_AGO_REVISION}&to=${NEW_REVISION}

MCP recipe

For agents using Model Context Protocol. The tool sequence below matches the workflow steps; copy the prompt as a system message.

Tool sequence

  1. 01 dsp_get_scene Fetch the persisted scene
  2. 02 dsp_apply_ops Apply the operation batch
  3. 03 dsp_validate_scene Validate the architecture scene
  4. 04 dsp_render_scene Render the updated diagram
  5. 05 dsp_diff_scene Optional: weekly Slack digest

Unique tools used: dsp_get_scene, dsp_apply_ops, dsp_validate_scene, dsp_render_scene, dsp_diff_scene.

Copyable agent prompt

Drop this verbatim into a system prompt for an MCP-connected agent. The Zindex MCP server (@zindex-ai/mcp, configured with a ZINDEX_API_KEY environment variable - setup guide) exposes the tools the prompt references.

You are an automated architecture-documentation agent. Your job is to keep an architecture diagram of an entire codebase in sync with the actual code, by scanning the repository on a schedule (or on every default-branch commit) and patching a long-lived persisted Zindex scene with whatever has been added, removed, or rewired since the last run.

The persisted Zindex scene id is `${SCENE_ID}`; it already exists. The scene is long-lived - you will never recreate it. Each run computes the smallest valid set of typed operations that move the scene from its current revision to one that matches the repository's current state. Over months, the revision history doubles as an architecture changelog.

Workflow on every run:

1. Call `dsp_get_scene({ sceneId: "${SCENE_ID}" })`. Read the current revision number and the elements list (you'll need both for the diff and for the post-run validation).

2. Walk the repository, applying the convention map you've been given. The canonical map for typical monorepos: `services/*/Dockerfile` directories are service nodes; `apps/*/package.json` directories are application nodes. The directory name (or `name` field in `package.json` / `go.mod`) becomes the stable element id - never re-derive ids on every run; the long-lived stable id is what makes the revision history meaningful across renames.

3. Apply the detector list against every source file. Database URL strings (`postgres://`, `redis://`) → a database node. Queue topic / queue-name imports → a queue node. Imports of external SDK packages (`stripe`, `@sendgrid/mail`, `@aws-sdk/client-s3`) → an externalSystem node. Each detected component lands in the appropriate tier frame (clients, edge, API, services, data, async, observability, external).

4. Derive edges from imports and config. Service A imports `@acme/sdk-b` → edge A→B. Service A's config references a database URL → edge A→database. Service A reads from queue Q → edge Q→A (consumes), styled dashed; service B publishes to Q → edge B→Q (publishes), also dashed. Reuse stable edge ids built from `e_${from_id}_${to_id}` so re-running doesn't churn edge identities.

5. Diff what you parsed against what's in the scene. New components → `createNode` (and update the appropriate frame's `children` array via `updateNode`). Removed components → `deleteElement`. New edges → `createEdge`. Changed edge labels → `updateEdge`. Renamed components → `updateNode` (do not delete-and-create, or you'll lose the FK / containment relationships).

6. Call `dsp_apply_ops` with one batch. `errorPolicy: "allOrNothing"`. The `revisionMessage` should describe what shipped that run - 'add notifications service; consume orders.placed event' is far more useful long-term than 'sync from main'.

7. Call `dsp_validate_scene`. Resolve `LABEL_DUPLICATION_DETECTED` (e.g. two services with a generic 'queries' label to the same database - differentiate or anchor on a column). `CANVAS_AUTO_EXTENDED` and `EDGE_LABEL_SUPPRESSED_REDUNDANT` are informational; ignore.

8. Call `dsp_render_scene({ format: "svg", theme: "clean" })`. Embed the rendered SVG in the repository's README via a stable URL - that's how the diagram becomes ambient documentation.

9. On a weekly cadence, call `dsp_diff_scene({ from: WEEK_AGO_REVISION, to: NEW_REVISION })` and post a short Slack message to the platform / engineering channel summarising new services, retired services, and new external dependencies. The diagram becomes an ambient awareness signal rather than a thing engineers only consult during onboarding.

Hard rules: never hand-edit the rendered SVG. Never regenerate the scene from scratch - always patch with stable ids. Never collapse a multi-tier topology into a single rank - the eight-tier frame layout is what makes the diagram readable as the system grows; preserve frame containment with each new node.

HTTP API recipe

For agents/devs not using MCP. Set $ZINDEX_API_KEY in the Authorization header on authenticated calls. Stateless endpoints (/v1/scenes/render, /v1/scenes/validate, /v1/scenes/normalize) need no key. Full reference: API endpoints, OpenAPI spec.

  1. 01 GET /v1/scenes/${SCENE_ID}

    Architecture scenes are long-lived (revisions stretch across months). Fetch the current state before deriving anything new.

  2. 02 POST /v1/scenes/${SCENE_ID}/ops

    One applyOps batch with `errorPolicy: "allOrNothing"`. The `revisionMessage` should describe what shipped: 'add notifications service; consume orders.placed event' is more useful in the revision history than 'sync from main'.

  3. 03 POST /v1/scenes/validate

    Resolve `LABEL_DUPLICATION_DETECTED` warnings (two services connecting to the same database with the same generic 'queries' label benefit from differentiating). `CANVAS_AUTO_EXTENDED` is informational - the canvas grows as the architecture grows; this is correct.

  4. 04 POST /v1/scenes/${SCENE_ID}/render

    Render to SVG. Tier frames stay translucent so the colour of each tier is visible behind the components. Embed the rendered SVG in the repository README via a stable URL (`/examples/living-architecture-docs.svg` or the project's own `/architecture.svg`).

  5. 05 GET /v1/scenes/${SCENE_ID}/diff?from=${WEEK_AGO_REVISION}&to=${NEW_REVISION}

    On weekly cadence, call `dsp_diff_scene` with `from=` the revision recorded a week ago and `to=` current. Post a short Slack message summarising the diff - new services, retired services, new external dependencies. This turns the architecture diagram into an ambient awareness signal rather than a thing engineers only consult when onboarding.

Operations

Raw

The typed-operation envelope that builds this scene from empty. POST to /v1/scenes/:id/ops after creating a scene, or pass to dsp_apply_ops. Each op carries a stable id so subsequent runs can update the same elements instead of regenerating.

  • 54 operations
  • 22 createEdge, 8 createFrame, 24 createNode
{
  "schemaVersion": "0.1",
  "errorPolicy": "allOrNothing",
  "revisionMessage": "Initial repo-derived architecture: clients, edge, API, services, data, async, observability, external",
  "ops": [
    {
      "op": "createFrame",
      "id": "clients",
      "title": "Clients",
      "containerType": "generic",
      "children": [
        "web",
        "mobile",
        "api_consumer"
      ],
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 40,
        "y": 30,
        "width": 1080,
        "height": 170
      }
    },
    {
      "op": "createFrame",
      "id": "edge",
      "title": "Edge",
      "containerType": "generic",
      "children": [
        "cdn",
        "waf"
      ],
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 40,
        "y": 220,
        "width": 1080,
        "height": 170
      }
    },
    {
      "op": "createFrame",
      "id": "api",
      "title": "API Layer",
      "containerType": "generic",
      "children": [
        "gateway",
        "auth"
      ],
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 40,
        "y": 410,
        "width": 1080,
        "height": 170
      }
    },
    {
      "op": "createFrame",
      "id": "services",
      "title": "Services",
      "containerType": "generic",
      "children": [
        "users",
        "catalog",
        "orders",
        "payments",
        "notifs"
      ],
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 40,
        "y": 600,
        "width": 1080,
        "height": 170
      }
    },
    {
      "op": "createFrame",
      "id": "async",
      "title": "Async",
      "containerType": "generic",
      "children": [
        "event_bus",
        "workers",
        "scheduler"
      ],
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 40,
        "y": 790,
        "width": 1080,
        "height": 170
      }
    },
    {
      "op": "createFrame",
      "id": "data",
      "title": "Data Plane",
      "containerType": "generic",
      "children": [
        "postgres",
        "redis",
        "search",
        "storage"
      ],
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 40,
        "y": 980,
        "width": 1080,
        "height": 170
      }
    },
    {
      "op": "createFrame",
      "id": "observability",
      "title": "Observability",
      "containerType": "generic",
      "children": [
        "logs",
        "metrics",
        "tracing"
      ],
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 1160,
        "y": 30,
        "width": 220,
        "height": 550
      }
    },
    {
      "op": "createFrame",
      "id": "external",
      "title": "External",
      "containerType": "generic",
      "children": [
        "stripe",
        "sendgrid"
      ],
      "style": {
        "fill": "rgba(255, 255, 255, 0.5)",
        "dash": [
          4,
          4
        ],
        "cornerRadius": 8
      },
      "layout": {
        "mode": "absolute",
        "x": 1160,
        "y": 600,
        "width": 220,
        "height": 540
      }
    },
    {
      "op": "createNode",
      "id": "web",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Web App",
      "icon": "lucide:monitor",
      "layout": {
        "mode": "absolute",
        "x": 230,
        "y": 75,
        "width": 180,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "mobile",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Mobile App",
      "icon": "lucide:smartphone",
      "layout": {
        "mode": "absolute",
        "x": 490,
        "y": 75,
        "width": 180,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "api_consumer",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Public API",
      "icon": "lucide:code",
      "layout": {
        "mode": "absolute",
        "x": 750,
        "y": 75,
        "width": 180,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "cdn",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "CDN",
      "icon": "lucide:globe",
      "layout": {
        "mode": "absolute",
        "x": 330,
        "y": 265,
        "width": 200,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "waf",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "WAF",
      "icon": "lucide:shield",
      "layout": {
        "mode": "absolute",
        "x": 630,
        "y": 265,
        "width": 200,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "gateway",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "API Gateway",
      "icon": "lucide:network",
      "layout": {
        "mode": "absolute",
        "x": 330,
        "y": 455,
        "width": 200,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "auth",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Auth Service",
      "icon": "lucide:lock",
      "layout": {
        "mode": "absolute",
        "x": 630,
        "y": 455,
        "width": 200,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "users",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Users",
      "icon": "lucide:users",
      "layout": {
        "mode": "absolute",
        "x": 110,
        "y": 645,
        "width": 140,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "catalog",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Catalog",
      "icon": "lucide:boxes",
      "layout": {
        "mode": "absolute",
        "x": 310,
        "y": 645,
        "width": 140,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "orders",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Orders",
      "icon": "lucide:package",
      "layout": {
        "mode": "absolute",
        "x": 510,
        "y": 645,
        "width": 140,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "payments",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Payments",
      "icon": "lucide:layers",
      "layout": {
        "mode": "absolute",
        "x": 710,
        "y": 645,
        "width": 140,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "notifs",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Notifications",
      "icon": "lucide:bell",
      "layout": {
        "mode": "absolute",
        "x": 910,
        "y": 645,
        "width": 140,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "event_bus",
      "nodeType": "queue",
      "shape": "roundedRect",
      "label": "Event Bus",
      "icon": "lucide:zap",
      "layout": {
        "mode": "absolute",
        "x": 230,
        "y": 835,
        "width": 180,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "workers",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Workers",
      "icon": "lucide:cpu",
      "layout": {
        "mode": "absolute",
        "x": 490,
        "y": 835,
        "width": 180,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "scheduler",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Scheduler",
      "icon": "lucide:clock",
      "layout": {
        "mode": "absolute",
        "x": 750,
        "y": 835,
        "width": 180,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "postgres",
      "nodeType": "database",
      "shape": "cylinder",
      "label": "PostgreSQL",
      "icon": "lucide:database",
      "layout": {
        "mode": "absolute",
        "x": 170,
        "y": 1010,
        "width": 160,
        "height": 130
      }
    },
    {
      "op": "createNode",
      "id": "redis",
      "nodeType": "database",
      "shape": "cylinder",
      "label": "Redis",
      "icon": "lucide:database",
      "layout": {
        "mode": "absolute",
        "x": 390,
        "y": 1010,
        "width": 160,
        "height": 130
      }
    },
    {
      "op": "createNode",
      "id": "search",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Search Index",
      "icon": "lucide:search",
      "layout": {
        "mode": "absolute",
        "x": 610,
        "y": 1025,
        "width": 160,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "storage",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Object Storage",
      "icon": "lucide:hard-drive",
      "layout": {
        "mode": "absolute",
        "x": 830,
        "y": 1025,
        "width": 160,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "logs",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Logs",
      "icon": "lucide:terminal",
      "layout": {
        "mode": "absolute",
        "x": 1190,
        "y": 90,
        "width": 160,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "metrics",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Metrics",
      "icon": "lucide:bar-chart",
      "layout": {
        "mode": "absolute",
        "x": 1190,
        "y": 240,
        "width": 160,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "tracing",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Tracing",
      "icon": "lucide:activity",
      "layout": {
        "mode": "absolute",
        "x": 1190,
        "y": 390,
        "width": 160,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "stripe",
      "nodeType": "externalSystem",
      "shape": "roundedRect",
      "label": "Payment\nProvider",
      "icon": "lucide:external-link",
      "layout": {
        "mode": "absolute",
        "x": 1190,
        "y": 700,
        "width": 160,
        "height": 110
      }
    },
    {
      "op": "createNode",
      "id": "sendgrid",
      "nodeType": "externalSystem",
      "shape": "roundedRect",
      "label": "Email\nProvider",
      "icon": "lucide:mail",
      "layout": {
        "mode": "absolute",
        "x": 1190,
        "y": 880,
        "width": 160,
        "height": 110
      }
    },
    {
      "op": "createEdge",
      "id": "e_web_cdn",
      "from": {
        "elementId": "web"
      },
      "to": {
        "elementId": "cdn"
      },
      "label": "HTTPS",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_mob_cdn",
      "from": {
        "elementId": "mobile"
      },
      "to": {
        "elementId": "cdn"
      },
      "label": "HTTPS",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_api_cdn",
      "from": {
        "elementId": "api_consumer"
      },
      "to": {
        "elementId": "cdn"
      },
      "label": "HTTPS",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_cdn_waf",
      "from": {
        "elementId": "cdn"
      },
      "to": {
        "elementId": "waf"
      },
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_waf_gw",
      "from": {
        "elementId": "waf"
      },
      "to": {
        "elementId": "gateway"
      },
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_gw_auth",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "auth"
      },
      "label": "validates",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_gw_users",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "users"
      },
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_gw_catalog",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "catalog"
      },
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_gw_orders",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "orders"
      },
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_orders_payments",
      "from": {
        "elementId": "orders"
      },
      "to": {
        "elementId": "payments"
      },
      "label": "triggers",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_users_db",
      "from": {
        "elementId": "users"
      },
      "to": {
        "elementId": "postgres"
      },
      "label": "queries",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_catalog_search",
      "from": {
        "elementId": "catalog"
      },
      "to": {
        "elementId": "search"
      },
      "label": "indexes",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_orders_db",
      "from": {
        "elementId": "orders"
      },
      "to": {
        "elementId": "postgres"
      },
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_orders_redis",
      "from": {
        "elementId": "orders"
      },
      "to": {
        "elementId": "redis"
      },
      "label": "cache",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_orders_evt",
      "from": {
        "elementId": "orders"
      },
      "to": {
        "elementId": "event_bus"
      },
      "label": "publishes",
      "router": "orthogonal",
      "style": {
        "dash": [
          4,
          4
        ]
      }
    },
    {
      "op": "createEdge",
      "id": "e_evt_workers",
      "from": {
        "elementId": "event_bus"
      },
      "to": {
        "elementId": "workers"
      },
      "label": "consumes",
      "router": "orthogonal",
      "style": {
        "dash": [
          4,
          4
        ]
      }
    },
    {
      "op": "createEdge",
      "id": "e_sched_workers",
      "from": {
        "elementId": "scheduler"
      },
      "to": {
        "elementId": "workers"
      },
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_workers_notifs",
      "from": {
        "elementId": "workers"
      },
      "to": {
        "elementId": "notifs"
      },
      "label": "triggers",
      "router": "orthogonal",
      "style": {
        "dash": [
          4,
          4
        ]
      }
    },
    {
      "op": "createEdge",
      "id": "e_workers_storage",
      "from": {
        "elementId": "workers"
      },
      "to": {
        "elementId": "storage"
      },
      "label": "writes",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_payments_stripe",
      "from": {
        "elementId": "payments"
      },
      "to": {
        "elementId": "stripe"
      },
      "label": "charges",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_notifs_sendgrid",
      "from": {
        "elementId": "notifs"
      },
      "to": {
        "elementId": "sendgrid"
      },
      "label": "emails",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_services_obs",
      "from": {
        "elementId": "services"
      },
      "to": {
        "elementId": "observability"
      },
      "label": "telemetry",
      "router": "orthogonal"
    }
  ]
}

Validation

Valid

Captured response from POST /v1/scenes/validate. The platform runs 40+ semantic checks; see the full list in the validation rules reference.

  • 2 diagnostics
  • 2 warnings
  • warning EDGE_ENDPOINT_KIND_INVALID /elements/e_services_obs/from

    Edge 'e_services_obs' connects from 'frame' element 'services' which is not a connectable kind.

  • warning EDGE_ENDPOINT_KIND_INVALID /elements/e_services_obs/to

    Edge 'e_services_obs' connects to 'frame' element 'observability' which is not a connectable kind.

Codes the platform can emit include TEXT_OVERFLOW, CANVAS_AUTO_EXTENDED, EDGE_LABEL_SUPPRESSED_REDUNDANT, EDGE_LABEL_SUPPRESSED_FANIN, EDGE_COLUMN_NOT_FOUND, LABEL_DUPLICATION_DETECTED, LAYOUT_ABSOLUTE_AT_ORIGIN, and MISSING_DIAGRAM_FAMILY. Each diagnostic carries a structured data field with element ids and context an agent can act on programmatically.

Revision diff

Raw

The structural diff between two revisions of the persisted scene — the response shape dsp_diff_scene returns. Stateful diagram evolution is Zindex's strongest differentiator: the same scene id evolves through immutable revisions, each diffable.

Evolution scenario

Three days of changes on `main`: the legacy `search` service was retired and replaced with `search_v2` (now reading directly from object storage rather than from PostgreSQL). Three small revisions condensed into one diff - the daily scheduled run picked up each change as it landed; this view spans the full week's evolution. The same revision history doubles as the engineering team's architecture changelog.

  • Revision 142145
  • +3 added
  • -1 removed
  • ~1 modified

+ Added

  • search_v2
  • e_gateway_search_v2
  • e_search_v2_storage

Removed

  • search

~ Modified

  • e_catalog_search

Raw dsp_diff_scene response

{
  "schemaVersion": "1.0",
  "sceneId": "saas-platform-arch",
  "fromRevision": 142,
  "toRevision": 145,
  "summary": {
    "added": 3,
    "removed": 1,
    "modified": 1
  },
  "added": [
    "search_v2",
    "e_gateway_search_v2",
    "e_search_v2_storage"
  ],
  "removed": [
    "search"
  ],
  "modified": [
    "e_catalog_search"
  ],
  "scenario": "Three days of changes on `main`: the legacy `search` service was retired and replaced with `search_v2` (now reading directly from object storage rather than from PostgreSQL). Three small revisions condensed into one diff - the daily scheduled run picked up each change as it landed; this view spans the full week's evolution. The same revision history doubles as the engineering team's architecture changelog."
}

Want to see this on your own scene? Run the CI recipe below — it calls dsp_diff_scene on every revision change and surfaces a real before / after on every PR.

CI/CD recipe

Scheduled Raw YAML

A complete, runnable GitHub Actions workflow for this example. Drop the YAML into .github/workflows/zindex-living-architecture-docs.yml, add the listed secrets, and the agent runs unattended on every qualifying trigger. Re-comments idempotently using a hidden marker so the PR conversation stays clean across pushes.

Trigger

Daily at 04:00 UTC plus on every push to main. Daily catches drift even without commits; main-branch runs catch architectural changes the moment they merge. PR-time analysis is handled by a separate workflow (see the PR architecture diff example) since per-PR architecture diffs are a different recipe with different output (transient before/after diagrams) than long-lived main-branch state.

  • Schedule: 0 4 * * *

Required secrets

  • ZINDEX_API_KEY required Zindex API key with scene-write scope.
  • ZINDEX_SCENE_ID required Long-lived persisted scene id for this repo's architecture diagram. Never recreate.
  • SLACK_WEBHOOK_URL Optional Slack incoming-webhook URL for the weekly architecture digest. Leave unset to skip digest posting.

Inputs

  • Repository checkout (full clone - fetch-depth: 0)
  • scripts/scan-architecture.mjs (you author this) - emits { components, edges } from convention map
  • scripts/architecture-to-ops.mjs (you author this) - turns scan output into typed ops with stable component-name ids
  • scripts/post-weekly-digest.mjs (optional) - calls dsp_diff_scene with from = revision a week ago and posts to Slack

Outputs

  • docs/architecture/diagram.svg - committed back to the repo so the README can embed it via a stable raw URL
  • A new persisted-scene revision per run (no-op when nothing changed)
  • Optional weekly Slack digest summarising new / retired services + dependencies

GitHub Actions workflow

# Zindex - Living architecture diagram. Scans the repository on a daily
# schedule (and on every push to main) and patches the persisted scene with
# whatever has been added, removed, or rewired since the last run. Over months
# the revision history doubles as an architecture changelog.
#
# Drop into .github/workflows/zindex-architecture.yml.

name: Zindex - Living architecture docs

on:
  push:
    branches: [main]
  schedule:
    - cron: "0 4 * * *"   # Daily 04:00 UTC safety net
  workflow_dispatch:

permissions:
  contents: write     # commit the rendered SVG into docs/architecture.svg
  pull-requests: read

concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  # Don't cancel main-branch runs - let them queue so the architecture
  # changelog stays linear.
  cancel-in-progress: false

env:
  ZINDEX_API_BASE: https://api.zindex.ai

jobs:
  sync-architecture:
    runs-on: ubuntu-latest
    # Don't recurse if we just pushed the rendered SVG ourselves.
    if: github.actor != 'github-actions[bot]'
    steps:
      - uses: actions/checkout@v4
        with:
          fetch-depth: 0    # need full history for the auto-commit

      # 1. Scan the repo. Author scripts/scan-architecture.mjs against your
      #    monorepo conventions: services/*/Dockerfile, apps/*/package.json,
      #    detector regexes for databases (postgres URL strings), queues
      #    (Kafka topic names), external SDK imports.
      - name: Scan repository for components and edges
        run: |
          mkdir -p out
          node scripts/scan-architecture.mjs > out/architecture.json

      - name: Capture current revision
        id: prev_rev
        env:
          ZINDEX_API_KEY: ${{ secrets.ZINDEX_API_KEY }}
          ZINDEX_SCENE_ID: ${{ secrets.ZINDEX_SCENE_ID }}
        run: |
          echo "rev=$(curl -fsSL -H "Authorization: Bearer $ZINDEX_API_KEY" \
            "$ZINDEX_API_BASE/v1/scenes/$ZINDEX_SCENE_ID" | jq -r '.revision')" >> "$GITHUB_OUTPUT"

      - name: Compute applyOps batch
        run: node scripts/architecture-to-ops.mjs out/architecture.json > out/ops.json

      - name: Apply ops
        id: apply
        env:
          ZINDEX_API_KEY: ${{ secrets.ZINDEX_API_KEY }}
          ZINDEX_SCENE_ID: ${{ secrets.ZINDEX_SCENE_ID }}
        run: |
          RESP=$(curl -fsSL -X POST \
            -H "Authorization: Bearer $ZINDEX_API_KEY" \
            -H "Content-Type: application/json" \
            --data-binary @out/ops.json \
            "$ZINDEX_API_BASE/v1/scenes/$ZINDEX_SCENE_ID/ops")
          echo "rev=$(echo "$RESP" | jq -r '.revision')" >> "$GITHUB_OUTPUT"
          echo "applied=$(echo "$RESP" | jq -r '.applied')" >> "$GITHUB_OUTPUT"

      - name: Render
        env:
          ZINDEX_API_KEY: ${{ secrets.ZINDEX_API_KEY }}
          ZINDEX_SCENE_ID: ${{ secrets.ZINDEX_SCENE_ID }}
        run: |
          mkdir -p docs/architecture
          curl -fsSL -X POST \
            -H "Authorization: Bearer $ZINDEX_API_KEY" \
            -H "Content-Type: application/json" \
            -d '{"format":"svg","theme":"clean"}' \
            "$ZINDEX_API_BASE/v1/scenes/$ZINDEX_SCENE_ID/render" \
            | jq -r '.output.content' > docs/architecture/diagram.svg

      # 2. Commit the rendered SVG back into the repo so the diagram lives
      #    next to the code it describes (and the README can embed a stable
      #    raw URL). Skip the commit if nothing changed - git itself is the
      #    no-op detector.
      - name: Commit rendered diagram
        if: steps.apply.outputs.applied != '0'
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add docs/architecture/diagram.svg
          if git diff --cached --quiet; then
            echo "::notice::No diagram changes to commit."
          else
            git commit -m "chore(docs): refresh architecture diagram (rev ${{ steps.apply.outputs.rev }})"
            git push
          fi

      # 3. Weekly digest: if today is Monday, post a short summary to Slack
      #    listing components added / removed / rewired in the past week.
      #    Implement scripts/post-weekly-digest.mjs against your Slack webhook
      #    and SCENE_ID - call dsp_diff_scene with from = revision a week ago.
      - name: Weekly architecture digest (Mondays)
        if: github.event.schedule == '0 4 * * *' && env.IS_MONDAY == '1'
        env:
          IS_MONDAY: ${{ format(github.event.schedule, '') == '' && (fromJSON('false')) || '1' }}
          SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
          ZINDEX_API_KEY: ${{ secrets.ZINDEX_API_KEY }}
          ZINDEX_SCENE_ID: ${{ secrets.ZINDEX_SCENE_ID }}
        run: node scripts/post-weekly-digest.mjs

PR comment template

The bot posts this comment on every triggering PR. The hidden marker <!-- zindex-bot:living-architecture-docs --> lets peter-evans/create-or-update-comment find and overwrite the previous comment instead of appending a new one.

<!-- zindex-bot:living-architecture-docs -->
This recipe runs on `push` to `main` rather than per-PR - see the PR architecture diff example for the per-PR comment workflow. The latest rendered architecture lives at `docs/architecture/diagram.svg` in the repo.

Agent resources

Machine-readable versions of this example. Agents should fetch these rather than scrape the rendered HTML.

  • living-architecture-docs.scene.json Canonical DSP scene Open
  • living-architecture-docs.ops.json Typed-operation envelope that builds the scene Open
  • living-architecture-docs.workflow.json Structured agent workflow (goal, inputs, outputs, steps) Open
  • living-architecture-docs.diff.json Sample dsp_diff_scene response (revision evolution) Open
  • living-architecture-docs.github-actions.yml Runnable GitHub Actions workflow for the CI/CD recipe Open
  • living-architecture-docs.svg Build-time rendered diagram Open
  • living-architecture-docs.md Agent-readable markdown summary Open
  • /examples/index.json Manifest of all examples (cross-linked) Open