← Examples

Request flow diagram from handler code

Maintain a current sequence diagram of your flagship endpoint's request lifecycle. The agent walks the handler's async/await calls and external integrations to keep the diagram in sync as the code evolves.

sequence Handler source code (TypeScript / Python / Go) scangenerateupdate

What this example shows

A canonical sequence diagram of an HTTP /checkout POST handler - the time-ordered interactions between the Client, the Checkout Service, and every collaborator the handler touches: Auth, Redis Cache, Cart, Inventory, the Stripe Payment Provider, and a Notification Queue. The diagram includes a sequence-fragment alt around the payment retry path, a note explaining Stripe’s idempotency-key convention, an async fire-and-forget publish to the notification queue, and a self-message for the response-build step. The “before” scene captures the handler before a recent latency-improvement PR (no Redis cache; synchronous notification HTTP call). The “after” scene reflects the handler post-PR (cache layer added, retry logic around payment, notifications moved to a queue). The diff between revisions is the deliverable a reviewer cares about - they see what the new code does differently without reading the PR diff line by line.

When to use it

Reach for this pattern when your team has a flagship endpoint (checkout, login, payment, document-upload, anything mission-critical) whose internal call graph is hard to hold in one engineer’s head. The diagram becomes the team’s authoritative answer to “what happens when a request hits this endpoint” - used in onboarding (new engineers learn the flow without grep-walking the codebase), incident response (on-call sees which collaborator to investigate), architecture reviews (anyone proposing a change can show the structural diff alongside the code diff), and compliance audits (security reviewers see exactly which third parties touch which data, in what order). Sequence diagrams are also the right shape when the AUDIT TRAIL matters as much as the structure - every revision is a snapshot of how the request flowed through the system at that point in time.

What the agent does

The agent watches the handler source file and its direct dependencies. On each PR that touches them, it walks the handler’s async/await chain, identifies the participants (services called via SDK or HTTP, queues published to, caches read, the database, external APIs), and applies typed operations to the persisted scene: createNode for new participants, createEdge with edgeType: "sequence.message" / "sequence.reply" / "sequence.async" for new interactions, createFrame with containerType: "fragment" for branching paths (alt for try/catch retry, opt for conditional calls, loop for iterating), and sequence.note nodes for implementation notes worth preserving in the diagram (idempotency keys, retry budgets, timeouts).

The flow is incremental - the persisted scene preserves stable lifeline and edge IDs across PRs, so adding one collaborator doesn’t reshuffle the whole diagram. The agent renders the updated scene, diffs the previous revision against the new one, and posts the visual diff (plus a structural-diff bullet list) as a PR comment.

What the output includes

  • A diagramFamily: "sequence" scene with one sequence.actor per external initiator (the Client) and one sequence.lifeline per internal collaborator.
  • Messages typed correctly: sequence.message for synchronous calls, sequence.reply for return values (rendered with dashed open arrows), sequence.async for fire-and-forget queue publishes (rendered with open arrowheads), sequence.create for object instantiation in OO codebases.
  • Combined fragments with extensions.operator: "alt" | "opt" | "loop" | "par" and operand dividers for branching paths - the canonical example here is the [charge succeeded] / [retry on 5xx] alt around the payment block.
  • Sequence notes anchored OVER a single lifeline (PlantUML / Mermaid convention) for inline implementation notes - visible in the diagram next to the message they describe.
  • Self-messages rendered as rounded U-loops for handler-internal computations the agent thinks are worth surfacing (validation, response building).
  • A revision diff showing exactly which lifelines, messages, fragments, or notes changed since the last run - the value-add over a one-shot sequence diagram.

What this example does not aim to do

This is not a tracing-driven recipe. It works from source code, not OpenTelemetry / Jaeger / Honeycomb spans. A trace-driven variant would identify the canonical request flow from real production traffic - useful for catching drift between the documented flow and what actually happens in production - but is a separate recipe with its own observability-stack dependencies. Build whichever fits your team; the source-code recipe is the lower-friction starting point.

Rendered diagram

