name: d2-diagram description: 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. license: MIT
d2 Diagram
Generate architecture diagrams as text with d2, then render and embed them. Three scripts support the workflow:
scripts/icons.py— resolve exacticon:URLs for AWS/GCP/Azure services.scripts/render.py— render/validate/format.d2files and embed them in markdown.scripts/title_pills.py— post-process an SVG to draw masking pills behind group titles (also wired intorender.py --title-pills).
Plus shared assets: assets/styles.d2 (importable layout + classes) and
assets/themes.json (named render presets) — see Theming.
Workflow
- Write a
.d2file. Keep the source next to the doc it illustrates so it stays re-renderable. Use containers for tiers/boundaries andclassesfor consistent styling. - For cloud diagrams, resolve icons first with
icons.pyand paste the URLs verbatim — never hand-writeicons.terrastruct.comURLs (they're URL-encoded and unguessable). - Validate, then render with
render.py(SVG by default). - Embed in markdown with
render.py --mdif 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 azureEach result prints a paste-ready
icon: <url>line.--jsonfor machine output;--limit Nto widen;categories/providersto 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.d2for a complete, validated AWS example whose icon URLs all came fromicons.py.
Other diagram types
- Sequence diagrams (auth flows, request/response): set
shape: sequence_diagramat the root; child objects become lifelines and connections become ordered messages. A self-edge is a self-call;a -> a: label. Seeexamples/auth-flow.d2. - User-journey / flowcharts:
ovalstart/end, plain steps,diamonddecisions, and edges labeledyes/nofor branches and retry loops. Seeexamples/user-flow.d2(also showssketchstyle).
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.pyfalls 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.pyretries with--no-bundle, keeping icons as remote refs that load when the image is viewed online. Pass--no-bundleexplicitly 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(orlayout-engine: elkinvars.d2-config). ELK routes edges orthogonally and places labels with far less overlap than the defaultdagre. 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), not3. 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, astroke, a bold font, andlabel.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(oroutside-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.pycan 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 (somersvg/cairobuilds), lines can appear to run through labels; preferrender.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:
- Named render presets —
assets/themes.jsonmaps friendly names to a theme id plus default render options (layout, pad, title-pills). Apply withrender.py --preset <name>(any explicit flag still wins);--list-presetsshows them. Presets:default,neutral-grey,terrastruct,aubergine,origami,c4,dark,auto-dark,sketch. Add your own by editing the JSON. - Shared style partial —
assets/styles.d2sets the elk layout default and reusableclasses(titled-boxgroup/subnet; cloudaws/azure/gcpbrand boundaries;service/datastore/queue/authz). Import it so every diagram shares one look:
See...@../assets/styles cloud: AWS Cloud {class: aws; web: Web {class: service}}examples/aws-arch.d2, which imports it. - Custom palette — d2 can't load a brand-new theme file, but you can recolor any
built-in theme's palette slots in your own vars (these are in-file, not a preset):
vars: {d2-config: {theme-overrides: {B1: "#0B5FFF"; B2: "#2E7D32"}}}
Best practices
- Run
--validateand--fmtbefore committing; commit the.d2source, not just the rendered image, so diagrams can be regenerated. - Prefer
classesover repeating inline styles; keep labels short and action-oriented. - For very large systems, split into multiple focused diagrams.
Requirements
- The
d2CLI onPATH:curl -fsSL https://d2lang.com/install.sh | sh -s -- # or: brew install d2 # or: go install oss.terrastruct.com/d2@latest # needs Go 1.20+ - Scripts are pure Python 3 stdlib (no pip installs).
- PNG/PDF export additionally needs d2's headless Chromium (auto-downloaded on first raster render; SVG needs nothing).
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.