- What It Is and What Problem It Solves
- Install
- Quick Start
- Examples
- TOML Format
- Minimal Example
- Properties Section
- Edge Kinds and the Legend
- Unit Types
- Optional Name (Humanization)
- Optional Type (Inference)
- Nesting (C2/C3 Diagrams)
- Nesting Context in Rendered Diagrams
- Links (Relationships)
- Expanded Units (Drill-Down)
- Styling
- Templates
- Multi-File Composition (Include)
- Relative Peer Resolution
- C4D Format
- Full Example
- CLI Reference
- Editor and GUI Clients
- Validation Rules
- Architecture
- License
Describe your architecture once in a TOML file; C4Drill generates a hierarchy of cross-linked C1/C2/C3 SVG diagrams with automatic drill-down navigation. The model is the diagram: edit the TOML, re-run the tool, and every level plus all navigation links is regenerated consistently. No runtime, no GUI, no manual drawing. Author in TOML or in the compact C4D brace format — the same model, the same pipeline (see C4D Format).
One TOML model produces the C1 context diagram below, the C2 container diagrams, the C3 component diagrams, and every drill-down link between them. Open any generated SVG to click through.
Real systems do not fit one sheet: dozens to hundreds of elements on a single diagram are unreadable, and hand-maintaining a linked C1 → C2 → C3 hierarchy drifts out of sync with the code. C4Drill generates the hierarchy from the model, so every level and all navigation links stay consistent after each re-run.
| Tool | Good at | But |
|---|---|---|
PlantUML (+ C4 module) |
Rich syntax, Markdown integration |
No enforced C4 levels; every level is a separate hand-written diagram — the drill-down hierarchy is not generated |
Mermaid |
Diagrams straight from Markdown (GitHub, Obsidian) |
Flat pictures: no C4 model, no levels, no zoom-in navigation |
Structurizr DSL |
Purpose-built C4 modeling language |
More general than the task — a modeling platform with workspaces; C4Drill is deliberately minimal and strict |
LikeC4 |
The closest: model + nested views with drill-down |
Requires a runtime/workspace; C4Drill emits static files you can commit and open anywhere |
C4Drill targets the static-artifact case: diagrams that live in the repository, are reviewed in pull requests, and render everywhere.
No new DSL to learn. TOML is a configuration format most engineers
already read, and the schema is the C4 model: type = "container" and
name = "API Service" read like prose. A bespoke DSL (or HCL) would add
syntax to learn without adding modeling power. C4Drill additionally
ships C4D, a second authoring format with the same modeling power and
no TOML boilerplate — a compact brace-block notation for multi-level
diagrams (see C4D Format); pick per file, convert freely.
go install github.com/Djarvur/c4drill/cmd/c4drill@latestOr build from source:
git clone https://github.com/Djarvur/c4drill
cd c4drill
go build -o c4drill ./cmd/c4drill# Generate SVG diagrams (default)
c4drill architecture.toml
# Generate to specific directory
c4drill architecture.toml -o ./docs/diagrams
# Generate DOT format for customization
c4drill architecture.toml -f dot -o ./output
# Generate PNG raster images (each with a linking HTML doc)
c4drill architecture.toml -f png -o ./output
# Generate C4-PlantUML sources (render with: plantuml -tsvg *.puml)
c4drill architecture.toml -f plantuml -o ./outputBoth authoring formats are interchangeable throughout: pass a
architecture.c4d file (see C4D Format) to any command shown here.
The models below are adapted from the
likec4 examples.
Generated diagrams are committed next to their TOML sources, so what you
see here is exactly what c4drill produces. Features likec4 has that
C4Drill does not (dynamic and deployment views, tags, metadata,
view-local styles) are either mapped to the closest C4Drill equivalent
or dropped with a note in the source file.
A SaaS platform with two external parties and multi-level drill-down.
The C1 diagram already shows one nesting level; clicking the nodes drills down further:
Regenerate:
c4drill examples/cloud-system/cloud-system.toml
c4drill examples/cloud-system/cloud-system.toml --expandedA stress test: very long display names and channel-technology labels.
C4Drill sizes labels from their content — tune the shape with
--label-ratio (default 1.6) or the C4DRILL_LABEL_RATIO environment
variable.
The longest labels live in the sensor processing pipeline (C3):
Parallel edges between the same pair of units collapse to a single
thicker edge in resolved views (first-wins dedup, D-01); the
--expanded diagram shows every parallel link with its own label.
Regenerate:
c4drill examples/overflow-test/overflow-test.tomlThe per-link rank attribute controls layout direction per edge:
-
rank = "forward"(default) — the target ranks after the source. -
rank = "equal"— both endpoints share a rank. -
rank = "reverse"(v1.13) — flips the vertical ordering with one option while the arrow still points at the target. This replaces the old two-part idiom of authoring the link as"←"plusarrow = "reverse"(the old idiom still works, butrank = "reverse"is the clear way to express it).
A minimal graph showing rank hints: D carries a link to C to satisfy
the orphan rule (see Validation Rules).
Regenerate:
c4drill examples/rank-for-better-layout/rank-for-better-layout.toml[properties]
name = "My System"
[user]
type = "person"
name = "User"
[webapp]
type = "system"
name = "Web Application"
# Every unit needs at least one link (orphan rule)
[[webapp.link]]
peer = "user"
description = "Uses"The [properties] section defines global settings for your
architecture:
[properties]
name = "E-Commerce Platform" # Required: Architecture name
description = "Online store system" # Optional: Description
color = "#E3F2FD" # Optional: Default background color
style = "filled" # Optional: Default visual style
border = "#1565C0" # Optional: Default border color
edges = "spline" # Optional: Edge routing (straight|spline|square|ortho)
lineLength = 40 # Optional: Max line length before wrap (0=auto)
expanded = ["payments"] # Optional: Units to expand by default
legend = true # Optional: Legend in the upper-right (default: on)The edges routing value applies to every generated diagram (C1, C2,
C3, expanded); square is an alias for ortho routing. A unit-level edges
field overrides the global value for that unit’s own diagram. Either value
can be overridden per invocation with the --edges CLI flag (see
CLI Reference) — no model edit needed.
Links accept a kind attribute that colours the edge by data flow — no
explicit color needed:
| Kind | Edge colour |
|---|---|
|
green |
|
red |
|
purple |
An explicit color overrides the kind colour (the kind still documents the
data flow). Unknown kinds render with the default edge colour.
Collapsed edges keep their kind identity (v1.13): when several links
collapse to a visible ancestor, the collapsed edge’s colour derives from the
constituents — all-read green, all-write red, mixed purple. Its line style
follows precedence: all constituents agree → that style; otherwise any solid
→ solid, else any dashed → dashed, else dotted. If any constituent carries a
custom color, kind colouring is suppressed and the default edge colour is
used.
Legend (default: on) — every diagram renders a legend in the upper-right listing the three kind colours and the line-style samples, followed by author-defined rows:
[properties]
legend = false # Disable the legend model-wide
[[properties.legendLine]]
label = "Nightly batch"
color = "#E65100"
style = "dashed"In the C4D format, legend: false disables the legend and custom lines are
pipe-split list items ("label|color|style"; the label cannot contain a
pipe):
properties {
legendLine: ["Nightly batch|#E65100|dashed"]
}Each unit is defined as a TOML section. The section name becomes the unit’s identifier.
Type is optional — defaults based on nesting level:
-
Root-level units →
system -
Units inside system/box →
container -
Units inside container →
component
[user]
type = "person" # Required
name = "Customer" # Optional: defaults to humanized identifier
description = "Online shopper" # OptionalAn actor outside the organization (partner, regulator, third-party
user). Same fields as person:
[auditor]
type = "personExternal"
name = "Compliance Auditor"
description = "External regulatory auditor"[webapp]
type = "system"
name = "Web Application"
description = "Frontend web app"
technology = "React, TypeScript" # Optional: Shown in diagram[postgres]
type = "db"
name = "PostgreSQL"
description = "Primary database"
technology = "PostgreSQL 15"[analytics]
type = "dbExternal"
name = "Analytics DB"
description = "Third-party analytics"[rabbitmq]
type = "queue"
name = "Message Queue"
description = "Async job processing"
technology = "RabbitMQ"[cloud]
type = "box"
name = "AWS Cloud"
description = "Cloud infrastructure"box is a universal shorthand: write it at any nesting depth and it
promotes to the level-appropriate variant (containerBox at C2,
componentBox at C3); at C1 (root or inside another box) it stays
box. See [_optional_type_inference] below.
Any unit accepts an optional reference field — an external
documentation URL. When the reference is linked, a 📖 marker appears next
to the unit name and the unit becomes clickable (GraphViz’s native URL
attribute in SVG). In -f html output, external http(s) references
open in a new tab, distinct from internal drill-down navigation.
A unit has a single URL slot, and navigation comes first:
-
Collapsed container (subunits hidden, 🔍 shown) — the click drills down into the unit’s child diagram. Its reference is not linked on this diagram: the unit’s own child diagram carries it — the boundary frame there shows 📖 and links to the docs.
-
Expanded container (subunits already depicted around it) — the title shows 📖 and links to the docs.
-
Unit without subunits — the whole node shows 📖 and links to the docs.
[api]
type = "system"
name = "API Service"
reference = "https://wiki.example.com/api-runbook" # Optional: 📖 marker, clickableAn empty string and an omitted field are equivalent (no 📖, not clickable).
The name field is optional. When omitted, the display name is
derived from the last segment of the unit’s identifier via a dumb
camelCase split:
# Explicit name (always wins — use this for acronyms or custom labels)
[linuxSystem.localIDP]
name = "My Custom Name"
# Name omitted — humanized from the last path segment "sessionManager"
[linuxSystem.sessionManager]
# displays as "Session Manager"Humanization rules:
-
Splits camelCase boundaries and Title-cases each word.
-
Operates on the last path segment only —
[linuxSystem.localIDP]becomes "Local IDP", not "Linux System Local IDP". -
Examples:
sessionManager→ "Session Manager",localIDP→ "Local IDP",linuxSystem→ "Linux System".
Acronyms: acronym preservation is not supported — gRPC humanizes
to "Grpc". Set name = explicitly to override; an explicit name =
always wins.
Backward compatibility: models that set name = on every unit are
unaffected — humanization only fires when name is omitted.
The type field is optional. When omitted, the type is inferred from
the parent unit’s type. The generic types — db/queue and the
grouping box — are also promoted to the level-specific variant based
on nesting. Three rules apply:
1. Default type by parent — the type assigned when type is omitted
entirely:
| Parent type | Inferred child type | Level |
|---|---|---|
(none — root) |
|
C1 |
|
|
C2 |
|
|
C1 (same-level grouping) |
|
|
C3 |
|
|
C2 (same-level grouping) |
|
|
C3 (same-level grouping) |
(other: db, queue, etc.) |
|
C1 fallback |
2. Generic db/queue promotion — when type = "db" or
type = "queue" is set explicitly, the type is promoted to the
level-specific variant based on the parent:
| Parent type | db becomes |
queue becomes |
Level |
|---|---|---|---|
(none) or |
|
|
C1 (unchanged) |
|
|
|
C2 |
|
|
|
C3 |
3. box promotion — type = "box" is valid at any depth and promotes
to the level-appropriate grouping variant based on the parent:
| Parent type | box becomes |
Level |
|---|---|---|
(none) or |
|
C1 (unchanged — same-level grouping) |
|
|
C2 |
|
|
C3 |
Once promoted, the box’s children follow the level-specific default
(container under containerBox, component under componentBox).
Before/after example:
# BEFORE — explicit types (verbose)
[platform]
type = "system"
[platform.webapp]
type = "container"
[platform.webapp.cache]
type = "componentDb" # generic db promoted because parent is container
# AFTER — type omitted, inferred (identical result)
[platform]
# type omitted → inferred "system" (no parent)
[platform.webapp]
# type omitted → inferred "container" (parent is system)
[platform.webapp.cache]
type = "db"
# explicit generic db → promoted to "componentDb" (parent is container)
# box shorthand anywhere — promoted to "containerBox"; children default to container
[platform.group]
type = "box"
[platform.group.svc]
# type omitted → inferred "container" (parent promoted to containerBox)An explicit non-generic type = always wins (no inference runs). The
explicit variants containerBox and componentBox are themselves
non-generic, so they pass through unchanged — use them only to pin the
level regardless of position.
Source: defaultTypeForParent and inferGenericType in
internal/parser/parser.go.
Systems and boxes can contain subunits using dotted notation:
[mainapp] # C1 level
type = "system"
name = "Main Application"
[mainapp.api] # C2 level (container)
type = "container"
name = "API Service"
[mainapp.webapp] # C2 level (container)
type = "container"
name = "Web App"
[mainapp.api.handlers] # C3 level (component)
type = "component"
name = "HTTP Handlers"
[mainapp.api.services] # C3 level (component)
type = "component"
name = "Business Services"Diagrams keep container context — elements never float free of their hierarchy (v1.21):
-
Full ancestor chains — every element depicted on a non-expanded view renders inside its complete ancestor-container chain. If a view shows a component three levels deep, the viewer sees it nested in its parent container inside its parent system, not as a flat top-level node.
-
Deep links keep their target — a link aimed at a deeply nested unit terminates at the true target inside its ancestor chain, not at a collapsed stand-in at the top level.
-
Expanded units show nested clusters — an expanded unit renders its nested containers as clusters (with the 🔍 drill-down marker and explore link), not as a flat list of names.
v1.22 reverses one v1.21 boundary: every node a view depicts — regular, boundary, or expanded — now renders inside its full ancestor-container chain, not at the top level. When the depicted element’s ancestors are not otherwise part of the view, the generator synthesises wrapper clusters for them, labelled with the container’s pretty name. Wrappers are pure structure: they carry no 🔍 drill-down glyph and no explore link, and they exist only where an element in the view needs them.
The one remaining top-level rule: units fully external to the depicted subtree — with no ancestor relationship to anything shown — still render at the top level. Collapsed subtrees are likewise not restructured; chains unfold only for the elements a view actually depicts.
See skill/examples/11-nesting-context.toml for a runnable three-level
example with a deep-link target and an expanded unit containing a
container, and skill/examples/13-wrapping.toml for boundary and
sibling entries rendered inside their ancestor chains.
Define relationships between units using (outgoing) or
(incoming). Define each relationship exactly once — the
two forms are just different places to write the same edge:
[user]
type = "person"
name = "User"
[webapp]
type = "system"
name = "Web Application"
# Outgoing link: User → Webapp (defined on the source)
[[webapp.link]]
peer = "user"
technology = "HTTPS"
description = "Browses"
[api]
type = "system"
name = "API Service"
# Incoming link: Webapp → API (defined on the target)
[[api.linkFrom]]
peer = "webapp"
technology = "REST/JSON"
description = "Calls"| Attribute | Description | Example |
|---|---|---|
|
Target unit identifier (required) |
|
|
Protocol/technology label |
|
|
Relationship description |
|
|
Arrow direction |
|
|
Layout ranking hint: |
|
|
Data-flow kind colouring (v1.13): read=green |
|
|
Edge color (overrides kind) |
|
|
Line style |
|
|
Where the label appears |
|
[api]
type = "system"
name = "API"
[[api.link]]
peer = "user"
technology = "HTTPS"
description = "Authenticates"
[[api.link]]
peer = "webapp"
technology = "REST"
description = "Serves data"Parallel edges: when several links connect the same pair of units,
resolved views collapse them into a single thicker edge (the first one
wins); the --expanded diagram shows every parallel link separately.
Mark units as "expanded" to generate C2/C3 diagrams for them:
[properties]
name = "My System"
expanded = ["mainapp"] # Generate C2 for mainapp
[mainapp]
type = "system"
name = "Main Application"
expanded = ["mainapp.api"] # Generate C3 for mainapp.api
[mainapp.api]
type = "container"
name = "API Service"
[mainapp.api.handlers]
type = "component"
name = "Handlers"
[mainapp.api.services]
type = "component"
name = "Services"
[[mainapp.api.handlers.link]]
peer = "mainapp.api.services"
description = "Calls"Output structure:
output/
├── architecture.svg # C1 diagram
├── architecture/ # C2 diagrams directory
│ └── mainapp.svg # C2 for mainapp
│ └── mainapp/ # C3 diagrams directory
│ └── api.svg # C3 for mainapp.apiOverride default colors and styles per unit:
[webapp]
type = "system"
name = "Web Application"
color = "#4A90D9" # Background color
border = "#2E5A8B" # Border color
style = "solid" # Border style (solid|dashed|dotted)
edges = "spline" # Edge routing for this unit's links
width = 300 # Explicit label width (0=auto)
height = 200 # Explicit label height (0=auto)width and height are rarely needed — labels are normally sized
from their content (see --label-ratio in the CLI Reference).
Since v1.13 the unit styling triple actually renders: color fills the node
(or the expanded-unit cluster) — a dark fill forces a white label for
legibility — border sets the border colour, and style sets the border
style (solid, dashed, or dotted). Author styling beats the automatic
box-content heuristic. Units without these fields keep the C4 type palette.
A template is a [template.<name>]` table that declares
parameters and a unit shape (including subunits and links); each
`[[use]]++ directive instantiates it with concrete values.
[template.microservice]
params = ["name", "tech", "upstreamBus"]
name = "${name} Service"
type = "container"
technology = "${tech}"
description = "${name} handles its domain"
reference = "https://wiki.example.com/${name}"
[[template.microservice.link]]
peer = "${upstreamBus}"
description = "Publishes ${name} domain events"
[[use]]
template = "microservice"
parent = "platform"
name = "auth"
tech = "Go, gRPC"
upstreamBus = "messageBus"Rules:
-
All declared params are required on every
[[use]](no defaults); a missing param is a hard error. -
${param}substitutes into every string field — name, description, technology, reference, color, and link fields (peer, description, technology). -
The link set is fixed: a template with one
[[template.X.link]]produces exactly one link per instantiation (no fan-out /for_each). -
Subunit subtrees are supported (declare
[template.X.child]); the subunit key is verbatim, only field values are substituted. -
Duplicate unit paths across instantiations are a hard error.
See skill/examples/06-templates.toml for a runnable example.
Assemble a diagram from multiple TOML files. Each [[include]]
directive pulls in another file relative to the including file’s
directory and merges its units into the model.
# entry.toml
[platform]
type = "system"
name = "Platform"
[[include]]
path = "auth.toml"
[[include]]
path = "templates.toml"
once = trueRules:
-
Paths are relative to the including file’s directory (not the CLI cwd).
-
Includes are transitive (an included file may itself include others).
-
once = truededuplicates by canonical path — a file included again (even via a different path) is skipped. -
The merge is flat (no namespacing): included units append in include order. Cross-file subunits are supported — an included file may re-declare a parent declared in the entry and contribute subunits under it.
-
Include cycles are a fatal error; missing files are a hard error.
-
Properties follow root-file-wins (the entry’s
[properties]takes precedence).
See skill/examples/08-include/ for a runnable multi-file example.
A bare peer value (no dot) resolves against the enclosing parent’s
ancestor scopes — walking up nearest-first until a sibling match is
found. A peer with a dot is absolute and used as-is.
[platform.api]
# Bare peer "cache" resolves via walk-up:
[[platform.api.link]]
peer = "cache" # sibling: platform.cache (nearest ancestor scope match)
[[platform.api.link]]
peer = "platform.cache" # absolute: has a dot, used as-isThe four resolution cases:
-
Sibling match — the nearest ancestor scope (the immediate parent) has a child with that name.
-
Aunt/grandparent match — walk up past the parent; a grandparent’s child matches.
-
Root match — walk all the way to the top-level scope.
-
Absolute fallback — a peer containing a dot is never walked-up; it is used verbatim.
Multiple matches at the same depth are impossible (sibling keys are unique per parent). A miss at root is a hard error naming the peer and the host unit.
See skill/examples/07-relative-peer.toml for a runnable example
demonstrating all four cases.
C4D (.c4d) is a second authoring format for the same model: a
brace-block format that is less verbose than TOML for multi-level
diagrams. Everything the TOML format expresses — units, links,
templates, includes, styling — has a C4D equivalent. c4drill
renders .c4d files directly through the full pipeline, c4drill
convert translates between the two formats (canonical-equivalent
round-trip), and c4drill fmt formats both.
|
Tip
|
Prefer C4D when a diagram has deep nesting or many links: edges live inside the unit they belong to, and one-line leaf blocks keep files compact. Stay with TOML when the file is machine-generated or edited by tooling that already speaks TOML. |
properties {
name: My System
}
user: person "User" { }
webapp: system "Web Application" {
-> user: Uses
}A unit is a header plus a brace block:
id: type "Display Name" { body }
-
id— the unit identifier (letters, digits,_,-). -
type— optional; an omitted type infers exactly as in TOML (see [_optional_type_inference]). Type keywords are the exact TOML type names (system,person,db,queue,box,container,component,containerDb,componentQueue, …). External units take theexternalmodifier after the type:system external,person external,db external. -
"Display Name"— optional; an omitted name humanizes from the id exactly as in TOML.
Nested units are brace blocks inside the parent:
shop: system "E-Commerce Platform" {
webapp: container "Web Application" { technology: "React, TypeScript" }
database: containerDb "Database" { technology: PostgreSQL }
}The brace block is required even when empty (x: system { }). The
generic db/queue/box types promote by nesting level exactly as in
TOML — database above is a containerDb because its parent is a
system.
Field statements inside a block mirror the TOML unit fields — one
field: value per line:
api: system "API Service" {
description: "Backend API"
technology: "Go"
reference: https://wiki.example.com/api-runbook
color: "#E3F2FD"
border: "#1565C0"
}The properties { } block carries the same keys as
[properties] in TOML (name, description, color,
style, border, edges, lineLength, expanded).
-
Barewords are fine when unambiguous:
technology: PostgreSQL. Quote with double quotes when the value contains{ },:,|, a comma or edge whitespace:technology: "React, TypeScript". -
Multi-line strings use triple quotes:
description: """Line one
line two"""-
URLs with a scheme prefix are valid barewords (
reference: https://wiki.example.com/apiabove). -
#starts a line comment; comments are preserved byc4drill fmt. -
List values accept the inline form (
expanded: [platform, webapp]) or one item per line:
expanded: [
platform
webapp
]Edges live inside unit blocks only — never at the top level. Four ASCII arrows:
| Arrow | Meaning |
|---|---|
|
Outgoing link ( |
|
Incoming link — the edge is declared on the target ( |
|
Bidirectional |
|
No arrowhead |
The label shorthand is → peer: "tech | description". A single
un-piped value is the description:
-> db: queries orders # description only
-> db: sql | # technology only
-> db: sql | queries orders # bothLink attributes ride a trailing brace block on the edge statement:
-> payment: "HTTPS | Processes payments" { rank: equal kind: write color: orange style: solid }kind: read | write | read-write colours the edge (green / red / purple);
rank: reverse flips the layout ranking while the arrow still points at the
target — the single-knob replacement for the ← + arrow: reverse pair.
The upper-right legend is on by default; legend: false in the properties
block disables it and legendLine: ["label|color|style"] appends custom rows
(see Edge Kinds and the Legend).
Note: kind is an enum-like field and is not substituted by template
${param} parameters (the same rule as arrow/rank); other link fields
substitute normally.
Peer targets use the same relative-peer semantics as TOML: a bare name resolves against the enclosing parent’s ancestry walking up nearest-first; a dotted path is absolute (see [_relative_peer_resolution]). Two edges to the same peer in one block are a hard error.
Templates are function-like declarations; use instantiates them.
${param} substitution, the all-params-required rule and the fixed
link set are identical to TOML templates (see Templates):
template microservice(name, tech, upstreamBus) {
type: container
name: "${name} Service"
technology: "${tech}"
-> ${upstreamBus}: "Publishes ${name} domain events"
}
platform: system "Platform" {
use microservice(name: auth, tech: "Go, gRPC", upstreamBus: messageBus)
}A use inside a unit block attaches the produced unit under that
parent (nested use); a use inside a template body instantiates
another template (template nesting). Arguments are named
(name: auth) or positional (auth); a positional value containing
: must be quoted.
Includes are include <path> statements at the top level, optionally
with the once modifier:
include templates.c4d once
include domains/auth.c4dPaths are relative to the including file’s directory; includes are
transitive, cycle-detected and flat-merged exactly as in TOML (see
Multi-File Composition (Include)). An include graph may freely mix
.toml and .c4d files — each file parses by its own extension.
Statements are newline- or ;-separated; ; enables one-line blocks —
the same compact style c4drill fmt emits for leaf units:
platform: system {
api: container { technology: Go; db: db { technology: PostgreSQL; -> messageBus: writes } }
}Field keywords (name, description, technology, reference,
color, style, border, edges, expanded, once, …) are
reserved in the unit-body namespace: a unit id colliding with one is a
hard parse error.
The same model fragment from skill/examples/03-links.toml — an
outgoing [[link]] plus an incoming
[[linkFrom]]:
[webapp]
type = "system"
name = "Web Application"
technology = "React"
[[webapp.link]]
peer = "api"
arrow = "reverse"
technology = "REST/JSON"
description = "Queries API"
color = "blue"
[api]
type = "system"
name = "API Gateway"
technology = "Node.js"
[[api.linkFrom]]
peer = "payment"
labelPosition = "head"
technology = "Webhook"
description = "Payment callback"
color = "purple"is, in C4D (the shipped twin skill/examples/03-links.c4d):
webapp: system "Web Application" {
technology: React
-> api: "REST/JSON | Queries API" { arrow: reverse color: blue }
}
api: system "API Gateway" {
technology: Node.js
<- payment: "Webhook | Payment callback" { color: purple labelPosition: head }
}c4drill fmt formats .c4d (and .toml) in place with comments
preserved; c4drill convert to-c4d / to-toml translate between the
formats after validating the source — see CLI Reference. The key
fixtures under skill/examples/ ship .c4d twins that render
identically to their .toml sources; the twins are generated with
c4drill convert to-c4d and enforced by a parity test suite.
[properties]
name = "E-Commerce Platform"
description = "Online shopping system"
edges = "spline"
expanded = ["webapp", "webapp.api"]
# External actors
[customer]
type = "person"
name = "Customer"
description = "Online shopper"
[admin]
type = "person"
name = "Admin"
description = "System administrator"
# Main system with containers
[webapp]
type = "system"
name = "Web Application"
description = "Main e-commerce platform"
technology = "Go, React"
expanded = ["webapp.api"]
[webapp.frontend]
type = "container"
name = "Frontend"
description = "React web application"
technology = "React, TypeScript"
[webapp.api]
type = "container"
name = "API Service"
description = "Backend REST API"
technology = "Go"
expanded = ["webapp.api"]
[webapp.api.handlers]
type = "component"
name = "HTTP Handlers"
description = "Request routing"
[webapp.api.services]
type = "component"
name = "Business Logic"
description = "Core services"
[webapp.db]
type = "db"
name = "PostgreSQL"
description = "Primary database"
technology = "PostgreSQL 15"
# generic `db` is promoted to `containerDb` by the nesting level
# External dependencies
[stripe]
type = "systemExternal"
name = "Stripe"
description = "Payment processing"
[redis]
type = "queue"
name = "Redis"
description = "Session cache"
technology = "Redis"
# Relationships (every unit needs at least one — orphan rule)
[[customer.link]]
peer = "webapp.frontend"
technology = "HTTPS"
description = "Shops"
[[admin.link]]
peer = "webapp.frontend"
technology = "HTTPS"
description = "Administers"
[[webapp.frontend.link]]
peer = "webapp.api.handlers"
technology = "REST/JSON"
description = "Calls"
[[webapp.api.handlers.link]]
peer = "webapp.api.services"
description = "Dispatches"
[[webapp.api.services.link]]
peer = "webapp.db"
technology = "SQL"
description = "Persists"
[[stripe.linkFrom]]
peer = "webapp.api.services"
technology = "Stripe API"
description = "Processes payments"
[[redis.linkFrom]]
peer = "webapp.api.services"
technology = "TCP"
description = "Caches sessions"c4drill <input.toml|input.c4d> [flags]
Flags:
--edges string Override edge routing style for every generated diagram (straight|spline|square|ortho)
--expanded Generate all-expanded diagram showing all units
-f, --format string Output format (dot|svg|html|png|plantuml) (default "svg")
-h, --help help for c4drill
--label-ratio float Width:height ratio for unit labels (default: 1.6, credit card proportions)
--no-colors Suppress colouring only: author unit/link colors and kind-derived edge colours
--no-labels Suppress edge label text only: nodes, clusters and the legend keep their labels
--no-length Suppress link spacing only: link length no longer sets minlen
--no-rank Suppress ranking hints only: link rank reverse/equal ignored
--no-styles Suppress line styles only: author unit/link style overrides
-o, --output string Output directory (default: same as input file)
--plain Ignore author-custom formatting: default unit/edge styling, spacing and ranking, plain-text labels
-v, --version version for c4drill-
--expandedrenders a single diagram containing all units (every level), instead of the per-level drill-down set. The file is named{basename}.expanded.{format}and contains no drill-down links. -
--format pngrenders each diagram as a PNG raster at the same per-level paths the SVG layout uses ({basename}.png,{basename}/{system}.png, …). A bare PNG cannot carry hyperlinks, so every raster also gains a sibling.htmldoc — the interactive layer over the image. Each doc embeds its PNG and navigates like the SVG output does: a clickable breadcrumb trail up the ancestor pages, one drill-down link per drill-capable unit to its child page, and externalreferenceURLs opening in a new tab. The in-diagram nav bar and legend are baked into the raster (visual parity with SVG); the clickable breadcrumb is re-emitted above the image in the HTML doc. Existingdot/svg/htmloutput is unchanged.[source,bash] ---- c4drill architecture.toml -f png -o ./output ---- * `--format plantuml` serializes each diagram as a https://github.com/plantuml-stdlib/C4-PlantUML[C4-PlantUML] source document (`.puml` extension, same per-level paths as the SVG layout: `++{basename}++.puml`, `++{basename}++/++{system}++.puml`, …). Every c4drill unit type maps onto the standard C4_Context/C4_Container/C4_Component macros (persons, systems, containers, components, db/queue variants, and grouping boxes as boundaries — upstream C4-PlantUML has no `Component_Boundary`, so `componentBox` frames with the generic `Boundary` macro), and relationships map onto `Rel`/`Rel_Back`/`BiRel` by arrow direction. The drill-down links ride the element macros' `$link` parameter, so converting with `plantuml -tsvg` produces SVGs whose nodes are real anchors pointing at the sibling `.svg` diagrams — the breadcrumb nav bar is not expressible in PlantUML and is omitted. Model colours ride `AddElementTag`/`AddRelTag` declarations; the legend flag emits `LAYOUT_WITH_LEGEND()`.[source,bash] ---- c4drill architecture.toml -f plantuml -o ./output plantuml -tsvg output/*.puml ---- * `--edges` (v1.23) overrides the edge routing style for the *whole invocation*: every generated diagram — the C1 root, every drill-down view, and the `--expanded` copy — renders with the requested style. The flag wins over BOTH the global `properties.edges` value AND any unit-level `edges` override, with no model edit — the same model can be rendered as per-invocation variants (expanded-with-straight vs non-expanded-with-spline). Values: `straight|spline|square|ortho` (`square` is the documented ortho alias). An invalid value is a hard error naming the offending value and the allowed enum — nothing is rendered. An explicit `--edges` also wins over `--plain` (user intent beats author-format suppression; see the composition notes below). Without the flag, routing is resolved exactly as before.
[source,bash] ---- c4drill architecture.toml --edges straight --expanded c4drill architecture.toml --edges spline ---- * `--plain` (v1.21) renders with author-custom formatting *ignored* — a neutral, type-palette look for reviews and diffs. It is a CLI-only flag: the model file gains no new keys, and it composes with `--expanded`. What is ignored:
-
Unit
color/style/border(including on expanded-unit clusters) — units fall back to the C4 type palette. -
Link
color/style— edges keep only kind-derived colours or the default. -
Link
lengthandrank— default spacing and forward ranking (no endpoint swap, no constraint suppression). -
properties.edges— default spline routing. (One exception, v1.23: an explicit--edgesflag still applies under--plain— user intent beats author-format suppression.) -
Custom label formatting — labels render as plain text; the name/technology/description content is preserved.
What deliberately stays: kind-derived edge colours (semantic, from `link kind`) and the legend (including custom legend lines), queue SVG pipe shapes, the 🔍/📖 glyphs and their drill-down/docs links, and collapsed subtrees stay collapsed.
[source,bash] ---- c4drill skill/examples/12-plain.toml --plain # neutral look c4drill skill/examples/12-plain.toml --plain --expanded ----
`convert` and `fmt` are unaffected by the flag. * *Granular suppression switches* (v1.22) turn off one formatting concern each, without the all-or-nothing `--plain` scope. They compose freely with each other, with `--plain`, and with `--expanded`:
-
--no-colors— suppresses author unit/link colours and kind-derived edge colours (link kind). Pinned boundary: the D-01 default source-border edge colour is structural and stays. When the kind-derived colours are used on their own (no author colours),--no-colorssuppresses them too. The legend keeps its rows but loses its colour swatches. -
--no-styles— suppresses author line-style overrides; edges fall back to the default style. -
--no-length— linklengthno longer sets graphvizminlen; default spacing applies. -
--no-rank— linkrank(reverse/equal) is ignored; edges rank forward with no endpoint swap or constraint suppression. -
--no-labels— edge label text only is suppressed: the[technology] descriptiontext on arrows is dropped while node, cluster (wrapper and boundary included), and legend labels all keep their text. Colour/style semantics, cluster structure, and explore/reference URL attributes survive. Pinned boundaries: the legend stays (it is metadata governed byproperties.legend, not an element label), and it applies to all generations including--expanded.Composition notes:
-
--plainremains the exact union of everything the granular switches (plus plain-text labels) turn off; a--plainrender is identical to applying all concerns at once. In particular the pinned interaction: under--plainalone, kind-derived colours survive (semantic, v1.21 behaviour), so--plain --no-colorsis what removes them. -
properties.edges(spline routing) is tied to--plainonly — no granular switch touches it. One deliberate delta (v1.23): an explicit--edgesflag is user intent, not author formatting, and survives--plain—--plain --edges splinerenders spline routing on every diagram, while--plainwith no--edgesstill suppresses the author value (pinned by test).[source,bash] ---- c4drill skill/examples/13-wrapping.toml --no-colors --no-rank c4drill skill/examples/13-wrapping.toml --no-labels --expanded ---- * `--label-ratio` overrides the width:height ratio used to size unit labels (default 1.6 — credit-card proportions). Higher values produce wider, shorter labels; lower values produce narrower, taller ones. The same value can be set via the `C4DRILL_LABEL_RATIO` environment variable; the flag takes precedence. * Input dispatch is by extension: `.toml` parses as TOML, `.c4d` as C4D (see <<C4D Format>>); any other extension is a hard error.
-
c4drill convert to-c4d <file.toml> [flags] # TOML -> C4D, writes <file>.c4d
c4drill convert to-toml <file.c4d> [flags] # C4D -> TOML, writes <file>.toml
Flags:
-o, --output string Output directory (default: next to the input)
--follow-includes Convert the whole include graph, rewriting
include paths to the target extension-
The source is validated first (the same stage composition as the render pipeline: parse → include.Resolve → template.Expand → peer.Resolve → validate). An invalid model is a hard error and no output file is written.
-
By default a single file converts alone: include directives, template declarations and
useinstantiations are preserved verbatim — the twin re-parses to the same model. -
--follow-includesconverts every file of the include graph, rewriting each include path to the twin extension so the converted graph stays self-contained (onceflags preserved; files already in the target format are skipped). -
-owrites the twin(s) into a different directory (created when missing; graph mode preserves the graph’s relative directory structure).
Example — migrate a whole multi-file diagram to C4D:
c4drill convert to-c4d --follow-includes architecture.tomlc4drill fmt [--check] <file|dir>...-
Formats
.c4dand.tomlfiles in place, gofmt-style:.c4dre-emits through the canonical C4D printer (comments preserved, compact one-line leaf blocks);.tomlnormalizes whitespace, indentation and blank-line grouping with comments preserved. Key order is the author’s in both formats — fmt never reorders. -
Arguments may be files or directories; directories walk recursively, formatting every
.c4dand.tomlfound. -
--checkreports misformatted files one per line and exits 1 without writing anything — the CI gate:
c4drill fmt --check . # exits 1 listing offending filesc4drill check <file.toml|file.c4d>-
Validates a model without rendering (issue #41): runs the same pipeline front-half as render —
graphs resolved, templates expanded, relative peers resolved, then full semantic validation — and reports exactly the errors render would report, then stops. -
Accepts both authoring formats:
.tomland.c4d. -
Exit codes:
0when the model is valid (silent);1when it is invalid, printing the same validation errors render prints (e.g. the orphan-unit ruleunit "x" has no incoming or outgoing links). -
Writes nothing and needs no output directory — safe for CI gates and edit loops:
c4drill check architecture.c4d # exit 0 valid / exit 1 + errors invalid
c4drill fmt --check . && c4drill check architecture.c4d # format + validate gatec4drill serve --lsp-
Runs the c4drill language server (issue #32) — the shared foundation the VS Code, JetBrains and Zed plugins and the GUI app are thin clients over. Editors launch it as
c4drill serve --lsp; stdin/stdout carry the protocol (Content-Length framed JSON-RPC), so nothing else may write to stdout. -
Covers both authoring formats.
textDocument/publishDiagnosticsruns the exact CLI pipeline, so message text and line numbers matchc4drill <file>— includinggraphs, where editing an included file (open buffer or watched on-disk change) republishes diagnostics for every including document. -
Both formats get the full feature surface:
textDocument/completion(all 17 unit types with nesting inference and generic-type promotion, unit/link fields, enum values, peers, template params, include paths),hover(resolved peer paths, level and promoted type; template param info),definition(peer to unit section, template reference, include target),documentSymbol(unit outline) andtextDocument/formatting(fmt parity). -
textDocument/semanticTokens/full(TOML dialect) marks unit-type keys/values, link-table segments and enum values for editors that layer semantic tokens over their grammars. Oninitializedthe server dynamically registersworkspace/didChangeWatchedFileswatchers for/.tomland/.c4d, so on-disk include edits republish diagnostics even in clients that never send the notification manually. -
Custom method
c4drill/renderDiagramrenders the CLI SVG pipeline in-process — params: document URI, optional target unit path (C1/C2/C3), all-expanded toggle, expanded-set and legend overrides — returning the SVG text plus render-time diagnostics. This is the live-preview primitive the editor plugins and the GUI app build on.
-
svg (default): Rendered SVG diagrams with clickable navigation links
-
html: Self-contained HTML files (SVG inlined) with working navigation in Safari/WebKit, which silently ignores SVG
<a>hyperlinks. Use-f htmlwhen diagrams will be opened in Safari or viewed viafile://. -
dot: Raw GraphViz DOT format for customization
-
png: PNG raster diagrams at the same per-level paths the SVG layout uses, each with a sibling
.htmldoc carrying the clickable navigation (see--format pngabove) -
plantuml: C4-PlantUML source documents (
.puml) whose$linkdrill-downs point at the sibling SVGs afterplantuml -tsvg(see--format plantumlabove)
Four clients ship in this repository, all thin layers over the same
core: language features come from the c4drill serve --lsp server
(see serve) and live previews from its custom
c4drill/renderDiagram request, so diagnostics, completion and
rendered output match c4drill <file> in every client; each
subsection points at the per-directory documentation.
Registers a c4drill-c4d language backed by a TextMate grammar and
scopes TOML opt-in: plain .toml files are never touched unless they
match the c4drill.toml.patterns globs (default
*/.architecture.toml) or are activated per file. An LSP client
launches c4drill serve --lsp (c4drill.server.path picks the
binary), and a preview webview re-renders the diagram as you type with
click-through drill-down navigation and breadcrumbs. See
extension/README.md for the full command and settings surface.
Builds on the IntelliJ platform’s built-in LSP client (requires IDE
2025.3+ of the commercial IDEs) and reuses the same C4D TextMate
grammar for highlighting, with the same TOML opt-in rule as the VS
Code extension. A "C4Drill Preview" tool window renders through
c4drill/renderDiagram with drill-down navigation, view controls and
SVG export. Details, install and build instructions in
idea-plugin/README.md.
Ships a tree-sitter-c4d grammar (Zed has no TextMate support), wires
the LSP server for diagnostics, completion, hover, definition and
formatting, and contributes render/export tasks that run the CLI
(c4drill: render diagram, export dot/PlantUML). TOML scoping rides
Zed’s file_types settings glob; a live preview panel is pending
Zed’s visual extension API. Details in zed-extension/README.md.
A Wails desktop app wrapping the full authoring loop: a code editor
for both formats with the shared language features, a live
auto-refreshing diagram preview with drill-down and export, and an AI
chat panel whose edit proposals land as confirmed diffs. Build it
with go build ./cmd/c4drill-gui; the same UI is also available in a
browser via go run ./cmd/c4drill-gui --serve. Build, run and
configuration details in internal/gui/README.md.
-
Referenced units must exist - Links can only reference defined units
-
No links on containers - Units with subunits cannot have their own links (
linkorlinkFrom) -
No linking to containers - Cannot link to units that have subunits
-
Subunits only for grouping types - Only
system,box,container,containerBox, andcomponentBoxcan contain subunits (leaf types such asdb,queue, and the level-specific variants cannot) -
Nesting hierarchy - Top-level units must be C1 types; inside a
system— C2 types (containers); inside acontainer— C3 types (components); boxes group same-level units only -
No orphans - Every unit must have at least one incoming or outgoing link, or contain subunits
C4Drill implements a compiler-style pipeline:
TOML → Parse → Validate → Generate Views → Build Graphs → Render → Write Files-
Parser: Reads TOML into structured model
-
Validator: Enforces C4 rules and reference integrity
-
View Generator: Creates C1/C2/C3 views from model
-
Graph Builder: Constructs graphviz-compatible structures
-
Renderer: Outputs DOT, SVG, HTML, and PNG via go-graphviz, plus C4-PlantUML source documents
-
Writer: Creates output directory hierarchy