Skip to content
Connect an agent
Get started

Diagrams as code

Make diagrams from the files they describe (Terraform, OpenAPI, Docker Compose, Kubernetes, Helm, Kustomize, the AWS CDK, Backstage catalogs, Structurizr workspaces, AsyncAPI documents, Prisma, Drizzle, SQL, GitHub Actions, GitLab CI/CD, and Azure Pipelines pipelines, CSV and JSON, folder listings, and git logs) and keep them in step: a pull request whose diagrams no longer match fails, and a workflow writes them again. Hand changes live in an overrides file, so they survive every run. It is for anyone who keeps architecture or schema diagrams in a repository. A working repository to copy from is plotwork-official/plotwork-examples (opens in a new tab).

For
  • GitHub Actions
  • GitHub

TL;DR

QuestionAnswer
Where do I say what to draw?In sources in plotwork.config.json: for each diagram, the files (from), their format, and where it goes (out), a .diagram file or a fence of a Markdown file.
What makes the diagrams?npx @buun_group/plotwork generate on your machine, or the GitHub Action with command: generate. Both run the same importers as the import_source tool, locally.
How do I change a generated diagram?In its overrides file (infra.overrides.json beside infra.diagram): labels, icons, tones, types, groups, hidden boxes, extra edges and lines, header options. Never in the generated file, which the next run writes over.
I already edited the file by hand.Run plotwork generate --capture: it writes your edits into the overrides file, then makes the diagram again.
What fails a pull request?A diagram that differs from what its files make now. The check names what changed: nodes and edges added, removed, and changed.
How are the diagrams written back?The Action in write mode opens or edits one pull request (the default), or commits to the branch with on-drift: commit.
Can it publish too?Yes, with publish: true and an API key in a secret: a stable link for a personal key, or the organisation library for an organisation key.
Is there a whole example?plotwork-examples (opens in a new tab): a small company's Terraform, OpenAPI, Compose, Kubernetes, SQL, workflow, and CSV files, with a source for each, an overrides file, a Markdown fence, and the check, regenerate, and smoke workflows.
Will the pictures look right on GitHub in dark mode?Set "schemes": "auto": each diagram gets one SVG that follows the reader's light or dark setting, so a plain link or <img> shows it right.
Does anything leave the machine?Not by default. Only publishing sends anything, and then only the generated diagram source, never the files it was made from.

1. Name the sources

Each entry of sources is one diagram. Paths start at the folder that holds the settings file.

json
{  "$schema": "https://docs.plotwork.dev/schemas/plotwork.config.json",  "sources": [    {      "name": "infra",      "from": "infra/**/*.tf",      "format": "terraform",      "out": "docs/infra.diagram"    },    {      "name": "api",      "from": "openapi.yaml",      "format": "openapi",      "view": "map",      "out": "docs/api.md#endpoints"    },    {      "name": "services",      "from": [        "compose.yaml",        "compose.override.yaml"      ],      "format": "compose",      "out": "docs/architecture.md#services"    },    {      "name": "schema",      "from": "prisma/schema.prisma",      "format": "prisma",      "out": "docs/schema.diagram",      "options": {        "tables": [          "User",          "Org*"        ]      }    }  ]}
FieldMeaning
fromA file, a folder, or a glob, or a list of them. One that starts with ! leaves out what it matches. .git, node_modules, and .terraform are never read.
formatsql, prisma, drizzle, openapi, terraform, terraform-plan, bicep, arm, pulumi, cloudformation, cdk, compose, kubernetes, helm, kustomize, backstage, structurizr, asyncapi, github-actions, gitlab-ci, azure-pipelines, csv, json, tree, or git-log, as Importing from code and data describes.
outA .diagram file, or a Markdown file and a fence id: docs/architecture.md#services.
nameA short name for --source and the summary. The out path when left out.
view, titleThe picture of a format drawn more than one way (endpoints, map, sequence for OpenAPI; git, timeline for a git log; context, container, component for a Structurizr workspace), and the title.
optionsThe importer's options: tables, paths, tags, operationId, chart, groupBy (owner for a Backstage catalog), varFiles, and root, the root module or CDK stack of several to draw (envs/prod), each a diagram of its own when left out, or the system or container a Structurizr view is about.
overridesThe overrides file. Left out: infra.diagram has infra.overrides.json, and the fence services of docs/architecture.md has docs/architecture.services.overrides.json.
picturesFor a .diagram out, also write its light and dark SVG beside it. On when left out.
schemeslight-and-dark writes infra.svg and infra.dark.svg; auto writes one infra.svg that holds both. defaults.schemes, then light-and-dark, when left out. For a Markdown out it applies to every fence of that file.
publishWhere the Action publishes it (section 6).