Request flow diagram from handler code — 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": "sequence",
  "scene": {
    "id": "request-flow-from-handler",
    "title": "Checkout request flow",
    "units": "px",
    "canvas": {
      "width": 1900,
      "height": 1150
    }
  },
  "elements": [
    {
      "id": "client",
      "kind": "node",
      "nodeType": "sequence.actor",
      "shape": "rect",
      "label": "Client"
    },
    {
      "id": "checkout",
      "kind": "node",
      "nodeType": "sequence.lifeline",
      "shape": "rect",
      "label": "Checkout Service"
    },
    {
      "id": "auth",
      "kind": "node",
      "nodeType": "sequence.lifeline",
      "shape": "rect",
      "label": "Auth Service"
    },
    {
      "id": "cache",
      "kind": "node",
      "nodeType": "sequence.lifeline",
      "shape": "rect",
      "label": "Redis Cache"
    },
    {
      "id": "inventory",
      "kind": "node",
      "nodeType": "sequence.lifeline",
      "shape": "rect",
      "label": "Inventory Service"
    },
    {
      "id": "payment",
      "kind": "node",
      "nodeType": "sequence.lifeline",
      "shape": "rect",
      "label": "Payment Provider\n(Stripe)"
    },
    {
      "id": "notify",
      "kind": "node",
      "nodeType": "sequence.lifeline",
      "shape": "rect",
      "label": "Notification Queue"
    },
    {
      "id": "m_post",
      "kind": "edge",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "client"
      },
      "to": {
        "elementId": "checkout"
      },
      "label": "POST /checkout"
    },
    {
      "id": "m_verify",
      "kind": "edge",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "auth"
      },
      "label": "verifyToken(jwt)"
    },
    {
      "id": "m_verify_ok",
      "kind": "edge",
      "edgeType": "sequence.reply",
      "from": {
        "elementId": "auth"
      },
      "to": {
        "elementId": "checkout"
      },
      "label": "user_id"
    },
    {
      "id": "m_cache_get",
      "kind": "edge",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "cache"
      },
      "label": "GET cart:{user_id}"
    },
    {
      "id": "m_cache_hit",
      "kind": "edge",
      "edgeType": "sequence.reply",
      "from": {
        "elementId": "cache"
      },
      "to": {
        "elementId": "checkout"
      },
      "label": "cart (hit)"
    },
    {
      "id": "m_reserve",
      "kind": "edge",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "inventory"
      },
      "label": "reserve(items)"
    },
    {
      "id": "m_reserved",
      "kind": "edge",
      "edgeType": "sequence.reply",
      "from": {
        "elementId": "inventory"
      },
      "to": {
        "elementId": "checkout"
      },
      "label": "reservation_id"
    },
    {
      "id": "note_payment",
      "kind": "node",
      "nodeType": "sequence.note",
      "shape": "rect",
      "label": "Stripe charge with\nidempotency key",
      "extensions": {
        "anchor": "payment"
      }
    },
    {
      "id": "frag_charge",
      "kind": "frame",
      "containerType": "fragment",
      "title": "[charge succeeded]",
      "children": [
        "m_charge",
        "m_charge_ok",
        "m_charge_retry",
        "m_charge_retry_ok"
      ],
      "layout": {
        "mode": "absolute",
        "x": 270,
        "y": 545,
        "width": 1410,
        "height": 190
      },
      "extensions": {
        "operator": "alt",
        "operands": [
          {
            "y": 635,
            "guard": "retry on 5xx (exp backoff: 1s, 2s, 4s)"
          }
        ]
      }
    },
    {
      "id": "m_charge",
      "kind": "edge",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "payment"
      },
      "label": "charge($total, idempotency_key)"
    },
    {
      "id": "m_charge_ok",
      "kind": "edge",
      "edgeType": "sequence.reply",
      "from": {
        "elementId": "payment"
      },
      "to": {
        "elementId": "checkout"
      },
      "label": "charge_id"
    },
    {
      "id": "m_charge_retry",
      "kind": "edge",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "payment"
      },
      "label": "charge (retry)"
    },
    {
      "id": "m_charge_retry_ok",
      "kind": "edge",
      "edgeType": "sequence.reply",
      "from": {
        "elementId": "payment"
      },
      "to": {
        "elementId": "checkout"
      },
      "label": "charge_id (after retry)"
    },
    {
      "id": "m_commit",
      "kind": "edge",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "inventory"
      },
      "label": "commit(reservation_id)"
    },
    {
      "id": "m_notify",
      "kind": "edge",
      "edgeType": "sequence.async",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "notify"
      },
      "label": "publish(order_placed)"
    },
    {
      "id": "m_self_validate",
      "kind": "edge",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "checkout"
      },
      "label": "build response"
    },
    {
      "id": "m_response",
      "kind": "edge",
      "edgeType": "sequence.reply",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "client"
      },
      "label": "200 OK + receipt"
    }
  ]
}

