← Examples

API dependency map from OpenAPI specs

Generate and maintain a diagram showing which services expose, consume, and depend on which APIs. Built from OpenAPI specs and SDK imports - kept current as services ship.

architecture OpenAPI specs / SDK imports scangenerateupdate

What this example shows

A directed graph of HTTP / SDK dependencies between services, with edge labels showing the actual endpoint or SDK call rather than abstract “depends on” arrows. The “Web App” and “Mobile App” hit the gateway, the gateway fans out to auth / users / orders / catalog APIs, internal services call each other (orders → billing, orders → users, catalog → search), and external SDK integrations (Stripe, SendGrid) sit at the edge of the graph.

When to use it

When your platform has more than ~10 services and “who calls whom” is no longer something a single engineer holds in their head. The diagram is invaluable for: scoping deprecation work (who’d be affected if we sunset Catalog?), auditing third-party data exposure (which services hand data to which vendors?), incident scoping (when Billing is down, what else fails?), and onboarding (here’s the call graph; here’s the entry points).

What the agent does

The agent scans the OpenAPI specs in your repo (and / or grep for SDK imports - import Stripe from "stripe"), builds the directed call graph, then applies typed operations to keep the persisted scene current. New endpoints become new edge labels; new services become nodes; deprecated endpoints disappear from the graph. The diagram is rebuilt on every push that touches an OpenAPI spec or an SDK import.

Edge labels show the actual endpoint where space allows (“POST /v1/login”, “GET /users/:id”) rather than abstract direction. For high-cardinality calls (e.g. dozens of REST endpoints between gateway and a single service), the agent collapses them to a representative (“REST”) to keep the diagram readable.

What the output includes

  • A directed call graph with stable node positioning across runs (same service has the same on-screen location every time).
  • Edge labels showing endpoint paths or SDK names - readers learn the integration shape from the diagram alone.
  • External systems (Stripe, SendGrid, etc.) visually distinct via the externalSystem node type.
  • Auto-layout via layoutStrategy: { algorithm: "hierarchical", direction: "LR" } - the agent never hand-positions; the engine figures it out.
  • A revision history showing how the API surface evolved (which endpoints existed when).

Rendered diagram

API dependency map from OpenAPI specs — 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": "api-dependency-map",
    "title": "API Dependency Map",
    "units": "px",
    "canvas": {
      "width": 1200,
      "height": 600
    }
  },
  "layoutStrategy": {
    "algorithm": "hierarchical",
    "direction": "LR",
    "nodeSpacing": 60,
    "rankSpacing": 130
  },
  "elements": [
    {
      "id": "web",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Web App",
      "icon": "lucide:monitor"
    },
    {
      "id": "mobile",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Mobile App",
      "icon": "lucide:smartphone"
    },
    {
      "id": "gateway",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "API Gateway",
      "icon": "lucide:network"
    },
    {
      "id": "auth_api",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Auth API",
      "icon": "lucide:lock"
    },
    {
      "id": "users_api",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Users API",
      "icon": "lucide:users"
    },
    {
      "id": "orders_api",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Orders API",
      "icon": "lucide:package"
    },
    {
      "id": "catalog_api",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Catalog API",
      "icon": "lucide:boxes"
    },
    {
      "id": "billing_api",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Billing API",
      "icon": "lucide:credit-card"
    },
    {
      "id": "search_api",
      "kind": "node",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Search API",
      "icon": "lucide:search"
    },
    {
      "id": "stripe",
      "kind": "node",
      "nodeType": "externalSystem",
      "shape": "roundedRect",
      "label": "Stripe",
      "icon": "lucide:external-link"
    },
    {
      "id": "sendgrid",
      "kind": "node",
      "nodeType": "externalSystem",
      "shape": "roundedRect",
      "label": "SendGrid",
      "icon": "lucide:mail"
    },
    {
      "id": "e_web_gw",
      "kind": "edge",
      "from": {
        "elementId": "web"
      },
      "to": {
        "elementId": "gateway"
      },
      "label": "REST",
      "router": "orthogonal"
    },
    {
      "id": "e_mob_gw",
      "kind": "edge",
      "from": {
        "elementId": "mobile"
      },
      "to": {
        "elementId": "gateway"
      },
      "label": "REST",
      "router": "orthogonal"
    },
    {
      "id": "e_gw_auth",
      "kind": "edge",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "auth_api"
      },
      "label": "POST /v1/login",
      "router": "orthogonal"
    },
    {
      "id": "e_gw_users",
      "kind": "edge",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "users_api"
      },
      "label": "GET /v1/users",
      "router": "orthogonal"
    },
    {
      "id": "e_gw_orders",
      "kind": "edge",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "orders_api"
      },
      "label": "REST",
      "router": "orthogonal"
    },
    {
      "id": "e_gw_catalog",
      "kind": "edge",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "catalog_api"
      },
      "label": "REST",
      "router": "orthogonal"
    },
    {
      "id": "e_orders_billing",
      "kind": "edge",
      "from": {
        "elementId": "orders_api"
      },
      "to": {
        "elementId": "billing_api"
      },
      "label": "POST /charge",
      "router": "orthogonal"
    },
    {
      "id": "e_orders_users",
      "kind": "edge",
      "from": {
        "elementId": "orders_api"
      },
      "to": {
        "elementId": "users_api"
      },
      "label": "GET /users/:id",
      "router": "orthogonal"
    },
    {
      "id": "e_catalog_search",
      "kind": "edge",
      "from": {
        "elementId": "catalog_api"
      },
      "to": {
        "elementId": "search_api"
      },
      "label": "POST /index",
      "router": "orthogonal"
    },
    {
      "id": "e_billing_stripe",
      "kind": "edge",
      "from": {
        "elementId": "billing_api"
      },
      "to": {
        "elementId": "stripe"
      },
      "label": "Stripe SDK",
      "router": "orthogonal"
    },
    {
      "id": "e_orders_sendgrid",
      "kind": "edge",
      "from": {
        "elementId": "orders_api"
      },
      "to": {
        "elementId": "sendgrid"
      },
      "label": "SendGrid SDK",
      "router": "orthogonal"
    }
  ]
}