Two sources that write one file, or a view the format does not draw, are S101 on their line of the settings file. A from that matches no file is S103.

A Markdown out needs a fence that says where the diagram goes. It may be empty; the first run fills it and writes the picture block under it, as sync does:

Markdown
## Services
```diagram id=services```

One picture for both themes

A light SVG opened on GitHub, or put in a page with a plain ![](infra.svg), shows its white cards in dark mode too. GitHub picks a dark file only from a <picture> element with a prefers-color-scheme: dark source. With schemes set to auto, each diagram gets one SVG that holds both schemes and switches inside the file with the same media query, so the file itself, a link to it, and a plain <img> all show the scheme the reader uses. The two schemes are painted from one layout, so the picture is the same as the two files, and the one file is smaller than the two together.

json
{  "defaults": {    "schemes": "auto"  },  "sources": [    {      "name": "infra",      "from": "infra/**/*.tf",      "format": "terraform",      "out": "docs/infra.diagram"    }  ]}

Use auto for any diagram people see on GitHub. A Markdown fence's picture block is then one <img> instead of a <picture>. Switching a repository to auto removes each .dark.svg that Plotwork drew there, so no file is left behind; a check before that run reports the removal as drift. A file under that name that Plotwork did not draw is never touched. sync reads the same default for the fences it draws.

A git-log source reads a file. Write it in an earlier step, such as git log --oneline --graph -n 40 > docs/history.txt, or the check fails on every new commit.

2. Generate

sh
npx @buun_group/plotwork generate
CommandWhat it does
generateMakes every diagram and writes the ones that changed: the .diagram file and its SVGs, or the fence and its picture block.
generate --checkWrites nothing. Exits 1 when a diagram differs from what its files make, and prints what changed.
generate --source infraOne source only.
generate --captureFirst writes the hand edits of each generated diagram into its overrides file.
generate --format githubEach problem and each drift as a GitHub annotation. --format json prints the plan, --format sarif a SARIF report for code scanning, and --format junit a JUnit report.
generate --since origin/mainOnly the sources whose files, overrides, or diagram changed since the ref, and every source when the settings file changed.

A drifted diagram prints its changes by meaning, from the same comparison as compare_diagrams: reordered or reformatted lines are no change, and a renamed id is a rename.

text
drift: docs/infra.diagram (infra, terraform from infra/**/*.tf)  Added 1 node (logs). Removed 1 edge (web -> db).  + node logs  - edge web -> dbfailed: 4 diagram(s) checked, 1 drifted, 0 error(s)

The output is the same for the same files on every machine. A generated file starts with a comment that names its source and its overrides file:

text
# Generated by plotwork generate from the source infra (terraform: infra/**/*.tf).# The next run writes over this file. Put changes in docs/infra.overrides.json, or run plotwork generate --capture# after an edit to keep it there, then run plotwork generate.

3. Overrides

An overrides file is JSON beside the diagram. It is applied after every import, so a change to the Terraform and a change to a label never fight.