Agent workflow

Maintain a current sequence diagram of an HTTP endpoint's request flow by walking the handler's async/await chain and external integrations, then patching the persisted Zindex scene each time a relevant code path changes.

Inputs

  • Handler file path (e.g. apps/api/src/routes/checkout.ts) plus its directly-imported service modules
  • Optional: monorepo paths to client SDK definitions for external integrations (Stripe, Twilio, etc.) so the agent emits accurate provider-side lifelines
  • Existing Zindex scene id (stored as a repo secret)
  • Zindex API key with scene-write scope

Outputs

  • Updated persisted scene with one lifeline per participant, one message per service interaction, and combined-fragment frames for branching paths
  • Rendered SVG showing the canonical request flow at the current revision
  • Revision diff highlighting added/removed lifelines, new fragments, message-type changes (sync→async, etc.)
  • PR comment summarising the structural change and linking to the rendered SVG
  1. 01

    Create or fetch the persisted scene

    On first run, create a scene with diagramFamily: "sequence". On subsequent runs, fetch the existing scene by id to read the current revision and elements.

    dsp_create_scene POST /v1/scenes
  2. 02

    Walk the handler's call graph

    Parse the handler source file (TypeScript via the TS compiler API, Python via ast, Go via go/parser). Identify every awaited service call, queue publish, cache read/write, DB query, and external API request - each becomes a (caller, callee, operationName, kind) tuple. Distinguish synchronous calls (await response) from asynchronous publishes (fire-and-forget queue/topic). Branching constructs (try/catch with retry, if/else with parallel paths) become fragment metadata. Self-message candidates: handler-internal helpers that are visually meaningful (validation, response shaping).

  3. 03

    Diff parsed flow against the persisted scene

    Compare parsed lifelines + messages + fragments against the current scene. New participants → createNode (sequence.lifeline / sequence.actor). New interactions → createEdge with appropriate edgeType. New branching paths → createFrame with containerType: "fragment" and the right operator (alt | opt | loop | par). Use stable element ids derived from service names so a renamed lifeline stays the same element.

    dsp_get_scene GET /v1/scenes/${SCENE_ID}
  4. 04

    Apply the operation batch

    Send the diff as one applyOps batch with errorPolicy=allOrNothing. The agent uses stable lifeline ids (service.kebab-case) so a renamed service updates the existing lifeline rather than creating a duplicate. Set a meaningful revisionMessage (e.g. "add Redis cache lifeline; move notifications to async publish").

    dsp_apply_ops POST /v1/scenes/${SCENE_ID}/applyOps
  5. 05

    Validate the updated scene

    Resolve any LABEL_DUPLICATION_DETECTED diagnostics - sequence diagrams allow same-label messages (different timestamps), but suspicious duplicates (e.g. two replies labelled the same on the same source) are usually mis-typed. CANVAS_AUTO_EXTENDED is informational; the engine sizes the canvas as the flow grows.

    dsp_validate_scene POST /v1/scenes/validate
  6. 06

    Render to SVG

    Render the updated scene at the new revision. The watermark stamps scene-id + revision + date so the rendered artifact is traceable back to the persisted scene. Theme defaults to clean; pass theme: "dark" if your docs site uses a dark canvas.

    dsp_render_scene POST /v1/scenes/${SCENE_ID}/render
  7. 07

    Diff against the previous revision

    Get a structural summary of what changed between PREV_REVISION and the new revision: added/removed/modified lifelines and messages. Drives the PR comment.

    dsp_diff_scene GET /v1/scenes/${SCENE_ID}/diff?from=${PREV_REVISION}&to=${NEW_REVISION}
  8. 08

    Post PR comment

    Post a structural-diff summary as a PR comment with the rendered SVG attached as a workflow artifact. The reviewer sees what the new code does differently without reading the full code diff.

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 flow against the persisted scene
  3. 03 dsp_apply_ops Apply the operation batch
  4. 04 dsp_validate_scene Validate the updated scene
  5. 05 dsp_render_scene Render to SVG
  6. 06 dsp_diff_scene Diff against the previous revision