Agent workflow

Maintain a current diagram of which services expose, consume, and depend on which APIs by parsing OpenAPI specs and SDK imports across the repository, then patching the persisted Zindex scene each time a service ships.

Inputs

  • Glob of OpenAPI spec files in the monorepo (e.g. services/*/openapi.yaml)
  • Glob of source files that import generated SDKs (used to derive consumer→provider edges)
  • Existing Zindex scene id (stored as a repo secret)
  • Zindex API key with scene-write scope

Outputs

  • Updated persisted scene with one node per service and one edge per cross-service call
  • Rendered SVG showing the current API topology
  • Revision diff highlighting newly added consumers / providers since the last run
  • Optional: PR comment when the topology changes meaningfully (new service added, edge removed)
  1. 01

    Create or fetch the persisted scene

    On first run, create a scene with hierarchical LR layoutStrategy. On subsequent runs, fetch the existing scene by id to read the current revision and elements.

    dsp_create_scene POST /v1/scenes
  2. 02

    Parse OpenAPI specs

    Walk every services/*/openapi.yaml in the monorepo. Each spec becomes a service node; emit operationIds + paths so they can later become edge labels (e.g. 'POST /v1/login'). Do not parse free-text README links - the spec is the source of truth.

  3. 03

    Derive consumer→provider edges

    Scan source files for imports of generated SDKs (e.g. `import { usersClient } from '@acme/sdk-users'`). Each import is evidence that the importing service consumes the imported API. Map the SDK package name back to the spec → an edge from consumer to provider, labeled with the most-frequently-called operationId.

  4. 04

    Diff parsed topology against the persisted scene

    Compare the parsed (services, edges) tuple to what's currently in the scene. New services become createNode ops; removed services become deleteElement ops; changed labels become updateEdge ops. Always reuse stable element ids (the service name) so a renamed label stays on the same edge.

    dsp_get_scene GET /v1/scenes/${SCENE_ID}
  5. 05

    Apply the operation batch

    Send the diff as one applyOps batch with errorPolicy=allOrNothing. Reuse stable element ids - `users_api`, `orders_api`, `e_orders_users` - so renames update the existing element rather than create a duplicate.

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

    Validate the new revision

    Confirm the topology is still valid: every edge endpoint resolves to a node, no duplicate edges between the same pair without distinct labels.

    dsp_validate_scene POST /v1/scenes/validate
  7. 07

    Render the updated diagram

    Render to SVG. The hierarchical LR layout naturally separates clients (left), gateway (centre), and provider APIs (right); cloud-icon services render icon-only and read cleanly inside the layout.

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

    Publish the rendered SVG to the docs site

    Upload the rendered SVG to the docs site (or commit to the repo's docs/ directory). Optionally, when the topology diff contains added or removed services, post a Slack message to the platform channel so the team is aware of the new dependency.

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_create_scene Create or fetch the persisted scene
  2. 02 dsp_get_scene Diff parsed topology against the persisted scene
  3. 03 dsp_apply_ops Apply the operation batch
  4. 04 dsp_validate_scene Validate the new revision
  5. 05 dsp_render_scene Render the updated diagram

Unique tools used: dsp_create_scene, dsp_get_scene, dsp_apply_ops, dsp_validate_scene, dsp_render_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 platform-documentation agent. Your job is to keep an API dependency-map diagram of an organisation's microservices in sync with the actual code, by parsing OpenAPI specs and SDK imports across the monorepo on a schedule (and on every PR that touches a service spec).

The persisted Zindex scene id is `${SCENE_ID}`; it already exists. Treat it as the canonical, mutable, immutable-revisioned source of truth for the API topology. Each run computes the smallest valid set of typed operations that move the scene from its current revision to one that matches the current code.

Workflow on every run:

1. Walk every `services/*/openapi.yaml` in the monorepo. Each spec corresponds to one service node - use the spec's `info.title` (or the directory name) as the stable element id. Capture the operationIds and paths so they can become edge labels.

2. Walk every source file in the monorepo and look for imports of generated SDK packages (`@acme/sdk-users`, `@acme/sdk-orders`, …). Each import is evidence that the importing service consumes the imported API. Build a `(consumer_service_id, provider_service_id, operationId)` tuple for the most-frequently-called operation between each pair.

3. Call `dsp_get_scene({ sceneId: "${SCENE_ID}" })` to read the current revision and elements. Diff what you parsed against what is persisted: new services → `createNode`, removed services → `deleteElement`, new edges → `createEdge`, removed edges → `deleteElement`, changed edge labels → `updateEdge`.

4. Call `dsp_apply_ops` with one batch. Set `errorPolicy: "allOrNothing"`. Reuse stable element ids - `users_api`, `orders_api`, `e_orders_billing` - so a renamed service or relabelled edge stays on the same element rather than producing a delete+create. Pass a meaningful `revisionMessage` like "add billing-api spec; new e_orders_billing edge from POST /charge".

5. Call `dsp_validate_scene` and resolve any `EDGE_LABEL_SUPPRESSED_REDUNDANT` and `LABEL_DUPLICATION_DETECTED` warnings. (`CANVAS_AUTO_EXTENDED` is informational - let the layout engine size the canvas as the topology grows.) `EDGE_COLUMN_NOT_FOUND` should not appear here; if it does, an edge is incorrectly using ER-family `column` anchors and needs `from/to.elementId` only.

6. Call `dsp_render_scene({ format: "svg", theme: "clean" })` and publish the rendered SVG to the docs site (or commit it to `docs/architecture/api-topology.svg`). The watermark stamps scene-id + revision + date, so the published artifact is traceable back to the persisted scene.

7. Call `dsp_diff_scene({ from: PREV_REVISION, to: NEW_REVISION })`. If the diff contains added or removed services (new providers or consumers), post a short Slack message to the platform channel summarising the change so the team is aware of the new cross-service dependency.

Hard rules: never hand-edit the rendered SVG; always edit the scene with `dsp_apply_ops` and re-render. Never regenerate the scene from scratch; always patch with stable ids - the topology is a long-lived, evolving graph, and stable ids are what make revision history meaningful. Treat the OpenAPI specs as the source of truth; if a spec contradicts a free-text comment elsewhere, follow the spec.

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 POST /v1/scenes

    On first run, create a scene with hierarchical LR layoutStrategy. On subsequent runs, fetch the existing scene by id to read the current revision and elements.

    Example response

    { "sceneId": "sc_api_topo", "revision": 1 }
  2. 02 GET /v1/scenes/${SCENE_ID}

    Compare the parsed (services, edges) tuple to what's currently in the scene. New services become createNode ops; removed services become deleteElement ops; changed labels become updateEdge ops. Always reuse stable element ids (the service name) so a renamed label stays on the same edge.

    Example response

    { "sceneId": "sc_api_topo", "revision": 47, "elements": ["..."] }
  3. 03 POST /v1/scenes/${SCENE_ID}/ops

    Send the diff as one applyOps batch with errorPolicy=allOrNothing. Reuse stable element ids - `users_api`, `orders_api`, `e_orders_users` - so renames update the existing element rather than create a duplicate.

    Example response

    { "sceneId": "sc_api_topo", "revision": 48, "applied": 6 }
  4. 04 POST /v1/scenes/validate

    Confirm the topology is still valid: every edge endpoint resolves to a node, no duplicate edges between the same pair without distinct labels.

  5. 05 POST /v1/scenes/${SCENE_ID}/render

    Render to SVG. The hierarchical LR layout naturally separates clients (left), gateway (centre), and provider APIs (right); cloud-icon services render icon-only and read cleanly inside the layout.

    Example request body

    { "format": "svg", "theme": "clean" }

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.

  • 22 operations
  • 11 createEdge, 11 createNode
{
  "schemaVersion": "0.1",
  "errorPolicy": "allOrNothing",
  "revisionMessage": "Initial API dependency map: clients, gateway, internal APIs, external systems",
  "ops": [
    {
      "op": "createNode",
      "id": "web",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Web App",
      "icon": "lucide:monitor"
    },
    {
      "op": "createNode",
      "id": "mobile",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Mobile App",
      "icon": "lucide:smartphone"
    },
    {
      "op": "createNode",
      "id": "gateway",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "API Gateway",
      "icon": "lucide:network"
    },
    {
      "op": "createNode",
      "id": "auth_api",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Auth API",
      "icon": "lucide:lock"
    },
    {
      "op": "createNode",
      "id": "users_api",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Users API",
      "icon": "lucide:users"
    },
    {
      "op": "createNode",
      "id": "orders_api",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Orders API",
      "icon": "lucide:package"
    },
    {
      "op": "createNode",
      "id": "catalog_api",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Catalog API",
      "icon": "lucide:boxes"
    },
    {
      "op": "createNode",
      "id": "billing_api",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Billing API",
      "icon": "lucide:credit-card"
    },
    {
      "op": "createNode",
      "id": "search_api",
      "nodeType": "service",
      "shape": "roundedRect",
      "label": "Search API",
      "icon": "lucide:search"
    },
    {
      "op": "createNode",
      "id": "stripe",
      "nodeType": "externalSystem",
      "shape": "roundedRect",
      "label": "Stripe",
      "icon": "lucide:external-link"
    },
    {
      "op": "createNode",
      "id": "sendgrid",
      "nodeType": "externalSystem",
      "shape": "roundedRect",
      "label": "SendGrid",
      "icon": "lucide:mail"
    },
    {
      "op": "createEdge",
      "id": "e_web_gw",
      "from": {
        "elementId": "web"
      },
      "to": {
        "elementId": "gateway"
      },
      "label": "REST",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_mob_gw",
      "from": {
        "elementId": "mobile"
      },
      "to": {
        "elementId": "gateway"
      },
      "label": "REST",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_gw_auth",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "auth_api"
      },
      "label": "POST /v1/login",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_gw_users",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "users_api"
      },
      "label": "GET /v1/users",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_gw_orders",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "orders_api"
      },
      "label": "REST",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_gw_catalog",
      "from": {
        "elementId": "gateway"
      },
      "to": {
        "elementId": "catalog_api"
      },
      "label": "REST",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_orders_billing",
      "from": {
        "elementId": "orders_api"
      },
      "to": {
        "elementId": "billing_api"
      },
      "label": "POST /charge",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_orders_users",
      "from": {
        "elementId": "orders_api"
      },
      "to": {
        "elementId": "users_api"
      },
      "label": "GET /users/:id",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_catalog_search",
      "from": {
        "elementId": "catalog_api"
      },
      "to": {
        "elementId": "search_api"
      },
      "label": "POST /index",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_billing_stripe",
      "from": {
        "elementId": "billing_api"
      },
      "to": {
        "elementId": "stripe"
      },
      "label": "Stripe SDK",
      "router": "orthogonal"
    },
    {
      "op": "createEdge",
      "id": "e_orders_sendgrid",
      "from": {
        "elementId": "orders_api"
      },
      "to": {
        "elementId": "sendgrid"
      },
      "label": "SendGrid SDK",
      "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.

  • 0 diagnostics

Scene validates with no diagnostics. Agents that produce scenes like this can ship the rendered SVG without a recovery loop.

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

A new Search API service ships with its own OpenAPI spec; the gateway now routes search queries directly to it. The legacy catalog→search edge label changed to reflect the new operationId. Reviewing the diff is faster than re-reading three OpenAPI files.

  • Revision 4748
  • +2 added
  • -0 removed
  • ~1 modified

+ Added

  • search_api
  • e_gw_search

~ Modified

  • e_catalog_search

Raw dsp_diff_scene response

{
  "schemaVersion": "1.0",
  "sceneId": "api-dependency-map",
  "fromRevision": 47,
  "toRevision": 48,
  "summary": {
    "added": 2,
    "removed": 0,
    "modified": 1
  },
  "added": [
    "search_api",
    "e_gw_search"
  ],
  "removed": [],
  "modified": [
    "e_catalog_search"
  ],
  "scenario": "A new Search API service ships with its own OpenAPI spec; the gateway now routes search queries directly to it. The legacy catalog→search edge label changed to reflect the new operationId. Reviewing the diff is faster than re-reading three OpenAPI files."
}

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

Pull request + scheduled Raw YAML

A complete, runnable GitHub Actions workflow for this example. Drop the YAML into .github/workflows/zindex-api-dependency-map.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

Runs on every PR that touches a service spec or a published SDK package, plus a weekly scheduled run on Monday 09:00 UTC. The schedule catches drift from imports that didn't move a spec - e.g. a new SDK consumer added on the consumer side without a spec change.

  • Schedule: 0 9 * * 1
  • Path filters: services/*/openapi.yaml, services/*/openapi.json, packages/*/package.json

Required secrets

  • ZINDEX_API_KEY required Zindex API key with scene-write scope.
  • ZINDEX_SCENE_ID required Long-lived persisted scene id for the API topology. Create once via dsp_create_scene; reuse across all runs.

Inputs

  • scripts/parse-topology.mjs (you author this) - walks services/*/openapi.* + package.json import graph, emits { services, edges }
  • scripts/topology-to-ops.mjs (you author this) - converts topology JSON to a Zindex applyOps batch with stable service-name ids

Outputs

  • out/diagram.svg - rendered topology diagram, uploaded as a 30-day workflow artifact
  • out/diff.json - structural diff vs previous revision
  • PR comment with topology changes and artifact link (PR runs only)
  • A new persisted-scene revision per run; the weekly run keeps the diagram fresh on main even if no PR landed that week

GitHub Actions workflow

# Zindex - API dependency map. Maintains a service-to-service dependency graph
# from OpenAPI specs and SDK imports. Runs on every PR touching a service spec
# AND on a weekly schedule (so newly-imported SDKs picked up by static analysis
# don't have to wait for a spec change to surface in the diagram).
#
# Drop into .github/workflows/zindex-api-topology.yml.

name: Zindex - API dependency map

on:
  pull_request:
    paths:
      - "services/*/openapi.yaml"
      - "services/*/openapi.json"
      - "packages/*/package.json"
  schedule:
    - cron: "0 9 * * 1"   # Monday 09:00 UTC weekly catch-up
  workflow_dispatch:

permissions:
  contents: read
  pull-requests: write

concurrency:
  group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
  cancel-in-progress: ${{ github.event_name == 'pull_request' }}

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

jobs:
  sync-topology:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # 1. Walk every services/*/openapi.* and emit a normalised
      #    { services: [...], operations: [...] } JSON. Then walk
      #    package.json files and import statements to derive consumer→provider
      #    edges. This step is project-specific - implement parse-topology.mjs
      #    against your monorepo's conventions.
      - name: Derive topology from specs + imports
        run: |
          mkdir -p out
          node scripts/parse-topology.mjs > out/topology.json
          echo "::notice::Detected $(jq '.services | length' out/topology.json) services, $(jq '.edges | length' out/topology.json) cross-service edges"

      # 2. Capture current revision (for the diff later).
      - 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"

      # 3. Compute typed-operation batch. Reuse stable element ids derived
      #    from service names so a renamed service updates the existing node
      #    rather than producing a delete + create pair.
      - name: Compute applyOps batch
        run: node scripts/topology-to-ops.mjs out/topology.json > out/ops.json

      # 4. Apply atomically.
      - name: Apply ops to persisted scene
        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"

      # 5. Render.
      - name: Render scene
        env:
          ZINDEX_API_KEY: ${{ secrets.ZINDEX_API_KEY }}
          ZINDEX_SCENE_ID: ${{ secrets.ZINDEX_SCENE_ID }}
        run: |
          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' > out/diagram.svg

      # 6. Diff against previous revision.
      - name: Diff revisions
        id: diff
        if: steps.apply.outputs.rev != steps.prev_rev.outputs.rev
        env:
          ZINDEX_API_KEY: ${{ secrets.ZINDEX_API_KEY }}
          ZINDEX_SCENE_ID: ${{ secrets.ZINDEX_SCENE_ID }}
          PREV: ${{ steps.prev_rev.outputs.rev }}
          NEW: ${{ steps.apply.outputs.rev }}
        run: |
          curl -fsSL \
            -H "Authorization: Bearer $ZINDEX_API_KEY" \
            "$ZINDEX_API_BASE/v1/scenes/$ZINDEX_SCENE_ID/diff?from=$PREV&to=$NEW" \
            > out/diff.json
          echo "added=$(jq -r '.summary.added' out/diff.json)" >> "$GITHUB_OUTPUT"
          echo "removed=$(jq -r '.summary.removed' out/diff.json)" >> "$GITHUB_OUTPUT"
          echo "modified=$(jq -r '.summary.modified' out/diff.json)" >> "$GITHUB_OUTPUT"

      - uses: actions/upload-artifact@v4
        id: upload
        with:
          name: api-dependency-map-svg
          path: out/diagram.svg
          retention-days: 30

      # 7. PR comment - only on pull_request runs (the weekly schedule run
      #    has no PR to comment on; it just refreshes the artifact).
      - uses: peter-evans/find-comment@v3
        if: github.event_name == 'pull_request'
        id: find_comment
        with:
          issue-number: ${{ github.event.pull_request.number }}
          comment-author: "github-actions[bot]"
          body-includes: "<!-- zindex-bot:api-dependency-map -->"

      - uses: peter-evans/create-or-update-comment@v4
        if: github.event_name == 'pull_request'
        with:
          comment-id: ${{ steps.find_comment.outputs.comment-id }}
          issue-number: ${{ github.event.pull_request.number }}
          edit-mode: replace
          body: |
            <!-- zindex-bot:api-dependency-map -->
            ### API dependency map updated · revision ${{ steps.apply.outputs.rev }}

            | | |
            |---|---|
            | Topology changes | +${{ steps.diff.outputs.added || '0' }} / -${{ steps.diff.outputs.removed || '0' }} / ~${{ steps.diff.outputs.modified || '0' }} |
            | Revision | ${{ steps.prev_rev.outputs.rev }} → ${{ steps.apply.outputs.rev }} |

            <details><summary>Download rendered SVG</summary>

            ${{ steps.upload.outputs.artifact-url }}

            </details>

            <sub>Rendered by Zindex · scene `${{ secrets.ZINDEX_SCENE_ID }}`</sub>

PR comment template

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

<!-- zindex-bot:api-dependency-map -->
### API dependency map updated · revision ${NEW_REVISION}

| | |
|---|---|
| Topology changes | +${ADDED} / -${REMOVED} / ~${MODIFIED} |
| Revision | ${PREV_REVISION} → ${NEW_REVISION} |

<details><summary>Download rendered SVG</summary>

${ARTIFACT_URL}

</details>

<sub>Rendered by Zindex · scene `${SCENE_ID}`</sub>

Agent resources

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

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