Documentation / Diagon

The language

Last updated: 9 October 2026

A Diagon diagram is a text file. Every line is one statement, and there are only four kinds: a comment, a directive, a node, or an edge. That's the whole language — everything else is attributes in braces.

This page is the complete reference. If you're new, Getting started walks through a first diagram; The editor covers what happens once the diagram is on screen.

How a file is read

Diagon reads your file line by line, top to bottom. Order doesn't affect layout — the engine works out ranks and positions itself — but it does affect readability, so most people declare nodes first and edges after.

There's one rule that changes everything else on this page: a { … } block that spans more than one line switches the whole file to a second, slightly different parser. A single-line { … } is always plain attributes.

Whether that node also becomes a container depends on what's in the block. A multi-line block holding only attributes stays an ordinary node — x: X { / fill: #123 / } is one plain box, not an empty group. A block that holds nodes or edges becomes a container with children. Either way, the second parser is now reading your whole file. The Containers section below covers what changes, and Flat and nested compared lists every difference in one table.

Comments

A comment is a whole line whose first non-space character is # or //. Both work.

diagon

# this line is a comment
// so is this one
direction: right
a -> b

There are no end-of-line comments. A # partway through a line is ordinary text: on a node line it lands inside your label, and on a direction: or routing: line it makes the value unrecognisable and you get an error. This trips people up often enough to be worth repeating — put comments on their own line.

Directives

Three statements configure the whole diagram.

StatementValuesDefault
direction:down, up, left, rightdown
routing:curved, orthogonal, straightcurved
icon-size:small, medium, large, or a number of pixelsmedium

diagon

direction: down
routing: orthogonal

load: Load balancer
a: Server A
b: Server B
load -> a
load -> b

direction sets the flow of the layout. routing sets how edges are drawn: curved splines, right angles with rounded corners, or direct lines. icon-size (since 1.4.0) sets how big the pictures in nodes are — see Icons.

An unrecognised value is an error with a line number, for example line 1: unknown routing "wiggly". The value is the entire rest of the line, which is why a trailing comment breaks it.

In a diagram with containers, direction: and icon-size: can also appear inside a container block, to flow that group differently from its parent or to size the icons in it. routing: is top-level only — and putting it inside a container fails silently in an unhelpful way: the line is read as a node declaration, so you get a stray box inside the group with the id <container>.routing, labelled with the value you typed. No error, no warning.

Nodes

FormMeaning
idA node whose label is its id
id: LabelA node with a label
id: Label { … }A node with a label and attributes
id { … }A node with attributes, label is the id

diagon

