d2-diagram
Skill0 starsChanged 3 months agoMITView on GitHub
Draw architecture, dependency, class, network and flow diagrams as D2 source rendered to PNG. Use when asked to draw, diagram, visualize or map anything structural — infrastructure (Terraform layers, EKS/VPC topology, Kubernetes workloads, CI pipelines, request paths) as well as code (module and import graphs, class relations, call flows, data models, state machines) — when a diagram must live in git next to the code, or when Mermaid output looks too crude for the destination. Also use to redraw an existing Mermaid or Graphviz dot source at higher quality.
- Installs packages
1 other project uses a version of this.
Adopted by
Most recently taken up by
- AlanSyue/my-skillstheir own version, first seen Feb 26, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- AlanSyue/my-skills★ 2same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
The file
We split this skill into the rules it gives an agent
7 rules. About 0 are common, 2 are distinctive, and 5 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
5
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 13
Never place coordinates by hand — describe containment and edges, let the layout engine solve it.
- Very specific: line 42
Read the real source before drawing: .tf files, terraform_remote_state blocks, k8s manifests, helm_release names.
- Very specific: line 44
Write the smallest readable .d2.
- Distinctive: line 64
Look at the PNG before reporting it.
- Very specific: line 174
Write the title as a plain quoted string with \n, on a shape: text node so it gets no border:
- Very specific: line 318
If someone genuinely needs a vector — a print, a slide deck — render one on the spot from the .d2 into /tmp and hand it over.
- Distinctive: line 443
Read the code first, then draw — but let a tool find the edges instead of guessing at imports:
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
D2 diagrams
D2 is a diagram scripting language: text in, picture out, layout computed by the engine.
Never place coordinates by hand — describe containment and edges, let the layout engine solve it.
Both engines ship inside the binary: dagre (the Dagre directed-graph library, the default) and
elk (Eclipse Layout Kernel, layered algorithm). d2 layout prints them.
Two files per diagram, and only two: <name>.d2 and <name>.png. SVG is never a deliverable
here and never gets committed — see What ships before rendering anything.
d2 is installed at /usr/local/bin/d2 (v0.7.1). It writes an SVG and stops; everything that turns
that into a picture a reader can look at is a separate package —
apt install librsvg2-bin fonts-adobe-sourcesans3 imagemagick. Check before drawing, not after —
a missing one of these does not fail loudly, it produces a picture with the wrong font metrics.
scripts/check_labels.py measures text with fontTools. Where it is not importable, run the
script under uv run --no-project --with fonttools: that borrows the library without installing
it, which matters because Debian refuses pip install into the system Python (PEP 668).
Choose the tool first
| Situation | Tool |
| Diagram lives in git, changes with the code, destination is GitHub README/PR | D2 |
| Must render natively inside a GitLab MR comment or issue body | Mermaid (```mermaid), GitLab bundles mermaid 11.13 + layout-elk |
| One-off editorial figure for a client deck or a marketing page | diagram-design skill, if installed |
| Graph must be derived from real state, not from reading code | terraform state list + terraform show -json, then draw that |
D2 when the diagram is a maintained artifact. Mermaid when the renderer is not yours.
Workflow
- Read the real source before drawing:
.tffiles,terraform_remote_stateblocks, k8s manifests,helm_releasenames. Never draw from memory of what the stack probably looks like. - State the thesis in one line — what the reader must see. Everything that does not serve it gets cut.
- Write the smallest readable
.d2. Containers over arrows, short labels over sentences. Format, validate, render to a scratch SVG, then rasterize (see Rendering):
d2 fmt cluster.d2 d2 validate cluster.d2 d2 cluster.d2 /tmp/cluster.svg # scratch only — /tmp, never the repo sed -E \ -e 's/font-family: "d2-[0-9]+-font-regular"/font-family: "Source Sans 3"/' \ -e 's/font-family: "d2-[0-9]+-font-bold"/font-family: "Source Sans 3"; font-weight: 700/' \ -e 's/font-family: "d2-[0-9]+-font-italic"/font-family: "Source Sans 3"; font-style: italic/' \ /tmp/cluster.svg > /tmp/cluster.fixed.svg rsvg-convert -z 2 /tmp/cluster.fixed.svg -o docs/diagrams/cluster.pngThe
sedis not optional and not cosmetic — Rasterizing is why. Skip it and every label falls back to librsvg's own sans: the weight is gone and the text outgrows the boxes D2 sized for it.- Trust the exit code, not the presence of the file — D2 leaves a partial render after an error.
- Look at the PNG before reporting it. An unread diagram is not a verified diagram — and look at it the way Reading the render says to, because the defects that survive this far are the ones a whole-page view hides.
Reading the render
A diagram that compiles, validates and rasterizes can still be wrong, and the wrongness is never in
the .d2 — it is in what the layout engine did with it. d2 validate cannot see any of this.
Look at the top edge of every container. This is where the one recurring defect lives: D2 centres
a container's title on its top border, and dagre routes an incoming edge into the top of that same
container. The two collide, and the arrow is drawn straight through the title. Two of the four
two diagrams in the gallery this skill comes from shipped like that — in one, the arrow replaced
the em-dash in Kubernetes — namespace gateway-system and read as punctuation.
Switching the layout engine does not help. Measured on 0.7.1 against a diagram built to collide:
dagre and elk both draw the edge through the title. elk is worth trying for other reasons —
see Syntax — but not for this.
The collision happens when the title still occupies the horizontal middle of the container, which is where the edge comes in. So:
- Short titles. A title narrower than half the container leaves the middle free.
cloud providernever collided;Kubernetes — namespace gateway-systemalways did. label.near: top-leftmoves the title into the corner — but only helps if the title is short enough not to reach the middle anyway. Both together is what fixed the example.- Two incoming edges straddle the centre instead of hitting it, so a centred title gets clipped from both sides. There the fix is the title, not its position.
Check it mechanically. Do not rely on your eye for this one. The check ships with the skill:
python3 scripts/check_labels.py /tmp/cluster.svg
# or, where fontTools is not importable and uv is:
uv run --no-project --with fonttools python3 scripts/check_labels.py /tmp/cluster.svgIt names the label, the edge crossing it, and the magick crop that shows it. Exit 1 means
something crosses. Run it on the scratch SVG, before the PNG is worth looking at.
Widths come from the real font when fontTools is importable and from an estimate when it is not;
the estimate is deliberately narrow, so a missing dependency loses findings rather than inventing
them. It says which mode it used on stderr.
The check exists because reading the picture is not reliable. Two collisions in the gallery of the repository this skill comes from were looked at directly and called clean; the checker found them in the same files minutes later. At full-page scale a 2 px arrow crossing a label is invisible.
For everything the checker does not cover, crop before you judge:
magick diagram.png -crop 1400x140+120+1930 +repage /tmp/check.png # w x h + x + yAlso worth a look, in order of how often they bite: labels touching or crossing the border of their
own box; edges that pass under an unrelated container instead of around it; and a label sitting on
top of another where two edges converge. And the aspect ratio: divide the viewBox width by its
height, and a number far from 1 is the cue to flip direction:, as
Aspect ratio explains.
Nothing here tells you the diagram is true. The check reads geometry; a picture can be flawless and still draw a dependency that does not exist, or miss the one that matters. That is what step 1 of the Workflow is for — read the source, never memory — and it is the one part no tool verifies.
Corrections are descriptive, because D2 is. There is no way to nudge a box: you change what the diagram says and let the engine re-solve. The whole set of levers:
| What went wrong | What to change |
| title under an incoming edge | shorter title, label.near: top-left, usually both |
| a column instead of a screenful | direction: right ↔ down |
| too many edges to follow | fewer nodes, or two diagrams — see Node budget |
| the wrong thing stands out | a different class — spine, focus, zone |
Node budget
Cut a node when it carries no information. The commonest offender in infra diagrams is the uniform edge: every Terraform root writes to the same S3 backend, so seven arrows to one "S3 backend" box say nothing that one line of subtitle does not. Same for "AWS" wrappers, "user" actors, and per-resource IAM roles.
Target: one screen, no crossing edges, under ~25 leaf nodes. More than that means two diagrams.
Brand tokens
Tailwind slate + blue-700 accent, orange-600 for the one thing under change.
Neutral, familiar, prints fine, reads on the white GitHub background.
Put the tokens in a theme.d2 beside the diagrams and import it — ...@theme on the first line.
One file then restyles every diagram in the repo. A ready copy lives in
references/theme.d2; it is the file used by the worked example.
Class roster: spine (solid blue, the 1-2 nodes read first), zone / zone-warm (large container:
white field, accent border), layer (ordinary grouping box), focus / hot (orange, what is
changing), comp (leaf), store (cylinder), ext (outside this stack), and for edges dep
(hard dependency), read (dashed blue, data read), cut (orange, the new path), gone (grey
dashed, being drained).
A solid fill belongs on small boxes only. Filling a container that wraps ten nodes floods the
diagram — use zone there: white inside, thick accent border, accent title.
Never use an |md ... | block — not on nodes, not on the title. Two separate reasons:
- On a node it renders as one flat wide strip and wrecks the row rhythm.
- Anywhere in the file it compiles to
<foreignObject>, and GitHub refuses to render an SVG that contains one — the file page shows "Unable to render code block" on both the Code and the Preview tab, and the image will not appear in a README either. Verified on a private repo, GitHub web UI, D2 v0.7.1.
Write the title as a plain quoted string with \n, on a shape: text node so it gets no border:
title: "cluster-a — example-sandbox (us-east-1)\nSubtitle line, no backticks — plain text only." {
shape: text
near: top-center
style: {font-size: 20; bold: true; font-color: "#0f172a"}
}Dropping the markdown also drops the italic and mono font subsets D2 embeds for it: the four diagrams in the worked example went from ~68 KB to ~39 KB each.
Two traps when converting a title to a plain string: ; is a statement separator, so an unquoted
label containing one splits into two nodes; and without shape: text the title gets a box drawn
around it.
The tokens, if writing the preamble by hand:
vars: {
d2-config: {
layout-engine: elk
theme-id: 0
pad: 40
}
}
classes: {
layer: { # ordinary grouping box
style: {fill: "#f8fafc"; stroke: "#cbd5e1"; stroke-width: 2; font-color: "#0f172a"; border-radius: 6}
}
base: { # accent — the 1-2 things read first
style: {fill: "#eff6ff"; stroke: "#2563eb"; stroke-width: 2; font-color: "#0f172a"; border-radius: 6}
}
comp: { # leaf component inside a box
style: {fill: "#ffffff"; stroke: "#94a3b8"; font-color: "#334155"; border-radius: 4}
}
ext: { # something outside this stack's control
style: {fill: "#f1f5f9"; stroke: "#64748b"; stroke-dash: 3; font-color: "#475569"}
}
dep: { # hard dependency / apply order
style: {stroke: "#94a3b8"; stroke-width: 2; font-color: "#64748b"}
}
read: { # data read, soft coupling
style: {stroke: "#2563eb"; stroke-width: 2; stroke-dash: 4; font-color: "#2563eb"}
}
}Reference values: paper #f8fafc, ink #0f172a, muted #64748b, rule #cbd5e1, accent #2563eb, accent-fill #eff6ff.
Fonts stay D2's bundled defaults — Source Sans Pro for text, Source Code Pro for monospace — and that is what every example here was rendered with.
There are eight override flags (--font-regular, -italic, -bold, -semibold and the same four
with -mono), each with its own D2_FONT_* variable. They do work — but not evenly. Measured on
0.7.1, one flag at a time, on a two-node diagram:
| flag | effect on the SVG |
--font-regular, D2_FONT_REGULAR | none, ever — byte-identical output |
--font-bold | applied, and this is the one that matters |
--font-italic | applied |
--font-semibold | none here — nothing in the sample was semibold |
| a path that does not exist | rejected outright: failed to read font at … |
--font-regular is not a partial set falling back silently, it has nothing to act on. D2 emits
only two @font-face blocks, bold and italic; there is no regular face at all, and plain text
carries no font-family. Passing bold, italic and semibold without regular produces a file
byte-identical to passing all four.
Bold is the flag that counts, because D2 sets shape labels in it — every label carries
class="text-bold". Substituting it swaps the embedded WOFF subset for one cut from your file
(4488 → 12592 bytes in the measured case).
None of it reaches a PNG by itself. librsvg does not read those embedded faces, so a custom family lands in the SVG and vanishes from the picture unless the class rules are repointed first — see Rasterizing.
Syntax that covers most infra diagrams
title: "cluster-a — example-sandbox (us-east-1)\nSubtitle on its own line. Plain text — no markdown, no backticks." {
shape: text
near: top-center
style: {font-size: 20; bold: true}
}
vpc: "1.VPC" {
class: base
sub: k8s subnets {class: comp}
}
svc: "3.EKS-SERVICES" {
class: layer
alb: aws-load-balancer-controller {class: comp}
}
vpc -> svc: applied first {class: dep}
ext -> svc: read while flag = false {class: read}Shapes accepted by 0.7.1, all checked against the binary: rectangle, square, page,
parallelogram, document, cylinder, queue, package, step, callout, stored_data,
person, diamond, oval, circle, hexagon, cloud, text, code, class, sql_table,
sequence_diagram. triangle is not one of them — it is only an arrowhead.
layers/scenarios/steps put several boards in one file. Note what that does to the output: it
writes a directory, out/index.svg plus out/scenarios/<name>.svg, not the single file the rest
of this skill assumes.
d2 themes lists built-in theme IDs, d2 layout lists engines. In 0.7.1 only dagre (default) and
elk are bundled; elk is usually better for layered infra, dagre for trees and flows.
Rendering and destination
What ships
| File | Role | Committed |
<name>.d2 | the source, the only thing anyone edits | yes |
<name>.png | what every reader actually looks at | yes |
theme.d2 | shared colors and line styles, one per diagram directory | yes |
*.svg | scratch step on the way to the PNG, lives in /tmp | no |
SVG is not a deliverable — it is an intermediate. The only reason it exists at all is that D2's
own PNG export is broken on this box (see Rasterizing),
so the pipeline is .d2 → scratch .svg → rsvg-convert → .png. Write that SVG under /tmp and let
it die there. Where d2 x.d2 x.png works directly, there is no SVG at all.
Two reasons it must not be committed, both hit in practice:
- GitHub will not open an SVG file page. Embedded in a README the picture appears, but a click lands on "Unable to render code block", so a reader can see a thumbnail and never enlarge it.
- It goes stale silently. The next person re-renders the PNG and forgets the SVG; now two files claim to be the same diagram and one of them lies.
Embed the PNG wrapped in a link to itself — the click then opens it full size in GitHub's viewer:
[](docs/diagrams/cluster-a.png)If someone genuinely needs a vector — a print, a slide deck — render one on the spot from the .d2
into /tmp and hand it over. Do not add it to the repository "just in case".
A PR diff of the .d2 is readable; that is the whole point over a hand-written SVG.
Aspect ratio decides whether the thumbnail is useful. A README column is roughly 900 px, so a
2:1 canvas already renders text at a third of its size and a 4:1 one is unreadable. Check the
viewBox after rendering and flip direction: (right ↔ down) until the ratio is near 1:1 —
on the worked example that turned 4.57:1 into 0.55:1 with a one-word change.
Render the PNG at 2× so the full-size click is sharp: rsvg-convert -z 2 on the scratch SVG.
Rasterizing — PNG export is broken on this box
d2 x.d2 x.png fails: it tries to fetch a Playwright driver from playwright.azureedge.net, a host
Microsoft has retired. The replacement (cdn.playwright.dev/dbazure/download/playwright) answers
307 to playwright.download.prss.microsoft.com, and the playwright-go downloader inside D2 does
not follow that redirect — pointing PLAYWRIGHT_DOWNLOAD_HOST at it turns the 404 into a 400,
not into a working driver. Measured 2026-08-20 on linux-amd64; the older note blaming arm64 was
too narrow, the driver URL is dead for every platform. Do not retry it, and do not report it as a D2
bug.
The whole procedure is four steps: d2 to a scratch SVG in /tmp, repoint the fonts,
rsvg-convert -z 2, PNG next to the source. No browser, no Playwright cache, no headless flags.
-z 2 is the 2× scale that keeps the full-size click sharp — it replaces the browser's
--force-device-scale-factor=2, and unlike the browser path it needs no window size, because the
SVG's own viewBox sets the canvas.
librsvg has to be pointed at the font, or the labels lose their weight and outgrow their boxes.
D2 embeds the font as a data URL in @font-face; librsvg ignores those and measures the text in
whatever sans it has, so the labels stop fitting the boxes D2 sized for them — under DejaVu,
unit_price_cents ran into bigint. What D2 embeds is a cut-down Source Sans Pro carrying only the
glyphs used, renamed per diagram (d2-<hash>-font-bold), so no fontconfig alias can catch it.
Two things go wrong at once, and the second is the one that gets missed: shape labels are set in bold, so substituting only the regular class leaves every label in librsvg's fallback. Repoint all four classes, and restate the weight and the slant — in D2's own faces they live inside the font file, not in the CSS, so they are lost the moment the family changes:
apt install fonts-adobe-sourcesans3 librsvg2-bin
d2 cluster.d2 /tmp/cluster.svg
sed -E \
-e 's/font-family: "d2-[0-9]+-font-regular"/font-family: "Source Sans 3"/' \
-e 's/font-family: "d2-[0-9]+-font-bold"/font-family: "Source Sans 3"; font-weight: 700/' \
-e 's/font-family: "d2-[0-9]+-font-italic"/font-family: "Source Sans 3"; font-style: italic/' \
-e 's/font-family: "d2-[0-9]+-font-mono"/font-family: "monospace"/' \
/tmp/cluster.svg > /tmp/cluster.fixed.svg
rsvg-convert -z 2 /tmp/cluster.fixed.svg -o docs/diagrams/cluster.pngThe family name is quoted only in the class rules and never in the @font-face blocks, which is
what makes that sed safe. All four expressions, every time — the one that is easy to drop is
-font-bold, and it is the one that matters, because D2 sets shape labels in bold.
Checked against the old browser render of the same source: identical pixel dimensions, RMSE 4–6% from antialiasing alone, no layout shift.
This is the one legitimate use of SVG in this skill. It never reaches the repository.
Then Read the PNG to actually look at it. Two rasterizers that are not substitutes:
convert/ImageMagick drops all CSS fills and produces a grayscale image; a headless Chromium
screenshot works but pulls a browser onto the box for one conversion — use it only where a
Playwright cache already exists.
--window-size sets the canvas; too small crops, too large leaves whitespace. Iterate once.
Code, not only infrastructure
Nothing here is infra-specific — the language draws whatever the nodes are named after. For code use
shape: class (fields and methods with +/- visibility) and shape: sql_table for a data model:
Record: {
shape: class
+zone_id: str
+to_change_batch(): dict
-validate(): bool
}
Client -> Record: returnsNever name a container classes — it is the reserved keyword for the style-class block, and every
child inside is then parsed as a class definition. The error is misleading: ... is an invalid class
field, must be reserved keyword. Same care with vars, layers, scenarios, steps, direction.
A method signature is parsed, so brackets in a return type break it: list[Record] fails, quote it
("list of Record") or simplify.
For a data model use shape: sql_table. Columns are name: type, and the constraint tag renders as
a right-hand marker (PK, FK, UNQ). Edges may attach to a specific column, which is what makes
the foreign key readable:
dns_record: {
shape: sql_table
id: bigint {constraint: primary_key}
zone_id: varchar(32) {constraint: foreign_key}
set_identifier: varchar(64) {constraint: unique}
}
route53_zone.id <-> dns_record.zone_id: "1 : N"Worked example: references/example-data-model.d2.
What D2 cannot draw
The dividing line is simple: D2 places nodes with a layout engine, so anything where the meaning lives in a coordinate or an area is out of reach. No amount of styling gets around it.
| Works natively | Needs a grid hack | Not possible |
| architecture, flow, sequence, state, class, ER | quadrant / 2×2 matrix (grid-rows, grid-columns) | Venn — circles cannot be made to overlap |
| tree, org chart, nested containers, layer stack | comparison matrix | radar / spider |
| swimlane, process, data flow, request path | calendar-ish grids | pyramid, funnel — there is no triangle shape |
| cycles and feedback loops | bar, line, scatter, gantt — no chart primitives, no axes | |
| treemap — areas cannot be made proportional |
shape: triangle fails outright: invalid shape, can only set "triangle" for arrowheads.
For those, reach for something else: the diagram-design skill if it is installed (it writes SVG by
hand, which is why it can do pyramids and Venns), or dataviz for anything that is genuinely a chart
over data. Do not spend an hour bending D2 into a funnel.
Read the code first, then draw — but let a tool find the edges instead of guessing at imports:
| Stack | Extractor |
| JS / TS | madge --json src/, or depcruise --output-type json |
| Python modules | pydeps --show-deps --no-output pkg/ |
| Python classes | pyreverse -o dot pkg/ (ships with pylint) |
| Go | go mod graph, go list -deps ./... |
| Java | jdeps -verbose:class app.jar |
| PHP | deptrac analyse --formatter=json |
Each emits JSON or dot; convert the edges to D2 lines and lay them out. That keeps the graph honest — a hand-drawn import graph is wrong the day someone adds an import.
Terraform-specific recipe
The dependency graph is written down already — extract it instead of guessing:
rg -n 'data "terraform_remote_state" "' --glob 'ext.data.tf' . # edges between roots
rg -o 'resource "helm_release" "[a-z0-9_-]+"' <layer>/*.tf # what each layer installs
rg -n 'module "' <layer>/*.tf # modules per rootOne container per Terraform root, leaf nodes for the 3-5 things it owns, edges for
terraform_remote_state reads. Apply order and coupling then read off the picture.
A worked example against a real layered EKS stack is in
references/example-terraform-layers.d2.
Terminal output as a picture
A README screenshot of a CLI — a table, colours, a highlighted match — drawn as a dark terminal
window. Not a screen capture: a capture shows the paths, hosts and data of the machine it was taken
on. Run the tool over invented data (a temporary HOME or config directory) and draw its real
output. The picture then changes only when the output does, and the .d2 diff shows how.
scripts/term2d2.py does the conversion. One grid row per line; a row is cut into cells at every
run of two or more spaces and at every colour change, and each cell is exactly as wide as its
characters, so a monospace table stays lined up. ANSI colour (SGR 30-37, 90-97, bold) survives;
--dim PREFIX greys out legend or footer lines.
# run it under `script` so the tool sees a terminal and prints its colours
CONFIG_DIR=$FIXTURE script -qc "stty cols 100; mytool list" /dev/null | tr -d '\r' \
| python3 scripts/term2d2.py 'mytool list' --dim 'Legend:' > docs/list.d2
d2 fmt docs/list.d2 && d2 validate docs/list.d2
d2 docs/list.d2 /tmp/list.svg
sed -E \
-e 's/font-family: "d2-[0-9]+-font-mono-bold"/font-family: "DejaVu Sans Mono"; font-weight: 700/' \
-e 's/font-family: "d2-[0-9]+-font-mono"/font-family: "DejaVu Sans Mono"/' \
-e 's/font-family: "d2-[0-9]+-font-bold"/font-family: "Source Sans 3"; font-weight: 700/' \
/tmp/list.svg > /tmp/list.fixed.svg
rsvg-convert -z 1.5 /tmp/list.fixed.svg -o docs/list.pngTraps, each met on a real README:
- The
sedneeds the two mono rules on top of the usual ones. Without them librsvg sets the cells in its default sans, and no column lines up. - Narrow the window, not the font. GitHub shrinks a picture to the README column, about
880 px, so the text a reader sees depends on how many characters a line holds, not on the font
size inside the PNG. Measured on one README: a 146-column table came out 2666 px wide at 15 px
and
-z 2, was shown at a third of that, and could not be read; the same output in a 100-column window is 1898 px and its text 1.4 times larger. Keepstty colsat 100 or less. If the tool wraps long cells at that width, that is what a narrow terminal shows — draw it so. - Font size and zoom decide only the click.
--font-size 15with-z 2keeps the full-size view sharp; the default 13 px at-z 1.5is enough for a listing of 60-80 columns. - 1-2 px between two colour cells. D2 centres a label by its own metric for the monospace font
(about 7.7 px a character at 13 px) and librsvg draws DejaVu (7.83). Invisible between words,
just visible after a hyphen:
ingress- nginx. Accept it; a larger--char-widthonly moves the gap. $in a label starts a substitution — the script writes single-quoted labels for that reason, with'inside the text turned into’.
The .d2 and the .png go to the repository, as for any other diagram. The fixture and the
command that produced the output belong in the notes that say how to redraw the picture.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

