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 onesequence.actorper external initiator (the Client) and onesequence.lifelineper internal collaborator. - Messages typed correctly:
sequence.messagefor synchronous calls,sequence.replyfor return values (rendered with dashed open arrows),sequence.asyncfor fire-and-forget queue publishes (rendered with open arrowheads),sequence.createfor 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
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
- 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.
- 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).
- 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.
- 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").
- 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.
- 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.
- 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.
- 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
- 01
dsp_create_sceneCreate or fetch the persisted scene - 02
dsp_get_sceneDiff parsed flow against the persisted scene - 03
dsp_apply_opsApply the operation batch - 04
dsp_validate_sceneValidate the updated scene - 05
dsp_render_sceneRender to SVG - 06
dsp_diff_sceneDiff 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.
- 01
POST/v1/scenesOn 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 } - 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": ["..."] } - 03
POST/v1/scenes/${SCENE_ID}/applyOpsSend 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 } - 04
POST/v1/scenes/validateResolve 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" }] } - 05
POST/v1/scenes/${SCENE_ID}/renderRender 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 ..." } } - 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.
{
"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
-
warningSELF_LOOP_EDGE_WARNING/elements/m_self_validateEdge '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 4 → 5
- +6 added
- -4 removed
- ~1 modified
+ Added
cachem_cache_getm_cache_hitfrag_chargem_charge_retrym_charge_retry_ok
− Removed
cartm_cart_getm_cart_datam_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
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).
Required secrets
-
ZINDEX_API_KEYrequired Zindex API key with scene-write scope. -
ZINDEX_SCENE_IDrequired 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>
Agent resources
Machine-readable versions of this example. Agents should fetch these rather than scrape the rendered HTML.
-
request-flow-from-handler.scene.jsonCanonical DSP scene Open -
request-flow-from-handler.ops.jsonTyped-operation envelope that builds the scene Open -
request-flow-from-handler.workflow.jsonStructured agent workflow (goal, inputs, outputs, steps) Open -
request-flow-from-handler.diff.jsonSampledsp_diff_sceneresponse (revision evolution) Open -
request-flow-from-handler.github-actions.ymlRunnable GitHub Actions workflow for the CI/CD recipe Open -
request-flow-from-handler.svgBuild-time rendered diagram Open -
request-flow-from-handler.mdAgent-readable markdown summary Open -
/examples/index.jsonManifest of all examples (cross-linked) Open
PR comment template
The bot posts this comment on every triggering PR. The hidden marker
<!-- zindex-bot:request-flow-from-handler -->letspeter-evans/create-or-update-commentfind and overwrite the previous comment instead of appending a new one.