What this example shows
A classic blog schema rendered as a Crow’s Foot ER diagram with PK / FK badges, column-row anchored edges, and auto-inferred cardinality glyphs. The canonical scene is the same shape an agent would produce after parsing migration files for a real codebase - the agent’s job is to keep this diagram current as the schema evolves, not to redraw it from scratch on every PR.
When to use it
Reach for this pattern whenever your team’s schema lives in migration files (Prisma, Drizzle, Alembic, Rails, SQLAlchemy, raw SQL). The diagram becomes a derived artifact: agents read migrations, apply typed operations to the persisted scene, and post a visual diff in the PR. New engineers see the schema as a picture; reviewers see exactly what each migration adds, removes, or changes.
What the agent does
The agent watches migration files in your repo, parses them into table / column / key / foreign-key changes, fetches the current persisted scene, and applies createNode, updateNode, createEdge, deleteElement operations to bring the scene up to date. It validates structurally (no orphan FKs, no duplicate IDs), renders the updated diagram, diffs the previous revision against the new one, and posts the visual diff as a PR comment.
The flow is incremental - the persisted scene preserves stable IDs across migrations, so adding one column doesn’t reshuffle the whole diagram. Engineers see “what changed in this PR” rather than “here’s the entire schema again.”
What the output includes
- A compartmented ER diagram with one entity per table, columns shown as labelled rows, PK / FK badges per column, and Crow’s Foot glyphs at every relationship endpoint.
- Auto-inferred cardinality from
pk/fk/unique/nullablecolumn metadata - agents don’t need to hand-set cardinality on most edges. - FK edges that anchor at the named column row (the dbdiagram.io / DBeaver convention) so the relationship is visible at a glance.
- A revision diff showing exactly which entities, columns, or relationships changed since the last PR.
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": "entityRelationship",
"scene": {
"id": "blog-er",
"title": "Blog Entity Relationship",
"units": "px",
"canvas": {
"width": 1100,
"height": 600
}
},
"layoutStrategy": {
"algorithm": "hierarchical",
"direction": "LR",
"nodeSpacing": 60,
"rankSpacing": 140
},
"elements": [
{
"id": "user",
"kind": "node",
"nodeType": "er.entity",
"shape": "rect",
"label": "Users",
"extensions": {
"columns": [
{
"name": "id",
"type": "int",
"pk": true
},
{
"name": "username",
"type": "varchar"
},
{
"name": "email",
"type": "varchar"
},
{
"name": "created_at",
"type": "timestamp"
}
]
}
},
{
"id": "post",
"kind": "node",
"nodeType": "er.entity",
"shape": "rect",
"label": "Posts",
"extensions": {
"columns": [
{
"name": "id",
"type": "int",
"pk": true
},
{
"name": "author_id",
"type": "int",
"fk": true
},
{
"name": "title",
"type": "varchar"
},
{
"name": "body",
"type": "text"
}
]
}
},
{
"id": "comment",
"kind": "node",
"nodeType": "er.entity",
"shape": "rect",
"label": "Comments",
"extensions": {
"columns": [
{
"name": "id",
"type": "int",
"pk": true
},
{
"name": "post_id",
"type": "int",
"fk": true
},
{
"name": "user_id",
"type": "int",
"fk": true
},
{
"name": "body",
"type": "text"
}
]
}
},
{
"id": "tag",
"kind": "node",
"nodeType": "er.entity",
"shape": "rect",
"label": "Tags",
"extensions": {
"columns": [
{
"name": "id",
"type": "int",
"pk": true
},
{
"name": "name",
"type": "varchar"
}
]
}
},
{
"id": "e-writes",
"kind": "edge",
"edgeType": "er.relationship",
"from": {
"elementId": "post",
"column": "author_id"
},
"to": {
"elementId": "user"
},
"router": "orthogonal"
},
{
"id": "e-comments",
"kind": "edge",
"edgeType": "er.relationship",
"from": {
"elementId": "comment",
"column": "user_id"
},
"to": {
"elementId": "user"
},
"router": "orthogonal"
},
{
"id": "e-has-comments",
"kind": "edge",
"edgeType": "er.relationship",
"from": {
"elementId": "comment",
"column": "post_id"
},
"to": {
"elementId": "post"
},
"router": "orthogonal"
},
{
"id": "e-tagged",
"kind": "edge",
"edgeType": "er.relationship",
"from": {
"elementId": "post"
},
"to": {
"elementId": "tag",
"cardinality": "many"
},
"router": "orthogonal",
"label": "tagged"
}
]
} Agent workflow
Keep an ER diagram in sync with database migrations on every PR. Parse the migration files changed in the PR, derive entity / column / FK changes, patch the persisted Zindex scene with typed operations, validate, render, and post the visual diff back to the PR.
Inputs
- List of migration files changed in the PR (Prisma / Drizzle / Alembic / Rails)
- Existing Zindex scene id (stored as a repo secret on first run)
- Zindex API key with scene-write scope
Outputs
- Updated persisted scene with a new immutable revision
- Rendered SVG of the new schema
- Revision diff (JSON) describing what entities / columns / FKs changed
- PR comment with the rendered diagram and summary of schema changes
- 01
Create or fetch the persisted scene
On first run, create a scene with diagramFamily=entityRelationship and an LR hierarchical layoutStrategy. On subsequent runs, fetch the existing scene by id to read the current revision.
- 02
Parse changed migration files
Read the migration files added or modified in the PR. Extract the table-level changes: new tables, dropped tables, added columns, renamed columns, dropped columns, new foreign keys, and new indexes. Most ORMs leave a structured trail (Prisma's create/alter blocks, Alembic op.create_table / op.add_column, Rails CreateTable / AddColumn) - parse those rather than free-text SQL.
- 03
Diff parsed schema against the persisted scene
For each entity in the persisted scene, check whether it still exists in the parsed schema. For each column inside each entity, check whether it was added, removed, renamed, or had its type changed. Emit a list of intended ops (createNode / updateNode / deleteElement / createEdge / updateEdge) - do not yet apply them.
- 04
Apply the typed-operation batch
Send the diff as a single applyOps batch with errorPolicy=allOrNothing so the revision either commits cleanly or fails atomically. Always reuse stable ids (`user`, `post`, etc.) so renames produce a true updateNode rather than a delete+create. The element id is the schema's stable handle; do not regenerate it on every run.
- 05
Validate the new revision
Confirm the scene still validates against ER semantics: every FK column referenced by an edge resolves, every entity has a primary key, no duplicate column names within an entity. The platform emits structured diagnostic codes (EDGE_COLUMN_NOT_FOUND, LABEL_DUPLICATION_DETECTED, …) - fix EDGE_COLUMN_NOT_FOUND and LABEL_DUPLICATION_DETECTED before continuing; CANVAS_AUTO_EXTENDED and EDGE_LABEL_SUPPRESSED_REDUNDANT are informational and can ship.
- 06
Render the updated scene
Render the new revision to SVG. The watermark stamps scene id + revision + date so the PR artifact is traceable back to the persisted scene without metadata sidecar files.
- 07
Diff against the previous revision
Ask the platform for the structural diff between the prior revision (recorded on the previous CI run) and the new one. The response lists added / removed / changed elements - perfect for a humanly-readable PR comment, and it does not require the agent to re-derive the diff on its own.
- 08
Post the rendered diagram + diff to the PR
Comment on the PR with the rendered SVG (or a link to the .svg artifact) and the human-readable diff summary. Re-comment idempotently on subsequent commits to keep the latest snapshot pinned.
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 schema against the persisted scene - 03
dsp_apply_opsApply the typed-operation batch - 04
dsp_validate_sceneValidate the new revision - 05
dsp_render_sceneRender the updated scene - 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 database-documentation agent. Your job is to keep an entity-relationship diagram of the application schema in sync with the actual schema definition, on every pull request, without ever drawing the diagram by hand.
The persisted Zindex scene id is `${SCENE_ID}`; treat it as the canonical, mutable, immutable-revisioned source of truth for the ER diagram. The scene already exists - do not create a new one. Each PR potentially introduces a new revision; your job is to compute the smallest valid set of typed operations that move the scene from its current revision to one that matches the schema after this PR.
Workflow on every run:
1. Read the migration files changed in this PR - Prisma `schema.prisma` and `migrations/**`, Drizzle `drizzle/meta/**`, Alembic `alembic/versions/**`, or Rails `db/migrate/**`. Use the migrations rather than free-text SQL: ORMs leave a structured trail you can parse reliably (Prisma's create/alter blocks, Alembic op.create_table / op.add_column, Rails CreateTable / AddColumn). Extract the table-level shape: tables, columns with types, primary keys, foreign keys.
2. Call `dsp_get_scene({ sceneId: "${SCENE_ID}" })` to read the current revision and elements. Diff what you parsed against what is persisted: which entities are new, which were dropped, which had columns added / removed / renamed / retyped, which FK relationships are new.
3. Call `dsp_apply_ops` with one batch of typed operations. Set `errorPolicy: "allOrNothing"` so the revision either commits cleanly or fails atomically. Always reuse stable ids (`user`, `post`, `comment`, …) - element ids are the schema's stable handle; renames must produce `updateNode`, not `deleteElement` + `createNode`, or you will lose the FK edges that anchor on those ids. Pass a meaningful `revisionMessage` like "add Comments table from migration 20260415_add_comments" so the revision history reads like a git log.
4. Call `dsp_validate_scene({ sceneId: "${SCENE_ID}" })`. Resolve any `EDGE_COLUMN_NOT_FOUND` (the FK references a column that does not exist - usually a typo or stale migration); resolve any `LABEL_DUPLICATION_DETECTED` (rename one of the duplicated edge labels). `CANVAS_AUTO_EXTENDED` and `EDGE_LABEL_SUPPRESSED_REDUNDANT` are informational and require no action.
5. Call `dsp_render_scene({ sceneId: "${SCENE_ID}", format: "svg", theme: "clean" })` to render the new revision. The watermark stamps scene-id + revision + date so the PR artifact is traceable.
6. Call `dsp_diff_scene({ sceneId: "${SCENE_ID}", from: PREV_REVISION, to: NEW_REVISION })` to get the structural diff (added / removed / changed elements). Use the diff in the PR comment so reviewers can read the schema change at a glance without re-deriving it from the migration.
7. Post a single PR comment containing the rendered SVG (or a link to the .svg artifact) and the diff summary. Re-comment idempotently on subsequent commits - overwrite the last comment rather than appending - so reviewers always see the latest snapshot pinned to the top of the conversation.
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. Never skip validation; an unvalidated scene risks shipping a diagram that does not match the schema. If validation surfaces an unrecoverable error, comment on the PR with the structured diagnostic and ask a human to resolve.
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=entityRelationship and an LR hierarchical layoutStrategy. On subsequent runs, fetch the existing scene by id to read the current revision.
Example request body
{ "schemaVersion": "0.1", "diagramFamily": "entityRelationship", "scene": { "id": "blog-er", "title": "Blog Entity Relationship", "canvas": { "width": 1100, "height": 600 } }, "layoutStrategy": { "algorithm": "hierarchical", "direction": "LR", "nodeSpacing": 60, "rankSpacing": 140 }, "elements": [] }Example response
{ "sceneId": "sc_a1b2c3", "revision": 1 } - 02
GET/v1/scenes/${SCENE_ID}For each entity in the persisted scene, check whether it still exists in the parsed schema. For each column inside each entity, check whether it was added, removed, renamed, or had its type changed. Emit a list of intended ops (createNode / updateNode / deleteElement / createEdge / updateEdge) - do not yet apply them.
Example response
{ "sceneId": "sc_a1b2c3", "revision": 12, "scene": { "id": "blog-er", "...": "..." }, "elements": [ "..." ] } - 03
POST/v1/scenes/${SCENE_ID}/opsSend the diff as a single applyOps batch with errorPolicy=allOrNothing so the revision either commits cleanly or fails atomically. Always reuse stable ids (`user`, `post`, etc.) so renames produce a true updateNode rather than a delete+create. The element id is the schema's stable handle; do not regenerate it on every run.
Example request body
{ "schemaVersion": "0.1", "errorPolicy": "allOrNothing", "revisionMessage": "add Comments table from migration 20260415", "ops": [ { "op": "createNode", "id": "comment", "nodeType": "er.entity", "shape": "rect", "label": "Comments", "extensions": { "columns": ["..."] } } ] }Example response
{ "sceneId": "sc_a1b2c3", "revision": 13, "applied": 4 } - 04
POST/v1/scenes/validateConfirm the scene still validates against ER semantics: every FK column referenced by an edge resolves, every entity has a primary key, no duplicate column names within an entity. The platform emits structured diagnostic codes (EDGE_COLUMN_NOT_FOUND, LABEL_DUPLICATION_DETECTED, …) - fix EDGE_COLUMN_NOT_FOUND and LABEL_DUPLICATION_DETECTED before continuing; CANVAS_AUTO_EXTENDED and EDGE_LABEL_SUPPRESSED_REDUNDANT are informational and can ship.
Example response
{ "ok": true, "diagnostics": [] } - 05
POST/v1/scenes/${SCENE_ID}/renderRender the new revision to SVG. The watermark stamps scene id + revision + date so the PR artifact is traceable back to the persisted scene without metadata sidecar files.
Example request body
{ "format": "svg", "theme": "clean" }Example response
{ "output": { "format": "svg", "content": "<svg ...>" }, "revision": 13, "diagnostics": [] } - 06
GET/v1/scenes/${SCENE_ID}/diff?from=${PREV_REVISION}&to=${NEW_REVISION}Ask the platform for the structural diff between the prior revision (recorded on the previous CI run) and the new one. The response lists added / removed / changed elements - perfect for a humanly-readable PR comment, and it does not require the agent to re-derive the diff on its own.
Example response
{ "added": ["comment", "e-comments", "e-has-comments"], "removed": [], "changed": ["post"], "summary": "+1 entity, +2 relationships, 1 column added to post" }
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": "Initial blog ER schema: users, posts, comments, tags",
"ops": [
{
"op": "createNode",
"id": "user",
"nodeType": "er.entity",
"shape": "rect",
"label": "Users",
"extensions": {
"columns": [
{
"name": "id",
"type": "int",
"pk": true
},
{
"name": "username",
"type": "varchar"
},
{
"name": "email",
"type": "varchar"
},
{
"name": "created_at",
"type": "timestamp"
}
]
}
},
{
"op": "createNode",
"id": "post",
"nodeType": "er.entity",
"shape": "rect",
"label": "Posts",
"extensions": {
"columns": [
{
"name": "id",
"type": "int",
"pk": true
},
{
"name": "author_id",
"type": "int",
"fk": true
},
{
"name": "title",
"type": "varchar"
},
{
"name": "body",
"type": "text"
}
]
}
},
{
"op": "createNode",
"id": "comment",
"nodeType": "er.entity",
"shape": "rect",
"label": "Comments",
"extensions": {
"columns": [
{
"name": "id",
"type": "int",
"pk": true
},
{
"name": "post_id",
"type": "int",
"fk": true
},
{
"name": "user_id",
"type": "int",
"fk": true
},
{
"name": "body",
"type": "text"
}
]
}
},
{
"op": "createNode",
"id": "tag",
"nodeType": "er.entity",
"shape": "rect",
"label": "Tags",
"extensions": {
"columns": [
{
"name": "id",
"type": "int",
"pk": true
},
{
"name": "name",
"type": "varchar"
}
]
}
},
{
"op": "createEdge",
"id": "e-writes",
"edgeType": "er.relationship",
"from": {
"elementId": "post",
"column": "author_id"
},
"to": {
"elementId": "user"
},
"router": "orthogonal"
},
{
"op": "createEdge",
"id": "e-comments",
"edgeType": "er.relationship",
"from": {
"elementId": "comment",
"column": "user_id"
},
"to": {
"elementId": "user"
},
"router": "orthogonal"
},
{
"op": "createEdge",
"id": "e-has-comments",
"edgeType": "er.relationship",
"from": {
"elementId": "comment",
"column": "post_id"
},
"to": {
"elementId": "post"
},
"router": "orthogonal"
},
{
"op": "createEdge",
"id": "e-tagged",
"edgeType": "er.relationship",
"from": {
"elementId": "post"
},
"to": {
"elementId": "tag",
"cardinality": "many"
},
"router": "orthogonal",
"label": "tagged"
}
]
} Validation
Valid
Captured response from POST /v1/scenes/validate. The
platform runs 40+ semantic checks; see the full list in the validation rules reference.
- 4 diagnostics
- 4 warnings
-
warningFAMILY_ER_MISSING_CARDINALITY/elements/e-writesER edge 'e-writes' is missing cardinality on one or both endpoints.
-
warningFAMILY_ER_MISSING_CARDINALITY/elements/e-commentsER edge 'e-comments' is missing cardinality on one or both endpoints.
-
warningFAMILY_ER_MISSING_CARDINALITY/elements/e-has-commentsER edge 'e-has-comments' is missing cardinality on one or both endpoints.
-
warningFAMILY_ER_MISSING_CARDINALITY/elements/e-taggedER edge 'e-tagged' is missing cardinality on one or both endpoints.
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 migration introduces a Tags table and a many-to-one relationship from Posts to Tags. The agent parsed the migration, added the new entity (with id, name columns) and a labelled relationship edge, and committed revision 12. The diff lists exactly what reviewers need to look at.
- Revision 11 → 12
- +2 added
- -0 removed
- ~0 modified
+ Added
tage-tagged
Raw dsp_diff_scene response
{
"schemaVersion": "1.0",
"sceneId": "blog-er",
"fromRevision": 11,
"toRevision": 12,
"summary": {
"added": 2,
"removed": 0,
"modified": 0
},
"added": [
"tag",
"e-tagged"
],
"removed": [],
"modified": [],
"scenario": "A new migration introduces a Tags table and a many-to-one relationship from Posts to Tags. The agent parsed the migration, added the new entity (with id, name columns) and a labelled relationship edge, and committed revision 12. The diff lists exactly what reviewers need to look at."
} CI/CD recipe
A complete, runnable GitHub Actions workflow for this example.
Drop the YAML into .github/workflows/zindex-er-diagram-from-migrations.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 your ORM's migrations directory. Pair with `workflow_dispatch` so you can manually re-run after a migration fix.
Required secrets
-
ZINDEX_API_KEYrequired Zindex API key with scene-write scope. Generate at /dashboard/api-keys. -
ZINDEX_SCENE_IDrequired The persisted scene id for this repo's ER diagram. Create the scene once via dsp_create_scene or POST /v1/scenes; store the returned id as a repo secret. Long-lived - never recreate.
Inputs
- Migration files changed in the pull request
- scripts/parse-migrations.mjs (you author this - converts your ORM's migration format to a normalised schema.json)
- scripts/schema-to-ops.mjs (you author this - turns the schema diff into createNode/updateNode/createEdge ops with stable element ids)
Outputs
- out/diagram.svg - the rendered ER diagram, uploaded as a workflow artifact (30-day retention)
- out/diff.json - structural diff between the prior and new revision
- PR comment with revision number, schema-change counts, and an artifact-download link
- A new immutable revision in the persisted Zindex scene
GitHub Actions workflow
# Zindex - keep an ER diagram in sync with database migrations on every PR.
# Drop into .github/workflows/zindex-er.yml. Set the listed secrets, customise
# the parse step for your ORM, and the agent runs unattended on every PR that
# touches your migrations directory.
#
# Trigger: PR opened/synchronised that touches prisma/migrations/** (or your
# ORM's equivalent).
# Secrets: ZINDEX_API_KEY, ZINDEX_SCENE_ID.
# Outputs: a workflow artifact containing the rendered SVG + a PR comment with
# diff summary and rendered diagram.
name: Zindex - ER diagram from migrations
on:
pull_request:
paths:
- "prisma/migrations/**"
- "drizzle/meta/**"
- "alembic/versions/**"
- "db/migrate/**"
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-er-diagram:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
# 1. Parse the changed migration files. This step is project-specific:
# write a small parser for your ORM that emits the table → columns →
# foreign keys structure as JSON to ./out/schema.json. The shape
# expected downstream is { entities: [{ id, label, columns: [...] }],
# relationships: [{ id, from: { entity, column }, to: { entity } }] }.
- name: Parse migrations to schema.json
run: |
mkdir -p out
node scripts/parse-migrations.mjs > out/schema.json
echo "::notice::Parsed $(jq '.entities | length' out/schema.json) entities, $(jq '.relationships | length' out/schema.json) relationships"
# 2. Read the persisted scene's current revision (so we can 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: |
REV=$(curl -fsSL \
-H "Authorization: Bearer $ZINDEX_API_KEY" \
"$ZINDEX_API_BASE/v1/scenes/$ZINDEX_SCENE_ID" | jq -r '.revision')
echo "rev=$REV" >> "$GITHUB_OUTPUT"
echo "::notice::Persisted scene at revision $REV"
# 3. Compute the typed-operation batch from the parsed schema. This is
# where your agent / script does the real work - turn schema diffs
# into createNode / updateNode / deleteElement / createEdge ops with
# stable element ids (table name → element id) so renames produce
# `updateNode` rather than delete-and-create.
- name: Compute applyOps batch
run: node scripts/schema-to-ops.mjs out/schema.json > out/ops.json
# 4. Apply the batch - atomic via errorPolicy=allOrNothing.
- 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")
NEW_REV=$(echo "$RESP" | jq -r '.revision')
echo "rev=$NEW_REV" >> "$GITHUB_OUTPUT"
echo "applied=$(echo "$RESP" | jq -r '.applied')" >> "$GITHUB_OUTPUT"
# 5. Render the new revision. Watermark stamps scene-id + revision + date.
- name: Render updated scene to SVG
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 the new revision against the prior one - this is the
# PR-comment payload.
- name: Diff revisions
id: diff
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
ADDED=$(jq -r '.summary.added' out/diff.json)
REMOVED=$(jq -r '.summary.removed' out/diff.json)
MODIFIED=$(jq -r '.summary.modified' out/diff.json)
echo "added=$ADDED" >> "$GITHUB_OUTPUT"
echo "removed=$REMOVED" >> "$GITHUB_OUTPUT"
echo "modified=$MODIFIED" >> "$GITHUB_OUTPUT"
# 7. Upload the rendered SVG as a workflow artifact (30-day retention).
# The PR comment links to this artifact.
- uses: actions/upload-artifact@v4
id: upload
with:
name: er-diagram-svg
path: out/diagram.svg
retention-days: 30
# 8. Find the previous bot comment so we can replace it (idempotent).
- uses: peter-evans/find-comment@v3
id: find_comment
with:
issue-number: ${{ github.event.pull_request.number }}
comment-author: "github-actions[bot]"
body-includes: "<!-- zindex-bot:er-diagram-from-migrations -->"
# 9. Post (or replace) the PR comment with the diff summary + diagram link.
- uses: peter-evans/create-or-update-comment@v4
with:
comment-id: ${{ steps.find_comment.outputs.comment-id }}
issue-number: ${{ github.event.pull_request.number }}
edit-mode: replace
body: |
<!-- zindex-bot:er-diagram-from-migrations -->
### ER diagram updated · revision ${{ steps.apply.outputs.rev }}
| | |
|---|---|
| Schema changes | +${{ steps.diff.outputs.added }} / -${{ steps.diff.outputs.removed }} / ~${{ steps.diff.outputs.modified }} |
| Ops applied | ${{ steps.apply.outputs.applied }} |
| 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.
-
er-diagram-from-migrations.scene.jsonCanonical DSP scene Open -
er-diagram-from-migrations.ops.jsonTyped-operation envelope that builds the scene Open -
er-diagram-from-migrations.workflow.jsonStructured agent workflow (goal, inputs, outputs, steps) Open -
er-diagram-from-migrations.diff.jsonSampledsp_diff_sceneresponse (revision evolution) Open -
er-diagram-from-migrations.github-actions.ymlRunnable GitHub Actions workflow for the CI/CD recipe Open -
er-diagram-from-migrations.svgBuild-time rendered diagram Open -
er-diagram-from-migrations.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:er-diagram-from-migrations -->letspeter-evans/create-or-update-commentfind and overwrite the previous comment instead of appending a new one.