json
{  "$schema": "https://docs.plotwork.dev/schemas/plotwork.overrides.json",  "header": {    "options": {      "direction": "right",      "legend": "off"    }  },  "items": {    "aws_s3_bucket.assets": {      "label": "Assets bucket",      "tone": "orange"    },    "aws_instance.web": {      "group": "compute",      "type": "function"    },    "aws_internet_gateway.gw": {      "hide": true    }  },  "groups": {    "compute": {      "label": "Compute",      "type": "app-tier",      "parent": "aws_vpc.main"    }  },  "edges": [    {      "from": "aws_instance.web",      "to": "aws_lb.web",      "label": "health",      "arrow": "-->"    },    {      "from": "aws_db_instance.orders",      "to": "aws_db_subnet_group.orders",      "hide": true    }  ],  "lines": [    {      "statement": "node runbook step \"Runbook\" sub=\"Rotated nightly\"",      "into": "compute"    }  ]}
SectionWhat it changes
headertitle, and header options such as direction, icons, legend, or desc. null takes an option away.
itemsFor each box, group, or table, by name: label, sub, icon, tone, type (a node type, or a group's type), group (move it into a group; null is the top level), other options, and hide.
groupsGroups to add, by the id they get: label, type, sub, icon, parent. Move items into them with group.
edgesEdges to add (from, to, label, arrow, options), or with hide, the import's edges from one end to the other to take away.
linesAny other line, such as a note box, as it is, optionally into a group.

How a thing is named

An entry names a thing by the name the file gives it, which does not change when the file grows. The id in the diagram can: two resources named web become web and web_2 in the order they are read.

FormatNameExample
terraform, terraform-planThe resource address, after its module and with its copy's key, the same for a plan as for the filesaws_s3_bucket.logs, module.network.azurerm_subnet.this["app"]
bicep, armThe symbolic name, after its modulemodule.data.vault, vnet/subnets/app, queue['orders']
pulumiThe logical name, or the URN when two share onelabels
cloudformationThe logical idOrdersTable
cdkThe construct path, without the Resource that wraps itOrdersApi/Ordering/Handler, OrdersApi/Ordering
composeservice:, network:, or volume: and its nameservice:api
kubernetes, helm, kustomize (built)Namespace, kind, and name; a Helm release: or chart: framepayments/Deployment/api, release:shop
kustomize (a folder)frame:, kustomization:, file:, patch:, or generator: and its pathfile:base/deployment.yaml
backstageThe entity referencecomponent:default/orders-api
structurizrThe DSL identifier, its path with hierarchical identifiers, or the path of names; group: and its pathbanking.api, group:banking/Payments
asyncapiapp: and the title, channel: and the address, broker: and the protocolchannel:orders.placed.v1
github-actionsjob: and the job id; on is the triggerjob:test
gitlab-cijob:, stage:, or trigger: and the namejob:unit-tests, stage:deploy
azure-pipelinesstage: and its name; job: and Stage.Job, or the job alone in a file of only jobsjob:Build.UnitTests
sql, prisma, drizzleThe table nameusers
The othersThe id in the diagramorders

The id in the diagram works for every format too, when it is stable enough for you.

When the importer changes

A new version of the importer can draw the same files differently: a Terraform stack whose local modules are now framed, or whose for_each copies are now drawn one by one. A stack the new rules have nothing to say about (no module, no for_each or count) is drawn exactly as before, byte for byte. When an upgrade does redraw a diagram, generate --check reports it as drift; run plotwork generate once, review the change, and commit it.

When the file changes

An entry whose thing is gone is a warning, S201, on its line of the overrides file: Stale override for aws_s3_bucket.logs: the import no longer draws it. An entry the diagram cannot take, such as an added group whose id is taken, is S202. Neither fails a run; remove or rename the entry. A change that leaves the diagram with an error, such as an unknown node type, is that error, on its line of the generated file.

Keep a hand edit

sh
npx @buun_group/plotwork generate --capture --source infra

--capture compares the file you edited with what the import and the current overrides make, writes each difference into the overrides file (labels, options, types, moves, hidden and added boxes, groups, edges, and lines), and then makes the diagram again, so it comes back the same with its comment on top. A source split into several diagrams is captured part by part, each against its own file, into its one overrides file.

4. Fail a pull request on drift

yaml
name: diagrams
on:  pull_request:
permissions:  contents: read
jobs:  drift:    runs-on: ubuntu-latest    steps:      - uses: actions/checkout@v7        with:          persist-credentials: false      - uses: plotwork-official/plotwork-action@dcec4036897b73bd3ea724b3991f4fd0ac880757 # v1.0.0-beta.14        with:          command: generate

The samples pin the latest release of the Action by its commit. @v1 becomes available at the first stable release.

Each drifted diagram is an error annotation on its file, and the job summary has a table of every diagram: where it comes from, the nodes and edges added and removed, its status, a link to its file, and the engine's table of what changed. A job summary cannot show a picture from the runner, so the changes are written out; the pull request from write mode, and a published diagram, show the pictures.

5. Write the diagrams back

Open a pull request

On a push to the default branch, or on a schedule, write mode makes the diagrams again and pushes them to one branch with one pull request. Each run replaces that branch with one commit on the latest base and edits the open pull request rather than opening another. Once nothing drifts, the next run closes that pull request with a comment and deletes its branch, when the branch's last commit is the action's own.

yaml
name: diagrams-regenerate
on:  push:    branches: [main]  schedule:    - cron: '0 4 * * *'
permissions:  contents: read
jobs:  regenerate:    runs-on: ubuntu-latest    permissions:      contents: write      pull-requests: write    steps:      - uses: actions/checkout@v7      - id: plotwork        uses: plotwork-official/plotwork-action@dcec4036897b73bd3ea724b3991f4fd0ac880757 # v1.0.0-beta.14        with:          command: generate          mode: write          pr-branch: plotwork/diagrams          pr-title: 'docs: regenerate diagrams from their sources'
SettingWhy
contents: write, pull-requests: writeTo push the branch and open the pull request.
Settings, Actions, General, "Allow GitHub Actions to create and approve pull requests"GitHub refuses a pull request from the workflow's token without it.
github-tokenA pull request opened with the workflow's own token starts no workflow, so its checks do not run. Pass a GitHub App or personal token to have them run.

on-drift: pr refuses a pull_request run: such a run is on a merge commit, not on a branch.

Commit to the branch

yaml
on:  pull_request:
permissions:  contents: read
jobs:  regenerate:    runs-on: ubuntu-latest    permissions:      contents: write    steps:      - uses: actions/checkout@v7        with:          ref: ${{ github.head_ref }}      - uses: plotwork-official/plotwork-action@dcec4036897b73bd3ea724b3991f4fd0ac880757 # v1.0.0-beta.14        with:          command: generate          mode: write          on-drift: commit          commit-message: 'docs: regenerate diagrams'

The commit carries the trailer Plotwork-Generated: true. A run started by that commit does nothing, so a token that does start workflows never loops. A pull request from a fork cannot be pushed to with the workflow's token. on-drift: fail writes the diagrams in the workspace and fails, for a later step of your own.

6. Publish

With publish: true and a key in PLOTWORK_API_KEY, write mode publishes each source that has a publish section.

json
{  "name": "infra",  "from": "infra/**/*.tf",  "format": "terraform",  "out": "docs/infra.diagram",  "publish": {    "organization": "acme",    "folder": "Platform/Infra"  }}
publish fieldMeaning
aliasA stable link of a personal key: one address that always shows the latest version. See Hosted diagrams.
organizationFile it in this organisation's library. Left out with no alias, the organisation of brand.organization is used. An organisation key publishes only to its own organisation, and needs neither: the API names the key's organisation and the diagram goes to its library. An organisation's diagrams have no public address, so an alias makes no stable link there, and the run says so.
folder, name, themeWhere it is filed, its name (the title when left out), and light, dark, or auto.
embedSourceWrite the diagram source into the hosted picture. Left out, it is not.
requiredFail the run when it cannot be published, such as with no key. Left out, a skipped publish is a notice.
yaml
name: diagrams-publish
on:  push:    branches: [main]
permissions:  contents: read
jobs:  publish:    runs-on: ubuntu-latest    permissions:      contents: write      pull-requests: write    steps:      - uses: actions/checkout@v7      - uses: plotwork-official/plotwork-action@dcec4036897b73bd3ea724b3991f4fd0ac880757 # v1.0.0-beta.14        with:          command: generate          mode: write          publish: true        env:          PLOTWORK_API_KEY: ${{ secrets.PLOTWORK_API_KEY }}

The key is masked in the log and never printed. The repository's defaults and brand.organization go with the source, so the hosted picture takes the organisation's brand and icon packs when its plan includes them. The links are in the job summary, with the pictures, and in the published output.

7. Inputs and outputs

InputDefaultMeaning
commandcheckgenerate for this guide.
modecheckcheck fails on drift and writes nothing; write makes the diagrams again.
on-driftprIn write mode: pr, commit, or fail.
pr-branchplotwork/diagramsThe branch pr pushes to. The action owns it.
pr-titledocs: regenerate diagrams from their sourcesThe pull request's title.
commit-messagedocs: regenerate diagrams from their sourcesThe commit message, before the trailer.
publishfalsePublish the sources that have a publish section, in write mode.
github-token${{ github.token }}The token that pushes and opens the pull request.
sinceA git ref: only the sources whose files changed since it run. Check out with fetch-depth: 0.
sarif-file, junit-fileFiles to write a SARIF report (for github/codeql-action/upload-sarif) and a JUnit report to.
config, working-directoryAs for every command. See Keep diagrams in sync in CI.
OutputMeaning
staleDiagrams that drifted.
filesThe drifted diagrams in check mode; the files written in write mode.
changesA JSON array, one entry per drifted diagram: its name, its file, the number of changes, and the sentence that names them.
pull-requestThe address of the pull request opened or edited, or empty.
publishedA JSON array of each source published: name, kind (personal or organization), url, stableLink, svgUrl (an organisation's picture, served to the key), id, and organization.
errors, warningsThe problems found.

8. Limits

LimitValue
Sources in one settings file250, in a file of at most 256 KB; a larger repository gives each team a settings file of its own and runs one job per working-directory
Patterns in one from20
Files one source reads2,000, at the local import limits, which only guard memory
An overrides file128 KB, and 500 entries in each section
A source too big for one pictureWritten as several .diagram files, infra.diagram, infra.2.diagram and on; a Markdown fence holds one diagram, so such a source needs a .diagram out or fewer things chosen with options
Parts a source no longer hasRemoved with their pictures, but only a file that carries that source's generated comment

See also

PageWhy
Importing from code and dataWhat each format draws, and its options
Repository settingsThe rest of plotwork.config.json: defaults, rules, and brand
Keep diagrams in sync in CIThe other commands of the GitHub Action
Hosted diagramsLinks, stable links, and what a plan hosts
OrganisationsOrganisation keys and the shared library
Enterprise rolloutA reusable workflow, required checks, code scanning, many teams in one repository, and air-gapped runners
plotwork-examples (opens in a new tab)A working repository with every format, and the smoke test of each release