Unique tools used: dsp_create_scene, 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 code-documentation agent. Your job is to keep a sequence diagram of an HTTP endpoint's request flow in sync with the actual handler code, by parsing the handler source on every PR that touches the relevant paths.

The persisted Zindex scene id is `${SCENE_ID}`; it already exists. Treat it as the canonical, immutable-revisioned source of truth for the request flow. 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. Parse the handler source file at `${HANDLER_PATH}`. Walk the function's `async`/`await` chain (TypeScript via the TS compiler API, Python via `ast`, Go via `go/parser`). For each awaited call, identify the callee (service / collaborator) and the kind: synchronous (the handler waits on a response - emit `sequence.message` + `sequence.reply`), asynchronous (the handler does not await a response - emit `sequence.async`), or self-handler (an internal helper worth surfacing - emit a self-`sequence.message` from the handler back to itself).

2. Identify branching paths. A `try { primary() } catch { retry() }` becomes an `alt` fragment with two operands ("primary" / "retry"). An `if (cond) { sometimes() }` becomes an `opt` fragment. A `for (item of items) { process(item) }` becomes a `loop` fragment. `Promise.all([a(), b()])` becomes a `par` fragment.

3. Annotate non-obvious behaviour with `sequence.note` nodes anchored OVER the relevant lifeline. Idempotency keys, retry budgets, timeouts, side-effect ordering - anything a future engineer or auditor would want to read alongside the code. Notes are NOT a substitute for code comments; use them for structural facts the diagram alone wouldn't convey.

4. Call `dsp_get_scene({ sceneId: "${SCENE_ID}" })` to read the current revision and elements. Diff what you parsed against what is persisted: new lifelines → `createNode`, removed lifelines → `deleteElement`, new messages → `createEdge`, message-kind changes (sync → async) → `updateEdge`. Keep stable lifeline ids (kebab-case service name) so renames update the existing lifeline rather than producing a delete + create.

5. Call `dsp_apply_ops` with one batch. Set `errorPolicy: "allOrNothing"`. Pass a meaningful `revisionMessage` like "add Redis cache layer; move notifications to async publish" - this surfaces in the revision history.

6. Call `dsp_validate_scene` and resolve any `LABEL_DUPLICATION_DETECTED` warnings (sequence diagrams allow same-label messages temporally, but a duplicate at the same source is usually mis-typed). `CANVAS_AUTO_EXTENDED` is informational - let the layout engine size the canvas as the flow grows.

7. Call `dsp_render_scene({ format: "svg" })` and publish the rendered SVG to the docs site (or commit it to `docs/architecture/${ENDPOINT}-flow.svg`). The watermark stamps scene-id + revision + date so the published artifact is traceable.

