d2 Diagram
Generate architecture diagrams as text with d2, then render
and embed them. Three scripts support the workflow:
scripts/icons.py — resolve exact icon: URLs for AWS/GCP/Azure services.
scripts/render.py — render/validate/format .d2 files and embed them in markdown.
scripts/title_pills.py — post-process an SVG to draw masking pills behind group
titles (also wired into render.py --title-pills).
Plus shared assets: assets/styles.d2 (importable layout + classes) and
assets/themes.json (named render presets) — see Theming.
Workflow
- Write a
.d2 file. Keep the source next to the doc it illustrates so it
stays re-renderable. Use containers for tiers/boundaries and classes for
consistent styling.
- For cloud diagrams, resolve icons first with
icons.py and paste the URLs
verbatim — never hand-write icons.terrastruct.com URLs (they're URL-encoded
and unguessable).
- Validate, then render with
render.py (SVG by default).
- Embed in markdown with
render.py --md if the diagram belongs in docs.
Syntax cheat-sheet
# Objects (default shape is rectangle) and connections
user: User { shape: person }
api: API Gateway { shape: hexagon }
cache: Redis { shape: cylinder } # cylinder = datastore
user -> api: HTTPS # -> directed, -- plain, <-> bidirectional
# Containers (nesting) + cross-container edges
backend: Backend {
svc: Order Service
db: Postgres { shape: cylinder }
}
api -> backend.svc
backend.svc -> backend.db: SQL
# Reusable styles via classes
classes: {
service: { style: { fill: "#E6F4EA"; stroke: "#34A853" } }
}
backend.svc.class: service
# Inline style + icon (see icons.py for cloud URLs)
queue: Event Bus { shape: queue; style.fill: "#FEF7E0" }
lambda: Worker { icon: https://icons.terrastruct.com/aws%2FCompute%2FAWS-Lambda.svg }
# Database schema / ERD
orders: { shape: sql_table; id: uuid; total: int }
Useful shapes: rectangle (default), square, cylinder (datastores),
person (actors), hexagon, cloud, queue, package, page, step,
diamond, callout, sql_table. Icons render top-left on containers and
centered on plain shapes; icon: decorates a shape, while shape: image makes
the image be the shape.
Software architecture recipe
Group components into layered containers (client → edge → application → data),
give each tier a class for visual consistency, and label connections with the
protocol or action. Render with elk (--layout elk, or set
layout-engine: elk) — its orthogonal routing keeps labels and edges from
overlapping; raise --elk-node-spacing if it's still tight.
See examples/software-arch.d2
for a complete 3-tier example (containers, classes, a sql_table, and a request
flow). General guidance on architecture-diagram structure:
Atlassian: architecture diagrams.
Cloud architecture recipe
Resolve every service icon before writing the diagram:
python scripts/icons.py search "lambda" --provider aws
python scripts/icons.py search "cloud storage" --provider gcp
python scripts/icons.py search "app service" --provider azure
Each result prints a paste-ready icon: <url> line. --json for machine
output; --limit N to widen; categories/providers to browse. The bundled
index (assets/icons.csv) is a snapshot of the most common AWS/GCP/Azure
services — if a service isn't found, try broader terms or a sibling service,
or browse https://icons.terrastruct.com.
Use one provider's icon set per diagram, and group resources by their real
boundaries (cloud account → region/VPC → subnet, or subscription → resource
group). Style the cloud boundary to match the brand (e.g. AWS #FF9900).
See examples/aws-arch.d2 for a complete, validated AWS example whose
icon URLs all came from icons.py.
Other diagram types
- Sequence diagrams (auth flows, request/response): set
shape: sequence_diagram
at the root; child objects become lifelines and connections become ordered
messages. A self-edge is a self-call; a -> a: label. See examples/auth-flow.d2.
- User-journey / flowcharts:
oval start/end, plain steps, diamond decisions,
and edges labeled yes/no for branches and retry loops. See examples/user-flow.d2
(also shows sketch style).
Rendering & embedding
# SVG (default; no dependencies, ideal for web/markdown). Output path derived from input.
python scripts/render.py examples/software-arch.d2
# Options: format, theme, layout, spacing, sketch, padding.
# --layout elk + --elk-node-spacing is the go-to fix for a cramped/overlapping diagram.
python scripts/render.py examples/aws-arch.d2 -o out.svg \
--theme 1 --layout elk --elk-node-spacing 100 --pad 40
# SVG with masking pills behind every group title (lines can't cross a title)
python scripts/render.py examples/software-arch.d2 --title-pills
# Apply a named theme preset (see Theming below); --list-presets to see them
python scripts/render.py examples/software-arch.d2 --preset c4
# Validate or autoformat before committing
python scripts/render.py examples/aws-arch.d2 --validate
python scripts/render.py examples/aws-arch.d2 --fmt
# Render and embed (or update) in a markdown file, between markers so re-runs
# replace the image in place rather than appending:
# <!-- d2:arch -->  <!-- /d2:arch -->
python scripts/render.py examples/software-arch.d2 --md README.md --md-marker arch
Graceful degradation (both handled automatically, no crash):
- PNG/PDF make d2 launch a headless Chromium (downloaded on first use). If it
can't be installed/launched (e.g. restricted network),
render.py falls back to
SVG and prints how to enable raster output.
- Icon bundling: by default d2 inlines remote icons into the output for a
self-contained file. If the icon host is unreachable at render time,
render.py
retries with --no-bundle, keeping icons as remote refs that load when the image
is viewed online. Pass --no-bundle explicitly to force this.
Readability — preventing label / icon / line overlap
Overlapping labels, edges, and icons are the most common quality problem. Apply
these rules when authoring (they are baked into the examples):
Use the ELK layout for architecture & flow diagrams — --layout elk (or
layout-engine: elk in vars.d2-config). ELK routes edges orthogonally and
places labels with far less overlap than the default dagre. This is the single
biggest win. If a diagram is still cramped, spread it out:
--layout elk --elk-node-spacing 100 (and/or --elk-padding "[top=60,left=50,bottom=50,right=50]").
Keep edge labels short — ideally ≤ 3 words (3. GraphQL (Bearer), not
3. GraphQL query with bearer access token). Push detail into the node label
or drop it. Wrap any unavoidably long label with \n.
Don't put icon: on a container that also carries an important label. A
container anchors both its label and its icon at the top, so on tight layouts
they crowd each other. Put icons on leaf nodes; label grouping containers
(VPCs, subnets, tiers) with text only.
Keep grouping-container labels off the routed edges. A container label
defaults to top-center, which is exactly where ELK routes edges into the box,
so lines draw over it. Two good options:
Titled box (preferred for VPC/subnet/tier groups) — give the container a
fill, a stroke, a bold font, and label.near: top-left. The label then
sits on a real background with a border, and d2 reserves a title band at the top
of the box so edges route into the children below it — backgrounds and no
lines over the title:
classes: {
subnet: {
label.near: top-left
style: {fill: "#EDEFF5"; stroke: "#5B6B8C"; stroke-width: 2; stroke-dash: 3; bold: true; font-color: "#33415C"}
}
}
vnet: VNet 10.0.0.0/16 {
snet_app: snet-app 10.0.3.0/24 {class: subnet; api: API; worker: Worker}
}
Outside label — label.near: outside-top-left (or outside-top-center)
floats the label above the border, fully clear of edges, when you don't want a
filled box. (d2 has no background behind an outside label.)
Either way, don't shrink these labels with a tiny font-size; the readable
default is fine.
Title pills (SVG, strongest guarantee). d2 draws connections after shape
labels, so in a dense diagram a routed line can still clip a group title. Pass
render.py --title-pills (SVG output only): it post-processes the SVG to draw an
opaque, bordered pill behind every group/container title and re-renders it on top
of the edges, so no line can show through. Pills inherit each container's own
fill/border (override with --pill-fill / --pill-stroke). scripts/title_pills.py
can also run standalone on any d2 SVG. This is the most robust way to satisfy
"lines must never cross a group title."
One short label per icon'd node. A node with an icon and a long multi-line
label squeezes the icon — prefer a concise name plus the icon.
Give the diagram air with --pad 40 (or more) and split very large systems
into multiple focused diagrams.
Sequence diagrams ignore the layout engine, so readability there is all about
concise, \n-wrapped message labels. d2 masks the lifeline/arrow behind each
label so lines don't strike through text — this is honored by d2's native
SVG/PNG export (and any compliant SVG renderer). If you rasterize the SVG with a
tool that ignores SVG masks (some rsvg/cairo builds), lines can appear to run
through labels; prefer render.py's own PNG export, which uses d2 directly.
Always eyeball the rendered output. If labels still collide, in order: switch
to elk, raise --elk-node-spacing, shorten labels, then bump --pad.
Theming
d2 ships built-in themes selected by numeric id (--theme / --dark-theme, or
in-file vars.d2-config.theme-id). Run d2 themes for the list — e.g. Neutral
0/1, Flagship 3, Aubergine 7, Origami 302, C4 303 (good for architecture);
dark 200/201. Two skill assets make theming reusable:
Best practices
- Run
--validate and --fmt before committing; commit the .d2 source, not just
the rendered image, so diagrams can be regenerated.
- Prefer
classes over repeating inline styles; keep labels short and action-oriented.
- For very large systems, split into multiple focused diagrams.
Requirements
assets/icons.csv is bundled verbatim from the public
tf2d2/terrastruct-icons project
(columns Cloud,Title,URL). Terrastruct does not change or expire existing icon
URLs, so the bundled values stay valid.
1---2name: d2-diagram3description: Author software- and cloud-architecture diagrams as code with d2 (d2lang.com), render them to SVG/PNG/PDF, and embed them into markdown. Includes a lookup for the correct AWS/GCP/Azure service icons (whose hosted URLs are impossible to guess) and a renderer that degrades gracefully when raster export or the icon host is unavailable. Use when asked to draw, diagram, or visualize a system, service topology, request flow, or cloud architecture — or to add such a diagram to docs/a README.4license: MIT5---67# d2 Diagram89Generate architecture diagrams as text with [d2](https://d2lang.com), then render10and embed them. Three scripts support the workflow:1112- `scripts/icons.py` — resolve exact `icon:` URLs for AWS/GCP/Azure services.13- `scripts/render.py` — render/validate/format `.d2` files and embed them in markdown.14- `scripts/title_pills.py` — post-process an SVG to draw masking pills behind group15 titles (also wired into `render.py --title-pills`).1617Plus shared assets: `assets/styles.d2` (importable layout + classes) and18`assets/themes.json` (named render presets) — see **Theming**.1920## Workflow21221. **Write a `.d2` file.** Keep the source next to the doc it illustrates so it23 stays re-renderable. Use containers for tiers/boundaries and `classes` for24 consistent styling.252. **For cloud diagrams, resolve icons first** with `icons.py` and paste the URLs26 verbatim — never hand-write `icons.terrastruct.com` URLs (they're URL-encoded27 and unguessable).283. **Validate, then render** with `render.py` (SVG by default).294. **Embed in markdown** with `render.py --md` if the diagram belongs in docs.3031## Syntax cheat-sheet3233```d234# Objects (default shape is rectangle) and connections35user: User { shape: person }36api: API Gateway { shape: hexagon }37cache: Redis { shape: cylinder } # cylinder = datastore38user -> api: HTTPS # -> directed, -- plain, <-> bidirectional3940# Containers (nesting) + cross-container edges41backend: Backend {42 svc: Order Service43 db: Postgres { shape: cylinder }44}45api -> backend.svc46backend.svc -> backend.db: SQL4748# Reusable styles via classes49classes: {50 service: { style: { fill: "#E6F4EA"; stroke: "#34A853" } }51}52backend.svc.class: service5354# Inline style + icon (see icons.py for cloud URLs)55queue: Event Bus { shape: queue; style.fill: "#FEF7E0" }56lambda: Worker { icon: https://icons.terrastruct.com/aws%2FCompute%2FAWS-Lambda.svg }5758# Database schema / ERD59orders: { shape: sql_table; id: uuid; total: int }60```6162Useful shapes: `rectangle` (default), `square`, `cylinder` (datastores),63`person` (actors), `hexagon`, `cloud`, `queue`, `package`, `page`, `step`,64`diamond`, `callout`, `sql_table`. Icons render top-left on containers and65centered on plain shapes; `icon:` decorates a shape, while `shape: image` makes66the image *be* the shape.6768## Software architecture recipe6970Group components into layered containers (client → edge → application → data),71give each tier a `class` for visual consistency, and label connections with the72protocol or action. **Render with `elk`** (`--layout elk`, or set73`layout-engine: elk`) — its orthogonal routing keeps labels and edges from74overlapping; raise `--elk-node-spacing` if it's still tight.75See **`examples/software-arch.d2`**76for a complete 3-tier example (containers, classes, a `sql_table`, and a request77flow). General guidance on architecture-diagram structure:78[Atlassian: architecture diagrams](https://www.atlassian.com/work-management/project-management/architecture-diagram).7980## Cloud architecture recipe81821. **Resolve every service icon** before writing the diagram:8384 ```bash85 python scripts/icons.py search "lambda" --provider aws86 python scripts/icons.py search "cloud storage" --provider gcp87 python scripts/icons.py search "app service" --provider azure88 ```8990 Each result prints a paste-ready `icon: <url>` line. `--json` for machine91 output; `--limit N` to widen; `categories`/`providers` to browse. The bundled92 index (`assets/icons.csv`) is a snapshot of the most common AWS/GCP/Azure93 services — if a service isn't found, try broader terms or a sibling service,94 or browse <https://icons.terrastruct.com>.95962. **Use one provider's icon set per diagram**, and group resources by their real97 boundaries (cloud account → region/VPC → subnet, or subscription → resource98 group). Style the cloud boundary to match the brand (e.g. AWS `#FF9900`).991003. See **`examples/aws-arch.d2`** for a complete, validated AWS example whose101 icon URLs all came from `icons.py`.102103## Other diagram types104105- **Sequence diagrams** (auth flows, request/response): set `shape: sequence_diagram`106 at the root; child objects become lifelines and connections become ordered107 messages. A self-edge is a self-call; `a -> a: label`. See **`examples/auth-flow.d2`**.108- **User-journey / flowcharts**: `oval` start/end, plain steps, `diamond` decisions,109 and edges labeled `yes`/`no` for branches and retry loops. See **`examples/user-flow.d2`**110 (also shows `sketch` style).111112## Rendering & embedding113114```bash115# SVG (default; no dependencies, ideal for web/markdown). Output path derived from input.116python scripts/render.py examples/software-arch.d2117118# Options: format, theme, layout, spacing, sketch, padding.119# --layout elk + --elk-node-spacing is the go-to fix for a cramped/overlapping diagram.120python scripts/render.py examples/aws-arch.d2 -o out.svg \121 --theme 1 --layout elk --elk-node-spacing 100 --pad 40122123# SVG with masking pills behind every group title (lines can't cross a title)124python scripts/render.py examples/software-arch.d2 --title-pills125126# Apply a named theme preset (see Theming below); --list-presets to see them127python scripts/render.py examples/software-arch.d2 --preset c4128129# Validate or autoformat before committing130python scripts/render.py examples/aws-arch.d2 --validate131python scripts/render.py examples/aws-arch.d2 --fmt132133# Render and embed (or update) in a markdown file, between markers so re-runs134# replace the image in place rather than appending:135# <!-- d2:arch -->  <!-- /d2:arch -->136python scripts/render.py examples/software-arch.d2 --md README.md --md-marker arch137```138139**Graceful degradation** (both handled automatically, no crash):140141- **PNG/PDF** make d2 launch a headless Chromium (downloaded on first use). If it142 can't be installed/launched (e.g. restricted network), `render.py` falls back to143 SVG and prints how to enable raster output.144- **Icon bundling**: by default d2 inlines remote icons into the output for a145 self-contained file. If the icon host is unreachable at render time, `render.py`146 retries with `--no-bundle`, keeping icons as remote refs that load when the image147 is viewed online. Pass `--no-bundle` explicitly to force this.148149## Readability — preventing label / icon / line overlap150151Overlapping labels, edges, and icons are the most common quality problem. Apply152these rules when authoring (they are baked into the examples):153154- **Use the ELK layout for architecture & flow diagrams** — `--layout elk` (or155 `layout-engine: elk` in `vars.d2-config`). ELK routes edges orthogonally and156 places labels with far less overlap than the default `dagre`. This is the single157 biggest win. If a diagram is still cramped, spread it out:158 `--layout elk --elk-node-spacing 100` (and/or `--elk-padding "[top=60,left=50,bottom=50,right=50]"`).159- **Keep edge labels short** — ideally ≤ 3 words (`3. GraphQL (Bearer)`, not160 `3. GraphQL query with bearer access token`). Push detail into the *node* label161 or drop it. Wrap any unavoidably long label with `\n`.162- **Don't put `icon:` on a container that also carries an important label.** A163 container anchors both its label and its icon at the top, so on tight layouts164 they crowd each other. Put icons on **leaf** nodes; label grouping containers165 (VPCs, subnets, tiers) with text only.166- **Keep grouping-container labels off the routed edges.** A container label167 defaults to `top-center`, which is exactly where ELK routes edges into the box,168 so lines draw over it. Two good options:169 - **Titled box (preferred for VPC/subnet/tier groups)** — give the container a170 `fill`, a `stroke`, a **bold** font, and `label.near: top-left`. The label then171 sits on a real background with a border, and d2 reserves a title band at the top172 of the box so edges route into the children *below* it — backgrounds **and** no173 lines over the title:174175 ```d2176 classes: {177 subnet: {178 label.near: top-left179 style: {fill: "#EDEFF5"; stroke: "#5B6B8C"; stroke-width: 2; stroke-dash: 3; bold: true; font-color: "#33415C"}180 }181 }182 vnet: VNet 10.0.0.0/16 {183 snet_app: snet-app 10.0.3.0/24 {class: subnet; api: API; worker: Worker}184 }185 ```186 - **Outside label** — `label.near: outside-top-left` (or `outside-top-center`)187 floats the label above the border, fully clear of edges, when you *don't* want a188 filled box. (d2 has no background behind an outside label.)189190 Either way, don't shrink these labels with a tiny `font-size`; the readable191 default is fine.192- **Title pills (SVG, strongest guarantee).** d2 draws connections *after* shape193 labels, so in a dense diagram a routed line can still clip a group title. Pass194 `render.py --title-pills` (SVG output only): it post-processes the SVG to draw an195 opaque, bordered pill behind every group/container title and re-renders it *on top*196 of the edges, so no line can show through. Pills inherit each container's own197 fill/border (override with `--pill-fill` / `--pill-stroke`). `scripts/title_pills.py`198 can also run standalone on any d2 SVG. This is the most robust way to satisfy199 "lines must never cross a group title."200- **One short label per icon'd node.** A node with an icon *and* a long multi-line201 label squeezes the icon — prefer a concise name plus the icon.202- **Give the diagram air** with `--pad 40` (or more) and split very large systems203 into multiple focused diagrams.204- **Sequence diagrams** ignore the layout engine, so readability there is all about205 concise, `\n`-wrapped message labels. d2 masks the lifeline/arrow *behind* each206 label so lines don't strike through text — this is honored by d2's native207 SVG/PNG export (and any compliant SVG renderer). If you rasterize the SVG with a208 tool that ignores SVG masks (some `rsvg`/`cairo` builds), lines can appear to run209 through labels; prefer `render.py`'s own PNG export, which uses d2 directly.210- **Always eyeball the rendered output.** If labels still collide, in order: switch211 to `elk`, raise `--elk-node-spacing`, shorten labels, then bump `--pad`.212213## Theming214215d2 ships built-in themes selected by numeric id (`--theme` / `--dark-theme`, or216in-file `vars.d2-config.theme-id`). Run `d2 themes` for the list — e.g. Neutral2170/1, Flagship 3, Aubergine 7, Origami 302, **C4 303** (good for architecture);218dark 200/201. Two skill assets make theming reusable:219220- **Named render presets** — `assets/themes.json` maps friendly names to a theme id221 plus default render options (layout, pad, title-pills). Apply with222 `render.py --preset <name>` (any explicit flag still wins); `--list-presets` shows223 them. Presets: `default`, `neutral-grey`, `terrastruct`, `aubergine`, `origami`,224 `c4`, `dark`, `auto-dark`, `sketch`. Add your own by editing the JSON.225- **Shared style partial** — `assets/styles.d2` sets the elk layout default and226 reusable `classes` (titled-box `group`/`subnet`; cloud `aws`/`azure`/`gcp` brand227 boundaries; `service`/`datastore`/`queue`/`authz`). Import it so every diagram228 shares one look:229 ```d2230 ...@../assets/styles231 cloud: AWS Cloud {class: aws; web: Web {class: service}}232 ```233 See `examples/aws-arch.d2`, which imports it.234- **Custom palette** — d2 can't load a brand-new theme file, but you can recolor any235 built-in theme's palette slots in your own vars (these are in-file, not a preset):236 ```d2237 vars: {d2-config: {theme-overrides: {B1: "#0B5FFF"; B2: "#2E7D32"}}}238 ```239240## Best practices241242- Run `--validate` and `--fmt` before committing; commit the `.d2` source, not just243 the rendered image, so diagrams can be regenerated.244- Prefer `classes` over repeating inline styles; keep labels short and action-oriented.245- For very large systems, split into multiple focused diagrams.246247## Requirements248249- The **`d2` CLI** on `PATH`:250 ```bash251 curl -fsSL https://d2lang.com/install.sh | sh -s -- # or:252 brew install d2 # or:253 go install oss.terrastruct.com/d2@latest # needs Go 1.20+254 ```255- Scripts are **pure Python 3 stdlib** (no pip installs).256- PNG/PDF export additionally needs d2's headless Chromium (auto-downloaded on257 first raster render; SVG needs nothing).258259`assets/icons.csv` is bundled verbatim from the public260[tf2d2/terrastruct-icons](https://github.com/tf2d2/terrastruct-icons) project261(columns `Cloud,Title,URL`). Terrastruct does not change or expire existing icon262URLs, so the bundled values stay valid.