# Diagrams as code

> Page: https://docs.plotwork.dev/diagrams-as-code. Index of every page: https://docs.plotwork.dev/llms.txt

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](https://github.com/plotwork-official/plotwork-examples).

## TL;DR

| Question | Answer |
|---|---|
| Where do I say what to draw? | In `sources` in [`plotwork.config.json`](https://docs.plotwork.dev/repository-settings.md): 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](https://github.com/plotwork-official/plotwork-examples): 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*"
        ]
      }
    }
  ]
}
```

| Field | Meaning |
|---|---|
| `from` | A 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. |
| `format` | `sql`, `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](https://docs.plotwork.dev/importing-from-code-and-data.md) describes. |
| `out` | A `.diagram` file, or a Markdown file and a fence id: `docs/architecture.md#services`. |
| `name` | A short name for `--source` and the summary. The `out` path when left out. |
| `view`, `title` | The 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. |
| `options` | The 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. |
| `overrides` | The overrides file. Left out: `infra.diagram` has `infra.overrides.json`, and the fence `services` of `docs/architecture.md` has `docs/architecture.services.overrides.json`. |
| `pictures` | For a `.diagram` out, also write its light and dark SVG beside it. On when left out. |
| `schemes` | `light-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. |
| `publish` | Where 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:

````md
## Services

```diagram id=services
```
````

### One picture for both themes

A light SVG opened on GitHub, or put in a page with a plain ``, 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
```

| Command | What it does |
|---|---|
| `generate` | Makes every diagram and writes the ones that changed: the `.diagram` file and its SVGs, or the fence and its picture block. |
| `generate --check` | Writes nothing. Exits 1 when a diagram differs from what its files make, and prints what changed. |
| `generate --source infra` | One source only. |
| `generate --capture` | First writes the hand edits of each generated diagram into its overrides file. |
| `generate --format github` | Each 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/main` | Only 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 -> db
failed: 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"
    }
  ]
}
```

| Section | What it changes |
|---|---|
| `header` | `title`, and header `options` such as `direction`, `icons`, `legend`, or `desc`. `null` takes an option away. |
| `items` | For 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`. |
| `groups` | Groups to add, by the id they get: `label`, `type`, `sub`, `icon`, `parent`. Move items into them with `group`. |
| `edges` | Edges to add (`from`, `to`, `label`, `arrow`, `options`), or with `hide`, the import's edges from one end to the other to take away. |
| `lines` | Any 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.

| Format | Name | Example |
|---|---|---|
| `terraform`, `terraform-plan` | The resource address, after its module and with its copy's key, the same for a plan as for the files | `aws_s3_bucket.logs`, `module.network.azurerm_subnet.this["app"]` |
| `bicep`, `arm` | The symbolic name, after its module | `module.data.vault`, `vnet/subnets/app`, `queue['orders']` |
| `pulumi` | The logical name, or the URN when two share one | `labels` |
| `cloudformation` | The logical id | `OrdersTable` |
| `cdk` | The construct path, without the `Resource` that wraps it | `OrdersApi/Ordering/Handler`, `OrdersApi/Ordering` |
| `compose` | `service:`, `network:`, or `volume:` and its name | `service:api` |
| `kubernetes`, `helm`, `kustomize` (built) | Namespace, kind, and name; a Helm `release:` or `chart:` frame | `payments/Deployment/api`, `release:shop` |
| `kustomize` (a folder) | `frame:`, `kustomization:`, `file:`, `patch:`, or `generator:` and its path | `file:base/deployment.yaml` |
| `backstage` | The entity reference | `component:default/orders-api` |
| `structurizr` | The DSL identifier, its path with hierarchical identifiers, or the path of names; `group:` and its path | `banking.api`, `group:banking/Payments` |
| `asyncapi` | `app:` and the title, `channel:` and the address, `broker:` and the protocol | `channel:orders.placed.v1` |
| `github-actions` | `job:` and the job id; `on` is the trigger | `job:test` |
| `gitlab-ci` | `job:`, `stage:`, or `trigger:` and the name | `job:unit-tests`, `stage:deploy` |
| `azure-pipelines` | `stage:` and its name; `job:` and `Stage.Job`, or the job alone in a file of only jobs | `job:Build.UnitTests` |
| `sql`, `prisma`, `drizzle` | The table name | `users` |
| The others | The id in the diagram | `orders` |

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'
```

| Setting | Why |
|---|---|
| `contents: write`, `pull-requests: write` | To 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-token` | A 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` field | Meaning |
|---|---|
| `alias` | A stable link of a personal key: one address that always shows the latest version. See [Hosted diagrams](https://docs.plotwork.dev/hosted-diagrams.md). |
| `organization` | File 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`, `theme` | Where it is filed, its name (the title when left out), and `light`, `dark`, or `auto`. |
| `embedSource` | Write the diagram source into the hosted picture. Left out, it is not. |
| `required` | Fail 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

| Input | Default | Meaning |
|---|---|---|
| `command` | `check` | `generate` for this guide. |
| `mode` | `check` | `check` fails on drift and writes nothing; `write` makes the diagrams again. |
| `on-drift` | `pr` | In write mode: `pr`, `commit`, or `fail`. |
| `pr-branch` | `plotwork/diagrams` | The branch `pr` pushes to. The action owns it. |
| `pr-title` | `docs: regenerate diagrams from their sources` | The pull request's title. |
| `commit-message` | `docs: regenerate diagrams from their sources` | The commit message, before the trailer. |
| `publish` | `false` | Publish the sources that have a `publish` section, in write mode. |
| `github-token` | `${{ github.token }}` | The token that pushes and opens the pull request. |
| `since` | | A git ref: only the sources whose files changed since it run. Check out with `fetch-depth: 0`. |
| `sarif-file`, `junit-file` | | Files to write a SARIF report (for `github/codeql-action/upload-sarif`) and a JUnit report to. |
| `config`, `working-directory` | | As for every command. See [Keep diagrams in sync in CI](https://docs.plotwork.dev/diagrams-in-ci.md). |

| Output | Meaning |
|---|---|
| `stale` | Diagrams that drifted. |
| `files` | The drifted diagrams in check mode; the files written in write mode. |
| `changes` | A JSON array, one entry per drifted diagram: its name, its file, the number of changes, and the sentence that names them. |
| `pull-request` | The address of the pull request opened or edited, or empty. |
| `published` | A 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`, `warnings` | The problems found. |

## 8. Limits

| Limit | Value |
|---|---|
| Sources in one settings file | 250, 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 `from` | 20 |
| Files one source reads | 2,000, at the local import limits, which only guard memory |
| An overrides file | 128 KB, and 500 entries in each section |
| A source too big for one picture | Written 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 has | Removed with their pictures, but only a file that carries that source's generated comment |

## See also

| Page | Why |
|---|---|
| [Importing from code and data](https://docs.plotwork.dev/importing-from-code-and-data.md) | What each format draws, and its options |
| [Repository settings](https://docs.plotwork.dev/repository-settings.md) | The rest of `plotwork.config.json`: defaults, rules, and brand |
| [Keep diagrams in sync in CI](https://docs.plotwork.dev/diagrams-in-ci.md) | The other commands of the GitHub Action |
| [Hosted diagrams](https://docs.plotwork.dev/hosted-diagrams.md) | Links, stable links, and what a plan hosts |
| [Organisations](https://docs.plotwork.dev/organisations.md) | Organisation keys and the shared library |
| [Enterprise rollout](https://docs.plotwork.dev/enterprise-rollout.md) | A reusable workflow, required checks, code scanning, many teams in one repository, and air-gapped runners |
| [plotwork-examples](https://github.com/plotwork-official/plotwork-examples) | A working repository with every format, and the smoke test of each release |