8. Call `dsp_diff_scene({ from: PREV_REVISION, to: NEW_REVISION })`. Post a PR comment summarising the structural diff: which lifelines were added/removed, which messages changed kind, which fragments were introduced. Reviewers see what the new code does differently before reading the code diff.

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 on each run; always patch with stable ids - the request flow is a long-lived, evolving graph and stable ids are what make revision history meaningful. Treat the handler source as the source of truth; if a comment in the code contradicts the actual call, follow the actual call.

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 diagramFamily: "sequence". On subsequent runs, fetch the existing scene by id to read the current revision and elements.

    Example response

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

    Compare parsed lifelines + messages + fragments against the current scene. New participants → createNode (sequence.lifeline / sequence.actor). New interactions → createEdge with appropriate edgeType. New branching paths → createFrame with containerType: "fragment" and the right operator (alt | opt | loop | par). Use stable element ids derived from service names so a renamed lifeline stays the same element.

    Example response

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

    Send the diff as one applyOps batch with errorPolicy=allOrNothing. The agent uses stable lifeline ids (service.kebab-case) so a renamed service updates the existing lifeline rather than creating a duplicate. Set a meaningful revisionMessage (e.g. "add Redis cache lifeline; move notifications to async publish").

    Example response

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

    Resolve any LABEL_DUPLICATION_DETECTED diagnostics - sequence diagrams allow same-label messages (different timestamps), but suspicious duplicates (e.g. two replies labelled the same on the same source) are usually mis-typed. CANVAS_AUTO_EXTENDED is informational; the engine sizes the canvas as the flow grows.

    Example response

    { "ok": true, "diagnostics": [{ "code": "CANVAS_AUTO_EXTENDED", "severity": "info" }] }
  5. 05 POST /v1/scenes/${SCENE_ID}/render

    Render the updated scene at the new revision. The watermark stamps scene-id + revision + date so the rendered artifact is traceable back to the persisted scene. Theme defaults to clean; pass theme: "dark" if your docs site uses a dark canvas.

    Example response

    { "output": { "mimeType": "image/svg+xml", "content": "<svg ..." } }
  6. 06 GET /v1/scenes/${SCENE_ID}/diff?from=${PREV_REVISION}&to=${NEW_REVISION}

    Get a structural summary of what changed between PREV_REVISION and the new revision: added/removed/modified lifelines and messages. Drives the PR comment.

    Example response

    { "added": ["cache", "m_cache_get", "m_cache_hit"], "removed": [], "modified": ["m_notify"] }

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.

  • 24 operations
  • 15 createEdge, 1 createFrame, 8 createNode
{
  "schemaVersion": "0.1",
  "errorPolicy": "allOrNothing",
  "revisionMessage": "Build /checkout request-flow scene: 8 lifelines, alt fragment around payment retry, async notification publish",
  "ops": [
    {
      "op": "createNode",
      "id": "client",
      "nodeType": "sequence.actor",
      "shape": "rect",
      "label": "Client"
    },
    {
      "op": "createNode",
      "id": "checkout",
      "nodeType": "sequence.lifeline",
      "shape": "rect",
      "label": "Checkout Service"
    },
    {
      "op": "createNode",
      "id": "auth",
      "nodeType": "sequence.lifeline",
      "shape": "rect",
      "label": "Auth Service"
    },
    {
      "op": "createNode",
      "id": "cache",
      "nodeType": "sequence.lifeline",
      "shape": "rect",
      "label": "Redis Cache"
    },
    {
      "op": "createNode",
      "id": "inventory",
      "nodeType": "sequence.lifeline",
      "shape": "rect",
      "label": "Inventory Service"
    },
    {
      "op": "createNode",
      "id": "payment",
      "nodeType": "sequence.lifeline",
      "shape": "rect",
      "label": "Payment Provider\n(Stripe)"
    },
    {
      "op": "createNode",
      "id": "notify",
      "nodeType": "sequence.lifeline",
      "shape": "rect",
      "label": "Notification Queue"
    },
    {
      "op": "createEdge",
      "id": "m_post",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "client"
      },
      "to": {
        "elementId": "checkout"
      },
      "label": "POST /checkout"
    },
    {
      "op": "createEdge",
      "id": "m_verify",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "auth"
      },
      "label": "verifyToken(jwt)"
    },
    {
      "op": "createEdge",
      "id": "m_verify_ok",
      "edgeType": "sequence.reply",
      "from": {
        "elementId": "auth"
      },
      "to": {
        "elementId": "checkout"
      },
      "label": "user_id"
    },
    {
      "op": "createEdge",
      "id": "m_cache_get",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "cache"
      },
      "label": "GET cart:{user_id}"
    },
    {
      "op": "createEdge",
      "id": "m_cache_hit",
      "edgeType": "sequence.reply",
      "from": {
        "elementId": "cache"
      },
      "to": {
        "elementId": "checkout"
      },
      "label": "cart (hit)"
    },
    {
      "op": "createEdge",
      "id": "m_reserve",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "inventory"
      },
      "label": "reserve(items)"
    },
    {
      "op": "createEdge",
      "id": "m_reserved",
      "edgeType": "sequence.reply",
      "from": {
        "elementId": "inventory"
      },
      "to": {
        "elementId": "checkout"
      },
      "label": "reservation_id"
    },
    {
      "op": "createNode",
      "id": "note_payment",
      "nodeType": "sequence.note",
      "shape": "rect",
      "label": "Stripe charge with\nidempotency key",
      "extensions": {
        "anchor": "payment"
      }
    },
    {
      "op": "createEdge",
      "id": "m_charge",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "payment"
      },
      "label": "charge($total, idempotency_key)"
    },
    {
      "op": "createEdge",
      "id": "m_charge_ok",
      "edgeType": "sequence.reply",
      "from": {
        "elementId": "payment"
      },
      "to": {
        "elementId": "checkout"
      },
      "label": "charge_id"
    },
    {
      "op": "createEdge",
      "id": "m_charge_retry",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "payment"
      },
      "label": "charge (retry)"
    },
    {
      "op": "createEdge",
      "id": "m_charge_retry_ok",
      "edgeType": "sequence.reply",
      "from": {
        "elementId": "payment"
      },
      "to": {
        "elementId": "checkout"
      },
      "label": "charge_id (after retry)"
    },
    {
      "op": "createFrame",
      "id": "frag_charge",
      "containerType": "fragment",
      "title": "[charge succeeded]",
      "children": [
        "m_charge",
        "m_charge_ok",
        "m_charge_retry",
        "m_charge_retry_ok"
      ],
      "layout": {
        "mode": "absolute",
        "x": 270,
        "y": 545,
        "width": 1410,
        "height": 190
      },
      "extensions": {
        "operator": "alt",
        "operands": [
          {
            "y": 635,
            "guard": "retry on 5xx (exp backoff: 1s, 2s, 4s)"
          }
        ]
      }
    },
    {
      "op": "createEdge",
      "id": "m_commit",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "inventory"
      },
      "label": "commit(reservation_id)"
    },
    {
      "op": "createEdge",
      "id": "m_notify",
      "edgeType": "sequence.async",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "notify"
      },
      "label": "publish(order_placed)"
    },
    {
      "op": "createEdge",
      "id": "m_self_validate",
      "edgeType": "sequence.message",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "checkout"
      },
      "label": "build response"
    },
    {
      "op": "createEdge",
      "id": "m_response",
      "edgeType": "sequence.reply",
      "from": {
        "elementId": "checkout"
      },
      "to": {
        "elementId": "client"
      },
      "label": "200 OK + receipt"
    }
  ]
}

