Architecture diagrams
What your agent reads
This is the skill text init installs. Your agent loads it when a task matches.
Overview
Generate the diagram from a Python script with scripts/archdiagram.py (a small design system on top of
draw.io XML), then lint → render → look → fix until the PNG is clean. Scripted layout makes every
rearrangement a cheap re-run, and the design system gives consistent type, colour and spacing.
Core principle: you are not done until you have viewed the rendered PNG and seen no overlaps. Estimates of text width are always wrong somewhere.
When to use / not
- Use for: infrastructure/solution diagrams, inventory-to-diagram work, "make it look like this reference", "fit on A4", "less white space", "align these in a grid".
- Not for: quick sketches in chat (use Mermaid), data charts (use a charting skill).
- For an Azure estate, gather facts first with
azure:azure-resource-lookup(orazCLI); see Workflow 1.
Workflow
- Gather verified facts. Resources, SKUs, IPs, subnets, private endpoints (+ target and sub-resource), DNS zone links, route tables / next hops, peering, public-access flags, identities, monitoring targets. Only draw relationships you verified; say "inferred from naming" otherwise. Never put secret values on a diagram.
- Plan the page before coordinates. Pick the smallest page that fits at font 10:
A4Ldefault,A3Lwhen detail would not fit,A5L(827×583) for small systems. Test for small: after compacting on A4L, if a whitespace finding remains that only filler or bigger icons would clear, rebuild on A5L and keep it when everything fits at font 10. Slack rule: on the chosen page, keep bands tight and park any leftover height in ONE band directly above the footer (lint allows it); never spread it as gaps between bands. Sketch the bands (below). Count items per row and checkpitch ≥ widest label + 8. - Write the build script in the working folder: copy
examples/event_driven_platform_a4.py(a copy findsscripts/in.claude/skillsor.agents/skillsof the project or a parent folder, or in~/.claude/skills; otherwise setARCHDIAGRAM_DIR=<this skill's folder>/scripts). Create scripts with the Write tool and change them with Edit or a patch-script file; never inline code in shell heredocs (quotes break them). - Loop:
d.save()→lint_file()→render()→ Read the PNG → fix → repeat. Two or three passes is normal. Lint reports overlaps and whitespace (boxes holding only a title, boxes under 15% filled, one-sided gaps, empty areas over 3% of the page, empty bands ≥ 32 px across half the page - connector lines count as content), plus icons over 40 px and cells hidden behind a filled box. Fix each finding by removing the space: move a real panel into the gap, shrink the box, tighten the band, or pick a smaller page. Filler is not a fix: no tables that repeat the drawing, no padded notes, no inflated icons to raise fill. - Deliver:
.drawio(editable),.png(3× ≈ 300 dpi on A4),.pdf(page size). Every image gets breathing room and an outer border: full pages already have the sheet frame fromframe_title(); a figure exported without it (e.g. a crop for a Word document) callsd.frame_content()just beforesave().render()adds a 12 px white margin around the PNG, so no border ever touches the image edge. Keep the build script next to them. Before overwriting a file the user may have edited in draw.io, check its mtime and back it up.
Layout bands (top → bottom)
| Band | Contents |
|---|---|
| Title | frame_title(): title, subtitle, environment chip, blue rule |
| Context | legend · key flows · external actors + hub/edge network · observations · title block |
| Network | main container (VNet/VPC) full width: workload subnets row, then the private-endpoint subnet |
| Services | PaaS/managed services outside the network container, in uniform rows under their endpoints |
| Operations | monitoring · DNS · images/identities panels; footer with provenance |
Fill the margins with panels (legend, flows, DNS, observations, title block) rather than leaving an empty column.
Design rules
| Rule | Why |
|---|---|
| Lay out in final page units at font size 10 (title 16). Never scale a drawing down to fit | Scaled A4 text drops to ~4 pt |
| Doesn't fit → shorten labels, move detail to the notes/markdown, or go A3. Don't shrink | Legibility beats completeness |
Label = bold name + one grey detail line; status in colour (d.color("public on","red")) |
Scannable and consistent |
Shorten repeated context: IPs as .9.4 when the subnet header has the /24 |
Saves width in grids |
Repeated items on a column grid (d.columns); pitch ≥ widest label + 8 |
Uniform rhythm, no collisions |
Two-row grids use lanes: row A exits left to cx-LANE, row B exits right to cx+LANE; services sit on the lanes |
Lines never cross labels |
Every edge: explicit exit/entry + waypoints; straight segments only |
draw.io auto-routing wanders |
| Legend lists only edge kinds actually drawn | No dead legend entries |
Draw only verified attributes: subnet() NSG / route-table badges stay off unless badges=True is backed by facts |
A default icon is a claim |
| Fixed icon sizes: resources 24-28 px, endpoints 20-22 px, badges 10-14 px | Oversized icons are filler |
Place row icons by their bottom edge (icon_b) so labels share one baseline |
Mixed icon heights make ragged rows |
Number the key flows (badge) and explain them in numbered_list |
Reader follows the story |
Problems go in a bullets panel (red ▲); good facts are shown as green status in the resource label, never as ▲ |
Red means "act on this" |
Prefer a numbered badge to an edge label; if a label is needed, pass label_pos so it sits on a clear segment |
Auto-placed labels land on icons |
Nodes with many connections (apps, gateways)
An icon's label side is off-limits to edges. Give each remaining side one job (e.g. inbound on the left,
private endpoint below, telemetry on the right) and split a shared side with exit/entry fractions (0.3 / 0.7).
Relationships that are properties rather than flows (VNet integration, delegation) go as text inside the subnet,
not as edges. Plan these nodes first; they constrain everything around them.
- Egress via an integration subnet: draw data flows from the integration subnet (the app's egress point) to the
target private endpoints; the source subnet must span every column it drops into (
rel()raises otherwise). Name the calling app in the key flow. - Managed private endpoints (Front Door Premium origins, Fabric / Power BI private link, managed VNets) live on the provider side: draw service → target directly, labelled 'Private Link (managed)', never through your PE subnet.
Quick reference (archdiagram.py)
Diagram(page=A5L|A4L|A3L…, font_size=10) · frame_title · container(kind=vnet|hub|subnet|group) · subnet(name, cidr) ·
icon / icon_c(cx) / icon_b(cx, bottom) · card(icon=path|"shape=...") · cloud · text · panel(accent) · legend(kinds) ·
numbered_list · bullets(h=None → auto) · table · title_block · badge(n,x,y) · edge(kind, src, tgt, exit, entry, points) ·
rel(id, x=|y=) (raises if outside the cell) · columns · footer · frame_content(pad) (outer border for figures
without a sheet frame) · add_edge_kind · save · lint_file(path) · render(path, border=12).
save() draws containers largest-first, so declaration order never hides content.
CLI: python archdiagram.py lint|render <file.drawio>; the example takes out.drawio --render. Icon paths: references/icons.md.
Common mistakes
| Mistake | Fix |
|---|---|
| Declaring done from the script, not the render | Always Read the PNG after each change |
| Trusting "lint: 0 issues" | Lint checks label/title overlaps and whitespace. Still check by eye: lines crossing labels or container titles, edge-label placement, free text under lines |
| Leaving empty bands, half-empty panels, wide boxes with one line of text | Act on every whitespace: finding; resize boxes to their content |
| Label overlaps a neighbour / line runs through a label | Widen pitch, move to lanes, or shorten the label |
| Icon renders as a grey box | Style must start with image; (lint flags it) |
| Heading overlaps a panel subtitle | Leave ≥ 18 px under panel titles |
| Container title crossed by a vertical line | Align the title away from the line (right/bottom) |
| Shell heredoc with an apostrophe breaks the command | Write scripts to files |
| PNG export is cropped, PDF is page-sized | Check the fit via the frame, the PDF via the printed page size |
| Figure for a document starts at the image edge, no border | Call frame_content() before save(); keep render()'s default 12 px margin |
| Guessed relationship drawn as fact | Draw only verified links; mark inferred ones |