db
api: API server
cache: Redis { shape: cylinder }
queue { fill: #1b2330 }

An id may contain letters, digits, _, . and -. Anything else is an error: a$b: X gives line 1: invalid id "a$b".

An id may not contain spaces, and this one fails quietly. Diagon takes the first word before the colon as the id and discards the rest, so my node: Label creates a node called my labelled Label — no error, no node. If a node you referenced elsewhere doesn't appear, check for a space in its id.

The label is everything after the first colon, verbatim. It can contain colons, spaces and punctuation.

Attributes

Attributes go inside { } after the node, separated by commas. Each one is key: value (or key = value). Keys are case-insensitive.

AttributeApplies toEffect
shape:nodesOne of the shapes listed below
fill:nodes, containersBackground colour
stroke:nodes, containers, edgesBorder or line colour
color:nodesLabel text colour
icon:nodesImage URL (SVG or PNG) drawn inside the node
icon-size:nodes, containerssmall, medium, large or a number of pixels — the size of this node's icon, or of every icon in a container
x: / y:nodes, flat diagrams onlyPin to a fixed position
w: / h:nodesExplicit width and height in pixels

diagon

web: Web app { fill: #1b2330, stroke: #5db3ff, color: #ffcb6b }
db: Postgres { shape: cylinder, fill: #1f6f43 }
web -> db: query { stroke: #c792ea }

Colours are hex (#1b2330) or CSS colour names (tomato). Functional forms have to be written without commas, because the comma is the attribute separator: rgb(255 0 0) — the space-separated CSS syntax — works, while rgb(255, 0, 0) is cut at the first comma and leaves you with the nonsense value rgb(255, silently. A value that isn't a colour at all — url(…), var(…) — is ignored, and the default colour is used.

Four things about attributes worth knowing before they surprise you:

  • Values can't contain commas. The comma is the separator, so icon: https://host/a,b.svg is silently cut at the comma.
  • Unknown keys are ignored. { wibble: 3 } parses without complaint and does nothing.
  • Shape names aren't validated. shape: blob doesn't error; you get a rectangle.
  • label: as an attribute does nothing. It parses, but the label comes from the text after the colon. Write id: My label, not id { label: My label }.

Attributes mean different things depending on what they're attached to. On an edge, only stroke: has any effect. On a container, only stroke: and fill: do, and the fill is drawn at 7% opacity so children stay readable.

Shapes

diagon

direction: right

r: Rect
p: Pill { shape: pill }
o: Round is the same pill { shape: round }
d: Diamond { shape: diamond }
c: Circle { shape: circle }
y: Cylinder { shape: cylinder }
h: Hexagon { shape: hexagon }
g: Parallelogram { shape: parallelogram }

rect is the default, and it already draws softly rounded corners. The other names are pill, round, diamond, circle, cylinder, hexagon and parallelogram — but that's seven distinct shapes across eight names, because round is an exact alias of pill. Both draw a stadium: a rectangle with fully semicircular ends. If you reach for round expecting a rectangle with rounder corners, you'll get a pill instead — and the default rect is already the rounded-corner one.

Icons

icon: takes a URL. The image is fetched over the network and drawn inside the node, both in the preview and again when you export.

diagon

direction: right
redis: Redis { icon: https://cdn.simpleicons.org/redis }
pg: Postgres { icon: https://cdn.simpleicons.org/postgresql }
redis -> pg

Icon size (since 1.4.0). Icons come in three sizes: small (32 px, close to the 34 px drawn before 1.4.0), medium (48 px, the default) and large (64 px); a bare number of pixels from 16 to 96 also works. Write icon-size: in any of three places, and the nearest one wins:

diagon

icon-size: large                 # every icon in the diagram

platform: Platform {
  icon-size: medium              # this box and everything nested in it
  api: API { icon: https://cdn.simpleicons.org/nodedotjs }
}
db: Postgres { icon: https://cdn.simpleicons.org/postgresql, icon-size: small }   # one node

A diagram that will end up on a slide reads better with icon-size: large on its first line. An unknown value is an error with a line number, like a bad direction. Keep an icon node in the default shape or a cylinder: on a circle, diamond, hexagon or pill (and round, which is the same pill) the icon crosses the outline.

Icons need a live connection — they're the one thing in a diagram that makes Diagon itself reach the internet, and the request goes to whichever host the URL names. Icons larger than 6 MB or slower than 10 seconds are dropped from exports. A host that serves no CORS headers can also block PNG export; The editor explains what to do about that.

Size and position

w: and h: set a node's size in pixels. x: and y: pin it to a fixed spot instead of letting the layout engine place it.

diagon

direction: right
api: API
db: Database { shape: cylinder, x: 640, y: 120 }
api -> db

Pins are what dragging writes back into your code — in flat diagrams. In diagrams with containers they're ignored, and a dragged position lives only in memory for that session. The editor covers the difference in detail.

Edges

There are four operators. They must have spaces around them — a->b is read as an id and rejected.

FormMeaning
a -> bArrow from a to b
a <- bArrow from b to a
a <-> bArrowheads at both ends
a -- bA line, no arrowhead
a -> b: labelA labelled edge
a -> b -> cA chain — one edge per pair
a -> aA self-loop

diagon

a -> b: sends
b <- c
c <-> d
d -- e
e -> f -> g
g -> g: retry

Chains can mix operators, and a label or attribute block on a chain applies to every edge in it.

Endpoints don't need to be declared first. a -> b on its own is a complete diagram — Diagon creates both nodes with their ids as labels. Declaring nodes up front is how you give them labels, shapes and colours.

An edge missing an endpoint is an error: a -> gives line 1: edge needs two endpoints.

Cycles are fine. The layout engine breaks them internally to work out the ranking, then draws every edge you wrote.

Containers

Write a { } block across several lines and the node becomes a box that other nodes live inside.

diagon

direction: right

cloud: Cloud {
  stroke: #607D8B
  api: API
  worker: Worker
  api -> worker
}
office: Office {
  stroke: #8D6E63
  laptop: Laptop
}
office.laptop -> cloud.api: https

Inside a block, a line is an attribute of the container if its key is one of shape, fill, stroke, color, icon, icon-size, x, y, w, h or label and it has no block of its own. Everything else is a child — a node, an edge, or another container.

Containers get a title bar and padding, and the box grows to fit whatever's inside it — it is never narrower than its title. Nest as deep as you like. Children that have no arrows between them are packed into a few rows (seven become 4 + 3; columns in a left-to-right diagram), and when a box holds a small flow plus some unconnected children, the unconnected ones sit in a compact block beside the flow.

Dotted paths

A dot addresses a node inside a container: cloud.api, cloud.aks.worker. Missing ancestors are created as you go.

diagon

direction: right

cloud: CML Cloud {
  stroke: #607D8B
  aks: aks-prod {
    api: API
    worker: Worker
    api -> worker
  }
  kafka: Kafka { shape: cylinder }
  aks.worker -> kafka
}
onprem: On-Premise {
  stroke: #8D6E63
  gw: LLM Gateway
}
onprem.gw -> cloud.aks.api: mcp
cloud.kafka -> onprem.gw: events

Inside a block you can use bare names: Diagon looks in the current container first, then at the top level, and creates the node in the current container if it finds nothing. Cross-container edges written at the top level need the full dotted path.

Per-container direction

A container can flow differently from the diagram around it:

diagon

outer: Outer {
  direction: right
  a: A
  b: B
  a -> b
}
c: C
outer.b -> c

Several statements on a line

Inside a container block, ; separates statements as well as a newline:

diagon

grp: Group {
  a: A; b: B
  a -> b
}

Quoted and multi-line labels

Inside a diagram with containers, labels get two conveniences: one surrounding pair of " or ' is stripped, and \n becomes a real line break.

diagon

pipe: Pipeline {
  ingest: "Ingest\nraw events"
  norm: Normalise
  ingest -> norm: "batch of 500"
}
sink: Sink { shape: cylinder }
pipe.norm -> sink

ingest renders on two lines and the quotes don't appear. In a flat diagram neither of these happens — the quotes stay and \n is two literal characters.

Flat and nested compared

The two parsers agree on most things and differ on a handful. If a diagram behaves unexpectedly after you add your first container, this table is usually why.

BehaviourFlat diagramWith containers
Quotes around a labelKeptStripped
\n in a labelTwo literal charactersA line break
Id characters checkedYesNo
Dots in idsOrdinary charactersHierarchy
Bad direction: / routing: valueReported as an errorIgnored
x: / y: pinsHonoured, and drag writes them backIgnored; drag pins are in memory only
Errors carry a line numberYesNo line numbers — and no node errors at all. The only thing the nested parser reports is a malformed edge

D2 files

Versions before 1.2.1 could read a subset of D2 behind a toolbar toggle. That mode is gone, and .d2 files no longer open.

Common errors

MessageWhat happened
line N: invalid id "a->b"An operator with no spaces around it
line N: invalid id "a$b"A character that isn't a letter, digit, _, . or -
line N: edge needs two endpointsAn operator with nothing on one side
line N: unknown direction "…"A bad value — often a trailing comment on the line
line N: unknown routing "…"The same, for routing

And the quiet ones, which produce no message at all: a space in an id (the rest is dropped), a # on a node line (it joins the label), a comma inside an attribute value (the value is cut), an unknown attribute key (ignored), a misspelled shape (a rectangle), and a routing: line inside a container block (it becomes a node).