Validation

Valid

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

  • 1 diagnostic
  • 1 warning
  • warning SELF_LOOP_EDGE_WARNING /elements/m_self_validate

    Edge 'm_self_validate' is a self-loop on element 'checkout'.

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

PR #847 'Improve checkout latency' lands. The author replaced the direct cart-service call with a Redis cache lookup (the handler no longer fans out to cart on the happy path), introduced a retry path for Stripe payment 5xx errors (wrapped in an alt fragment), and switched the post-checkout email from a synchronous HTTP call to an async queue publish. Six new elements (cache lifeline, two cache messages, the retry fragment, two retry messages); four removed (cart lifeline + its two messages, plus the synchronous notification reply); one modified (m_notify changed from sequence.message to sequence.async). The reviewer sees the structural diff in the PR comment alongside the rendered SVG before reading the code diff line-by-line.

  • Revision 45
  • +6 added
  • -4 removed
  • ~1 modified
Before · revision 4
Request flow diagram from handler code — revision 4
After · revision 5
Request flow diagram from handler code — revision 5

+ Added

  • cache
  • m_cache_get
  • m_cache_hit
  • frag_charge
  • m_charge_retry
  • m_charge_retry_ok

Removed

  • cart
  • m_cart_get
  • m_cart_data
  • m_notify_ok

~ Modified

  • m_notify

Raw dsp_diff_scene response

{
  "schemaVersion": "1.0",
  "sceneId": "request-flow-from-handler",
  "fromRevision": 4,
  "toRevision": 5,
  "summary": {
    "added": 6,
    "removed": 4,
    "modified": 1
  },
  "added": [
    "cache",
    "m_cache_get",
    "m_cache_hit",
    "frag_charge",
    "m_charge_retry",
    "m_charge_retry_ok"
  ],
  "removed": [
    "cart",
    "m_cart_get",
    "m_cart_data",
    "m_notify_ok"
  ],
  "modified": [
    "m_notify"
  ],
  "scenario": "PR #847 'Improve checkout latency' lands. The author replaced the direct cart-service call with a Redis cache lookup (the handler no longer fans out to cart on the happy path), introduced a retry path for Stripe payment 5xx errors (wrapped in an alt fragment), and switched the post-checkout email from a synchronous HTTP call to an async queue publish. Six new elements (cache lifeline, two cache messages, the retry fragment, two retry messages); four removed (cart lifeline + its two messages, plus the synchronous notification reply); one modified (m_notify changed from sequence.message to sequence.async). The reviewer sees the structural diff in the PR comment alongside the rendered SVG before reading the code diff line-by-line."
}

