# D2 Diagram

> 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.

- Skill: `colbytimm/d2-diagram` (Agent Skill, multi-file: 14 files)
- Install (CLI): `npx skillmds@latest add colbytimm/d2-diagram`
- Raw SKILL.md: https://api.skillmd.com/api/skills/colbytimm/d2-diagram/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: DevOps & Infra
- License: MIT
- Author: colbytimm (https://skillmd.com/u/colbytimm)
- Updated: 2026-09-22
- Page: https://skillmd.com/skills/colbytimm/d2-diagram

---


# d2 Diagram

Generate architecture diagrams as text with [d2](https://d2lang.com), 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

1. **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.
2. **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).
3. **Validate, then render** with `render.py` (SVG by default).
4. **Embed in markdown** with `render.py --md` if the diagram belongs in docs.

## Syntax cheat-sheet

```d2
# 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](https://www.atlassian.com/work-management/project-management/architecture-diagram).

## Cloud architecture recipe

1. **Resolve every service icon** before writing the diagram:

   ```bash
   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>.

2. **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`).

3. 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

```bash
# 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 --> ![arch](path.svg) <!-- /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:

    ```d2
    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:

- **Named render presets** — `assets/themes.json` maps friendly names to a theme id
  plus default render options (layout, pad, title-pills). Apply with
  `render.py --preset <name>` (any explicit flag still wins); `--list-presets` shows
  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.d2` sets the elk layout default and
  reusable `classes` (titled-box `group`/`subnet`; cloud `aws`/`azure`/`gcp` brand
  boundaries; `service`/`datastore`/`queue`/`authz`). Import it so every diagram
  shares one look:
  ```d2
  ...@../assets/styles
  cloud: AWS Cloud {class: aws; web: Web {class: service}}
  ```
  See `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):
  ```d2
  vars: {d2-config: {theme-overrides: {B1: "#0B5FFF"; B2: "#2E7D32"}}}
  ```

## 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

- The **`d2` CLI** on `PATH`:
  ```bash
  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](https://github.com/tf2d2/terrastruct-icons) project
(columns `Cloud,Title,URL`). Terrastruct does not change or expire existing icon
URLs, so the bundled values stay valid.

