Use when writing, generating, reviewing, or improving D2 diagram code. Trigger when the user asks to create architecture diagrams, flowcharts, sequence diagrams, ER diagrams, class diagrams, or any diagram using D2 syntax. Also trigger when working with .d2 files or when the d2_render MCP tool is available.
Install
npx skillscat add itsjool/d2-mcp/d2 Install via the SkillsCat registry.
D2 Diagram Language Reference
D2 is a declarative diagram scripting language that compiles to SVG. Text → diagrams.
Docs: https://d2lang.com | Playground: https://play.d2lang.com
Core Syntax
Shapes
# Bare identifier = rectangle by default
server
# With a display label (key vs label)
server: "API Server"
# Set shape type
db: {
shape: cylinder
}
# Multiple on one line
web; app; dbAvailable shape types:rectangle (default), square, circle, oval, diamond, hexagon, cloud,cylinder, queue, package, parallelogram, document, page, step,callout, stored_data, person, c4-person,sql_table, class, sequence_diagram, image
Connections
A -> B # directed arrow
A <- B # reverse
A -- B # undirected line
A <-> B # bidirectional
A -> B: label # with label
# Chaining
A -> B -> C -> D
# Multiple connections (creates separate arrows, not override)
A -> B
A -> B # second distinct arrow
# Referencing a specific connection (0-indexed)
(A -> B)[0].style.stroke: redContainers (nesting)
cloud: {
label: "AWS"
vpc: {
web: "Web Tier"
app: "App Tier"
web -> app
}
}
# Cross-container connections using _ (parent reference)
cloud: {
aws: {
db
db -> _.gcloud.backup # _ = parent scope
}
gcloud: {
backup
}
}Text and Markdown
# Standalone markdown block
explanation: |md
## Architecture Overview
This diagram shows the **three-tier** architecture.
- Web tier
- App tier
- Data tier
|
# LaTeX / math
formula: |latex
\frac{\partial f}{\partial x} = 2x
|Style Reference
All styles live under the style key:
my_shape: {
style: {
fill: "#4a90d9" # background color
stroke: "#2c5f8a" # border color
stroke-width: 2
stroke-dash: 5 # dashed border
border-radius: 8 # rounded corners
font-size: 14
font-color: white
opacity: 0.9
shadow: true
bold: true
italic: false
3d: true # rectangles/squares only
multiple: true # stacked visual effect
double-border: true # rectangles and ovals only
text-transform: uppercase
}
}Connection styles:
A -> B: {
style: {
stroke: red
stroke-width: 3
stroke-dash: 5
animated: true # flowing animation in SVG
bold: true
font-color: "#666"
}
}Arrowheads:
A -> B: {
source-arrowhead: {
shape: diamond
style.filled: true
}
target-arrowhead: {
shape: circle
}
}Arrowhead shapes: triangle (default), arrow, diamond, circle, box,cf-one, cf-many, cf-one-required, cf-many-required, cross
Global Styling with Globs
Apply styles to all shapes or connections at once:
# Style all shapes
*.style.fill: "#f0f4ff"
*.style.stroke: "#3b5bdb"
*.style.border-radius: 6
# Style all connections
(* -> *)[*].style.stroke: "#888"
(* -> *)[*].style.animated: true
# Scoped globs (only inside a container)
cloud: {
*.style.fill: "#e8f5e9"
}Variables and Substitutions
vars: {
primary: "#3b5bdb"
secondary: "#74c0fc"
accent: "#f06595"
# In-file config (overridden by CLI flags)
d2-config: {
layout-engine: elk
theme-id: 0
}
}
server: {
style.fill: ${primary}
style.stroke: ${secondary}
}Reusable Style Classes
classes: {
important: {
style: {
stroke: red
stroke-width: 3
bold: true
}
}
faded: {
style: {
opacity: 0.4
}
}
}
# Apply to shapes
critical_db.class: important
legacy_service.class: faded
# Apply multiple (left-to-right, later wins)
service.class: [important; faded]
# Apply to connections
A -> B: {class: important}SQL Tables (ER Diagrams)
users: {
shape: sql_table
id: int {constraint: primary_key}
email: varchar {constraint: unique}
name: varchar
org_id: int {constraint: foreign_key}
created_at: timestamp
}
organizations: {
shape: sql_table
id: int {constraint: primary_key}
name: varchar
}
# Foreign key connection
users.org_id -> organizations.id
# Multiple constraints
users: {
shape: sql_table
id: int {constraint: [primary_key; not_null]}
}UML Class Diagrams
UserService: {
shape: class
# Fields: visibility prefix + name: type
-users: User[]
+db: Database
#cache: Cache
# Methods: visibility + name(params): return
+getUser(id: string): User
+createUser(data: UserInput): User
-validateEmail(email: string): bool
}Visibility: + public, - private, # protected
Sequence Diagrams
auth_flow: {
shape: sequence_diagram
# Actors are auto-created on first reference
# but explicit order controls visual position
client
gateway
auth_service
db
client -> gateway: "POST /login"
gateway -> auth_service: "validate(credentials)"
auth_service -> db: "SELECT user WHERE email=?"
db -> auth_service: "user record"
auth_service -> gateway: "JWT token"
gateway -> client: "200 OK + token"
# Notes (standalone shape on actor with no connections)
auth_service."validates password hash"
# Groups / fragments
retry: {
gateway -> auth_service: "retry"
}
}Icons
# Icon from URL (Terrastruct's free icon library)
server: {
icon: https://icons.terrastruct.com/tech/server.svg
}
# Local file
logo: {
icon: ./assets/logo.png
}
# Standalone image shape
github: {
shape: image
icon: https://icons.terrastruct.com/social/github.svg
}
# Control icon position
server: {
icon: https://icons.terrastruct.com/tech/server.svg
icon.near: top-left
}Free icons: https://icons.terrastruct.com
Composition: Layers, Scenarios, Steps
# Layers: independent views (no inheritance)
layers: {
overview: {
web -> app -> db
}
detailed: {
web: "Nginx" { shape: rectangle }
web -> app: "HTTP/1.1"
app -> db: "PostgreSQL wire protocol"
}
}
# Scenarios: variations on base diagram
web -> app -> db
scenarios: {
with_cache: {
app -> cache: "read-through"
}
with_cdn: {
cdn -> web
}
}
# Steps: sequential, each inherits previous
steps: {
s1: { user }
s2: { user -> web }
s3: { user -> web -> app }
s4: { user -> web -> app -> db }
}Themes
Set via CLI: d2 --theme=0 input.d2 output.svg
Or in-file: vars: { d2-config: { theme-id: 300 } }
| ID | Name | Character |
|---|---|---|
| 0 | Neutral Default | Clean, professional |
| 1 | Neutral Grey | Muted, monochrome |
| 3 | Flagship Terrastruct | Vibrant, colorful |
| 4 | Cool Classics | Blues and greens |
| 8 | Colorblind Clear | Accessible palette |
| 200 | Dark Mauve | Dark mode |
| 300 | Terminal | Monospace, hacky |
| 302 | Origami | Paper aesthetic |
| 303 | C4 | Architecture style |
Theme overrides (fine-tune colors):
vars: {
d2-config: {
theme-overrides: {
B1: "#0057b8" # primary brand color
N7: "#1a1a2e" # darkest neutral
}
}
}Color codes: N1–N7 (neutrals), B1–B6 (brand), AA2–AA5 (accent A), AB4–AB5 (accent B)
Layouts
Set via CLI: d2 --layout=elk input.d2 output.svg
Or in-file: vars: { d2-config: { layout-engine: elk } }
| Engine | Best For | Notes |
|---|---|---|
| dagre | Everything — use this always | Fast, handles nested containers and cross-container connections well |
| elk | Only if user explicitly requests it | Extremely slow in WASM (minutes per render). Do not choose it yourself. |
Default rule: never set layout-engine at all. Dagre is the default and handles the vast majority of diagrams well, including nested containers and cross-container connections. Only set layout-engine: elk if the user specifically asks for it.
Direction control:
direction: right # top-level: up, down, left, right
# ELK supports per-container direction
container: {
direction: right
a -> b -> c
}Imports
# Spread file contents into current scope
...@shared_styles.d2
# Assign file to a key
network: @network_diagram.d2
# Import specific object from file
db_schema: @schema.usersBeautiful Diagram Patterns
Architecture Diagram
# Global style
*.style.border-radius: 6
*.style.font-size: 13
direction: right
internet: {
shape: cloud
label: "Internet"
}
frontend: {
label: "Frontend\n(React)"
icon: https://icons.terrastruct.com/dev/react.svg
style.fill: "#e8f4fd"
}
api: {
label: "API Gateway"
style.fill: "#fff3cd"
}
services: {
label: "Microservices"
style.fill: "#f8f9fa"
auth: "Auth Service"
orders: "Orders Service"
payments: "Payments Service"
}
db: {
shape: cylinder
label: "PostgreSQL"
style.fill: "#d4edda"
}
cache: {
shape: queue
label: "Redis"
style.fill: "#fce8e6"
}
internet -> frontend
frontend -> api: "HTTPS"
api -> services.auth: "JWT validate"
api -> services.orders
api -> services.payments
services.orders -> db
services.payments -> db
services.auth -> cache: "session"Flowchart
direction: down
start: {shape: circle; style.fill: "#4caf50"; style.font-color: white}
end: {shape: circle; style.fill: "#f44336"; style.font-color: white; label: "End"}
decision: {shape: diamond; label: "Valid?"}
process: "Process Request"
error: "Return Error"
start -> process
process -> decision
decision -> end: "Yes"
decision -> error: "No"
error -> start: "Retry"ER Diagram
users: {
shape: sql_table
id: uuid {constraint: primary_key}
email: varchar(255) {constraint: [unique; not_null]}
name: varchar(100)
created_at: timestamptz
}
posts: {
shape: sql_table
id: uuid {constraint: primary_key}
author_id: uuid {constraint: foreign_key}
title: varchar(255)
body: text
published_at: timestamptz
}
comments: {
shape: sql_table
id: uuid {constraint: primary_key}
post_id: uuid {constraint: foreign_key}
author_id: uuid {constraint: foreign_key}
body: text
}
posts.author_id -> users.id
comments.post_id -> posts.id
comments.author_id -> users.idSaving Rendered SVGs
Always make two d2_render calls and save three files:
Step 1 — Render for saving (full SVG with embedded fonts, for browser viewing):
d2_render(d2_code=..., skip_fonts=false)Save this output as diagrams/<stem>.svg
Step 2 — Render for display (fonts stripped, compact for LLM context):
d2_render(d2_code=..., skip_fonts=true)Use this output to show the diagram in the conversation.
Convention:
- Directory:
diagrams/(relative to the current project root; create if it doesn't exist) - Filename stem:
YYYY-MM-DD_HH-MM_<slug>- Timestamp uses local time in 24-hour format
- Slug is 2–4 words derived from the diagram's subject, lowercase, hyphenated
- Example stem:
2025-02-20_14-32_auth-flow-sequence - Example stem:
2025-02-20_09-05_aws-three-tier-architecture - Example stem:
2025-02-20_17-45_users-orders-er-diagram
Save two files with the same stem:
diagrams/<stem>.d2— the D2 source code (use the Write tool)diagrams/<stem>.svg— the full SVG from Step 1 (use the Write tool)
After saving, tell the user both paths so they can edit the source or open the SVG in a browser.
SVG files open directly in any browser and are fully interactive (tooltips, links, animations).
Quick Tips
- Keys vs Labels: The key is the identifier (
my_shape); the label is the display text (my_shape: "Display Text"). Connections use keys, not labels. - Semicolons separate multiple declarations on one line:
a; b; cora -> b -> c - Repeated connections each create a new arrow — D2 does not merge them
_(underscore) refers to the parent container, useful for cross-container connectionsnearkeyword positions shapes or icons relative to constants:top-left,top-center,top-right,center-left,center-right,bottom-left,bottom-center,bottom-right- SVG class attributes are written from D2's
classproperty, enabling CSS/JS post-processing - Animated connections (
style.animated: true) create flowing arrows in SVG output - Use
style.multiple: truefor a "stacked documents" visual effect - SQL tables:
strokestyles the body,fillstyles the header - Avoid
width/heighton containers with dagre — use elk for that feature