CI/CD recipe

Pull request Raw YAML

A complete, runnable GitHub Actions workflow for this example. Drop the YAML into .github/workflows/zindex-request-flow-from-handler.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 the checkout handler, its directly-imported service modules, or the Stripe SDK. The path list is project-specific - point it at YOUR flagship endpoint and its dependency tree. No cron: the request-flow diagram is code-driven, so a PR is the only meaningful trigger (vs. the api-dependency-map example, where a weekly cron catches drift from imports added without a spec change).

  • Path filters: apps/api/src/routes/checkout.ts, apps/api/src/services/checkout/**, packages/sdk-stripe/**

Required secrets

  • ZINDEX_API_KEY required Zindex API key with scene-write scope.
  • ZINDEX_SCENE_ID required Long-lived persisted scene id for the request flow. Create once via dsp_create_scene; reuse across all runs. Different endpoints get different scene ids.

Inputs

  • scripts/parse-handler.mjs (you author this) - walks the handler source via the TS compiler API, emits { lifelines, messages, fragments, notes } JSON
  • scripts/handler-to-ops.mjs (you author this) - converts the parsed flow to a Zindex applyOps batch with stable lifeline-name ids

Outputs

  • out/checkout-flow.svg - rendered sequence diagram, uploaded as a 30-day workflow artifact
  • out/diff.json - structural diff (added/removed lifelines + messages + fragments) vs the previous revision
  • PR comment summarising the structural change with a link to the rendered SVG
  • A new persisted-scene revision per PR; the revision history becomes the team's audit trail of how the request flow has evolved

GitHub Actions workflow

# Zindex - Request flow from handler. Maintains a sequence diagram of an
# HTTP endpoint's request flow by parsing the handler source on every PR
# that touches the relevant code paths. Code-driven, so PR-only - no
# weekly cron (vs. the api-dependency-map example, which catches drift
# from imports added without a spec change).
#
# Drop into .github/workflows/zindex-checkout-flow.yml.

name: Zindex - Checkout request flow

on:
  pull_request:
    paths:
      - "apps/api/src/routes/checkout.ts"
      - "apps/api/src/services/checkout/**"
      - "packages/sdk-stripe/**"
  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-flow:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      # 1. Parse the handler source. Walk the function's async/await chain
      #    via the TS compiler API. Emit a normalised
      #    { lifelines: [...], messages: [...], fragments: [...], notes: [...] }
      #    JSON. This step is project-specific - implement parse-handler.mjs
      #    against your handler's import structure and the languages you use.
      - name: Parse handler call graph
        run: |
          mkdir -p out
          node scripts/parse-handler.mjs apps/api/src/routes/checkout.ts > out/flow.json
          echo "::notice::Detected $(jq '.lifelines | length' out/flow.json) lifelines, $(jq '.messages | length' out/flow.json) messages, $(jq '.fragments | length' out/flow.json) fragments"

      # 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 lifeline ids derived
      #    from service names (kebab-case) so a renamed service updates the
      #    existing lifeline rather than producing a delete + create pair.
      - name: Compute applyOps batch
        run: node scripts/handler-to-ops.mjs out/flow.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/applyOps")
          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/checkout-flow.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: checkout-flow-svg
          path: out/checkout-flow.svg
          retention-days: 30

      # 7. PR comment.
      - 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:request-flow-from-handler -->"

      - 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:request-flow-from-handler -->
            ### Checkout request flow updated · revision ${{ steps.apply.outputs.rev }}

            | | |
            |---|---|
            | Structural 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:request-flow-from-handler --> lets peter-evans/create-or-update-comment find and overwrite the previous comment instead of appending a new one.

<!-- zindex-bot:request-flow-from-handler -->
### Checkout request flow updated · revision ${NEW_REVISION}

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

${STRUCTURAL_SUMMARY}

<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.

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