Getting started with D2

D2 turns a text description of a system into a diagram. You write what exists and how it connects; a layout engine decides where everything goes. This page is the first stop before you write one: the mental model, the syntax you will use every day, how layout works, and recipes you can copy.

user: Customer {shape: person}

shop: Online shop {
  web: Web app
  api: API
  web -> api: JSON
}

db: Orders DB {shape: cylinder}

user -> shop.web: browses
shop.api -> db: writes
CustomerOnline shopOrders DBWeb appAPI JSONbrowseswrites

Start in two minutes

You picked the CLI in watch mode, which is the most comfortable loop: you save the file, the browser tab refreshes with the new render.

  1. Install

    The install script picks the best method for your machine (it uses your package manager if it can). Run it with --dry-run first to see what it will do.

    $ curl -fsSL https://d2lang.com/install.sh | sh -s -- --dry-run
    $ curl -fsSL https://d2lang.com/install.sh | sh -s --
    $ d2 --version

    Alternatives: go install github.com/d2lang/d2@latest, or a binary from GitHub releases. Since 0.9.0 the TALA engine and PNG/PDF/PPTX/GIF export are built in, so there is nothing else to install.

  2. Create a file and start watching

    $ echo 'x -> y: hello world' > input.d2
    $ d2 -w input.d2 out.svg

    A browser tab opens and live-reloads on every save. Errors show up in that tab and in the terminal, with line numbers.

  3. Get editor support

    Install the VS Code extension or the Vim plugin. Syntax highlighting catches many errors as you type: if a word suddenly changes color, it probably hit a reserved symbol.

  4. Tidy and export

    $ d2 fmt input.d2
    $ d2 input.d2 out.png
    $ D2_LAYOUT=elk d2 -w input.d2 out.svg

    fmt fixes indentation and shortens imports. The output format follows the file extension (svg, png, pdf, pptx, gif, or txt for ASCII art). The last line shows how to switch layout engine for a session.

The mental model

D2 has very little syntax because almost everything is the same idea: a key, optionally a label, optionally a map of fields in braces. Read these six ideas once and most of the language becomes predictable.

aws.dbkey path :  Orders DBlabel {shape: cylinder}map of fields
websource key ->arrow aws.dbtarget key : readslabel {style.stroke-dash: 3}map of fields

Keys are identity, labels are display

The key is the name you use in code; the label is what the reader sees. If you give no label, the key is shown. Keep keys short (be, db) and labels readable (Backend). Keys are case-insensitive.

# Key on the left, label on the right
be: Backend
fe: Frontend

# Connections use KEYS, not labels
fe -> be: calls
BackendFrontend calls

Connections point at keys, and mentioning a key creates it

There is no separate "declare" step: a -> b creates both shapes if they do not exist. The flip side is the most common beginner bug: writing a label in a connection (Backend -> Frontend) silently creates two new shapes.

Same key, same object: declarations merge

You can declare a shape once and add fields to it anywhere later. Each line refines the same object. Connections are the exception: writing a -> b twice draws two arrows.

merge.d2Open in Playground
# Declared once...
api: Payments API

# ...then extended anywhere later.
# Same key = same object, settings merge.
api.shape: hexagon
api.style.fill: "#FFF4D6"

api -> ledger
Payments APIledger

Nesting is scope

A shape with a map of children becomes a container. Dots build a path into it (aws.vpc.app), and _ means "my parent" when you need to reach outward from inside a container.

nesting.d2Open in Playground
aws: AWS {
  vpc: VPC {
    app: App server
  }
}

# Dots form a path to a nested key
aws.vpc.app -> aws.s3: uploads
AWSVPCs3App server uploads

You describe, the engine places

There are no coordinates, and the order of lines usually does not matter (sequence diagrams are the exception). You steer the result with a few levers instead: direction, the choice of layout engine, grids, and near. See Layout.

Keep the system apart from its look

D2's own design principle is "design the system, not the diagram". Describe shapes and connections first, then style them in bulk with classes, globs, variables and themes. Styles can even live in a separate file you import.

Syntax basics

Each card shows the source on the left and what D2 0.9.0 actually rendered on the right. Every snippet opens in the Playground so you can tinker.

Shapes and labels

Docs

A key on its own creates a rectangle. key: label sets the text, and shape picks another form. Semicolons put several declarations on one line. All 18 standard shapes are in the catalog below.

shapes.d2Open in Playground
# A bare key creates a rectangle
server

# key: label
db: PostgreSQL
db.shape: cylinder

# Several shapes on one line
cache; jobs
jobs.shape: queue
cache.shape: stored_data
serverPostgreSQLcachejobs

Connections

Docs

Four arrows: ->, <-, <-> and -- (no arrowhead). Add a label after a colon. A chain like a -> b -> c declares two connections at once.

connections.d2Open in Playground
direction: right
client -> api: request
api <- worker: polls
api <-> cache
logs -- api

# Chaining: three connections
api -> db -> backup
clientapiworkercachelogsdbbackup request polls

Arrowheads and repeated connections

Docs

Set source-arrowhead / target-arrowhead on a connection. Options: triangle (default), arrow, diamond, circle, box, cross, and crow's feet cf-one, cf-one-required, cf-many, cf-many-required. An arrowhead only appears on an end that has one, so a source arrowhead needs <->. Repeated connections are separate objects you can target by index.

arrowheads.d2Open in Playground
direction: right
# A source arrowhead needs a source end: use <->
users <-> orders: places {
  source-arrowhead.shape: cf-one-required
  target-arrowhead.shape: cf-many
}

# Same pair twice = two connections
a -> b
a -> b

# Target one of them by index (0-based)
(a -> b)[1].style.stroke: red
usersordersab places

Containers

Docs

Give a shape a map with children and it becomes a container. Label it with the shorthand aws: AWS {…} or with label: inside. Use dot paths from outside and _ to reach the parent from inside.

containers.d2Open in Playground
cloud: AWS {
  lb: Load balancer
  api: API
  lb -> api
}

# Reach inside with a dot path
cloud.api -> cloud.db

office: Office {
  # _ means "my parent" (here: root)
  laptop -> _.cloud.lb: VPN
}
AWSOfficeLoad balancerAPIdblaptop VPN

Styles

Docs

Every visual property lives under style, either as a map or with dot notation (workers.style.multiple: true). Connections take styles too. Quote hex colors, because # starts a comment.

KeywordValuesNotes
fill, stroke, font-colorCSS color name or quoted hexfill is shapes only
stroke-width1 to 15
stroke-dash0 to 10dashed borders and lines
border-radius0 to 20high values give a pill
opacity0 to 1handy for fading in scenarios
font-size8 to 100
shadow, 3d, multiple, double-bordertrue / falseshapes only
bold, italic, underline, animatedtrue / falseanimated moves dashes along a line
font, text-transform, fill-patternmono / uppercase… / dots, lines, grain
styles.d2Open in Playground
direction: right
api: {
  style: {
    fill: "#E8EEFF"
    stroke: "#0D32B2"
    stroke-width: 2
    border-radius: 8
    shadow: true
  }
}
workers.style.multiple: true

api -> workers: jobs {
  style.stroke-dash: 3
  style.animated: true
}
apiworkers jobs

Text, Markdown and code

Docs

Block strings open with a pipe and a language: |md for Markdown, |go, |ts… for highlighted code, |latex for math. They close with a matching |. If the content itself contains a pipe, use ||md … ||. shape: text gives plain floating text.

why: |md
  ### Why a queue?
  - absorbs traffic bursts
  - **retries** come for free
|

snippet: |go
  q.Publish(ctx, job)
|

hint: just floating text {shape: text}

why -> snippet
Why a queue?absorbs traffic burstsretries come for freeq.Publish(ctx, job)q.Publish(ctx, job)just floating text

Icons, tooltips and links

Docs

icon takes a URL or a local path; shape: image shows the icon alone. Browse the free set at icons.d2lang.com. tooltip and link make SVG output interactive (in PNG/PDF they move to an appendix).

icons.d2Open in Playground
direction: right
# Free icon set: https://icons.d2lang.com
server: API server {
  icon: https://icons.d2lang.com/tech/022-server.svg
  tooltip: Runs on 3 nodes
  link: https://example.com/runbook
}

# shape: image shows the icon alone
github: {
  shape: image
  icon: https://icons.d2lang.com/dev/github.svg
}

server -> github: pulls code

Preview not shown here: icons are downloaded and embedded when you render, so try this one in the Playground.

Titles, notes and label positions

Docs

near pins a shape to the edge of the diagram: top-left, top-center, top-right, center-left, center-right, bottom-left, bottom-center, bottom-right. On label and icon it also accepts outside-… and border-… variants. That is how you add titles and side notes.

title: |md
  # Checkout flow
| {near: top-center}

note: Draft, not final {
  shape: text
  near: bottom-right
}

cart -> payment -> receipt
payment.label.near: outside-top-center
Checkout flowDraft, not finalcartpaymentreceipt

Classes

Docs

A class is a named bundle of fields (shape, style, size, even label). Apply it with class:, or several at once with class: [a; b]. Fields you set directly on the shape win over the class.

classes.d2Open in Playground
classes: {
  service: {
    style.fill: "#E8EEFF"
    style.border-radius: 6
  }
  store: {
    shape: cylinder
    style.fill: "#FFF4D6"
  }
}

api.class: service
auth.class: service
db.class: store

api -> db
auth -> db
apiauthdb

Variables and config

Docs

vars defines values you reuse with ${name}. The special d2-config map holds settings that would otherwise be CLI flags (layout-engine, theme-id, dark-theme-id, pad, center, sketch), so the file renders the same everywhere. Flags and environment variables still take precedence.

vars: {
  env: prod
  # Config that would otherwise be CLI flags
  d2-config: {
    layout-engine: elk
    pad: 20
  }
}

api: API (${env})
db: DB (${env})
api -> db
API (prod)DB (prod)

Globs

Docs

Globs change many things in one line. * matches siblings in the current scope, ** recurses into containers, *** also reaches layers and imports. (* -> *)[*] targets connections. Filter with &field: value (or !& to exclude). Globs also apply to shapes declared after them.

globs.d2Open in Playground
web -> api -> db
api -> cache
db.shape: cylinder
cache.shape: cylinder

# Every top-level shape
*.style.border-radius: 8

# Only shapes that are cylinders (filter with &)
*: {
  &shape: cylinder
  style.fill: "#FFF4D6"
}

# Every connection
(* -> *)[*].style.stroke-dash: 3
webapidbcache

Comments and quoting

Docs

# comments to the end of the line; """ opens and closes a block comment. Labels are usually unquoted. Quote them when they contain symbols D2 uses (: ; # { } . $). Single quotes also stop ${…} substitution.

quoting.d2Open in Playground
# Line comment
x -> y # trailing comment

"""
Block comment,
spanning lines
"""

# Quote labels that contain reserved symbols.
# Single quotes also stop ${...} substitution.
z: 'Price: $5 # not a comment'
# Hex colors MUST be quoted (# starts a comment)
z.style.fill: "#FFF4D6"

Nothing to see here: this card is about how text is parsed.

Imports

Docs

Split diagrams across files. key: @file makes a whole file the value of a key; ...@file spreads its contents into the current map; @file.key takes one object. Leave off the .d2 extension. Paths are relative to the importing file. The Playground cannot do imports, so use the CLI.

main.d2
# Spread a whole file into this map
...@styles

# Import a file as the value of a key
team: @people

# Import just one object from a file
lead: @people.alice

team.bob -> api
api.class: svc
teamAliceapiAliceBob
people.d2
alice: Alice {shape: person}
bob: Bob {shape: person}
styles.d2
classes: {
  svc: {style.fill: "#E8EEFF"}
}

Shape catalog

Set any of these with shape:. Special shapes with their own rules are covered in the recipes: sql_table, sequence_diagram, plus class for UML (docs), text and image. circle and square keep a 1:1 ratio.

catalog.d2Open in Playground
rectanglesquarepageparallelogramdocumentcylinderqueuepackagestepcalloutstored_datapersonc4-persondiamondovalcirclehexagoncloud
grid-rows: 3
grid-columns: 6
grid-gap: 16
rectangle: {shape: rectangle}
square: {shape: square}
page: {shape: page}
parallelogram: {shape: parallelogram}
document: {shape: document}
cylinder: {shape: cylinder}
queue: {shape: queue}
package: {shape: package}
step: {shape: step}
callout: {shape: callout}
stored_data: {shape: stored_data}
person: {shape: person}
c4-person: {shape: c4-person}
diamond: {shape: diamond}
oval: {shape: oval}
circle: {shape: circle}
hexagon: {shape: hexagon}
cloud: {shape: cloud}

Layout: how D2 places things

A layout engine computes every position from your description. You don't move boxes; you pull on four levers.

Lever 1: direction

Sets the flow of the whole diagram: down (default), up, right, left. With dagre and ELK it is global; only TALA lets you set it per container.

direction.d2Open in Playground
direction: right
plan -> build -> test -> ship
test -> build: fails
planbuildtestship fails

Lever 2: the layout engine

Three engines ship with D2. The same source below gives three quite different pictures:

platform.d2
users: Users {shape: person}
edge: Edge {
  cdn: CDN
  lb: Load balancer
}
core: Core services {
  api: API
  auth: Auth
  worker: Worker
}
data: Data {
  pg: Postgres {shape: cylinder}
  redis: Redis {shape: cylinder}
  bus: Event bus {shape: queue}
}
users -> edge.cdn -> edge.lb -> core.api
core.api -> core.auth
core.api -> data.pg
core.api -> data.redis
core.api -> data.bus -> core.worker
core.worker -> data.pg
dagre (default)Playground
UsersEdgeCore servicesDataCDNLoad balancerAPIAuthWorkerPostgresRedisEvent bus

Fast, layered and hierarchical, with curved edges. Good for trees and simple flows. Weaker with many containers and crossing edges.

ELKPlayground
UsersEdgeCore servicesDataCDNLoad balancerAPIAuthWorkerPostgresRedisEvent bus

Layered with orthogonal (right-angle) edges and tidier containers. A good next step when dagre gets tangled, and it points SQL foreign keys at the exact row.

TALAPlayground
UsersEdgeCore servicesDataCDNLoad balancerAPIAuthWorkerPostgresRedisEvent bus

Built for software architecture: not tied to one hierarchy, favours symmetry, places labels around obstacles. Small edits can reshuffle the whole layout.

Pick an engine with -l elk, D2_LAYOUT=elk, or in the file with vars: {d2-config: {layout-engine: elk}}. List engines and their options with d2 layout and d2 layout tala. For TALA's randomness, --tala-seeds controls the layout attempts.

FeaturedagreELKTALA
direction per containernonoyes
near another shape (not just a corner)nonoyes
Lock position with top + leftnonoyes
width / height on containersnoyesyes
Connection from a container to its own childnoyesyes
SQL foreign keys point at the exact rownoyesyes
Routed (not straight) edges between grid cellsnonoyes

Lever 3: grids

Setting grid-rows and/or grid-columns on the root or on a container switches it to a strict grid, which ignores the engine for placement. Whichever key comes first decides the fill order: rows first fills row by row. Set one key alone and cells stretch to fill. Space them with grid-gap, vertical-gap, horizontal-gap; grid-gap: 0 makes tables and stacks.

# grid-rows comes first, so cells fill row by row
grid-rows: 2
grid-columns: 3
grid-gap: 12

Web; Mobile; Partner API
Orders; Billing; Search
WebMobilePartner APIOrdersBillingSearch
nested-grid.d2Open in Playground
stack: Platform {
  grid-rows: 3
  grid-gap: 0
  apps: Applications {
    grid-columns: 3
    web; ios; android
  }
  runtime: Kubernetes
  infra: AWS
}
PlatformApplicationsKubernetesAWSwebiosandroid

Lever 4: near, sizes and pinning

near with a corner constant takes a shape out of the flow (titles, legends, notes; see the near card). width and height fix a shape's size. With TALA only, near: other.shape keeps two shapes close and top + left pin one in place while the rest flows around it.

Layout docs Grid docs Positions docs

Recipes

Complete, working starting points for the diagrams you said you will write most. Copy one, rename the keys, and keep the structure.

Architecture overview

Legend docs

Use for: system context, service maps, deployment views.

Containers for boundaries (cloud, VPC, team), classes to keep styling in one place, a Markdown title with near: top-center, and a legend declared in vars.d2-legend. ELK keeps the edges orthogonal and the container tidy.

architecture.d2Open in Playground
Order platform, high levelCustomerAWSStripeAPI gatewayOrders servicePayments serviceEvent busOrders DB SQLOrderPlacedHTTPSchargeLegendInternal serviceThird party
vars: {
  d2-config: {
    layout-engine: elk
  }
  d2-legend: {
    svc: Internal service {
      style: {fill: "#E8EEFF"; stroke: "#0D32B2"}
    }
    ext: Third party {
      style: {fill: "#F2F4F7"; stroke: "#667085"; stroke-dash: 3}
    }
  }
}
direction: right

title: |md
  # Order platform, high level
| {near: top-center}

classes: {
  svc: {
    style: {fill: "#E8EEFF"; stroke: "#0D32B2"; border-radius: 6}
  }
  store: {
    shape: cylinder
    style: {fill: "#FFF4D6"; stroke: "#B7791F"}
  }
  ext: {
    style: {fill: "#F2F4F7"; stroke: "#667085"; stroke-dash: 3}
  }
}

customer: Customer {shape: person}

aws: AWS {
  gw: API gateway {class: svc}
  orders: Orders service {class: svc}
  pay: Payments service {class: svc}
  bus: Event bus {shape: queue}
  db: Orders DB {class: store}

  gw -> orders
  orders -> db: SQL
  orders -> bus: OrderPlaced
  bus -> pay
}

stripe: Stripe {class: ext}

customer -> aws.gw: HTTPS
aws.pay -> stripe: charge

Flowchart

More examples

Use for: decision logic, request handling, user journeys.

oval for start and end, diamond for decisions, edge labels for the branches. The default direction (down) and engine (dagre) suit this well.

flowchart.d2Open in Playground
Request receivedLogged in?Redirect to loginIn cache?Serve cached pageRender pageSave to cacheSend response noyesyesno
start: Request received {shape: oval}
auth: Logged in? {shape: diamond}
login: Redirect to login
cached: In cache? {shape: diamond}
serve: Serve cached page
render: Render page
store: Save to cache
done: Send response {shape: oval}

start -> auth
auth -> login: no
auth -> cached: yes
cached -> serve: yes
cached -> render: no
render -> store -> done
serve -> done
login -> done

Sequence diagram

Docs

Use for: API calls, auth handshakes, anything where time order matters.

Set shape: sequence_diagram. Unlike everywhere else, line order is display order, and every actor shares one scope. Declare actors first to fix their order; a nested key on an actor is a span, a container without connections is a group, a nested key with no connections is a note.

sequence.d2Open in Playground
UserWeb appAPIStripeDBPayment Click "Pay"POST /ordersvalidate cartINSERT orderokchargesucceeded201 CreatedShow receiptIdempotency key prevents double charges
shape: sequence_diagram

# Declare actors first: this fixes their left-to-right order
user: User {shape: person}
web: Web app
api: API
stripe: Stripe
db: DB

user -> web: Click "Pay"
web -> api: POST /orders
api -> api: validate cart

# A nested key on an actor = a span (activation bar)
api.tx -> db: INSERT order
db -> api.tx: ok

# A container with no connections of its own = a group
payment: Payment {
  api -> stripe: charge
  stripe -> api: succeeded
}

# A nested key with no connections = a note
api."Idempotency key prevents double charges"

api -> web: 201 Created
web -> user: Show receipt

Database schema (ERD)

Docs

Use for: schemas, data models, migrations to review.

Each row of a sql_table is column: type, with an optional constraint (primary_key, foreign_key, unique show as PK, FK, UNQ). Connect rows, not tables, and use ELK or TALA so lines land on the exact row. One recursive glob adds crow's feet everywhere.

schema.d2Open in Playground
usersidintPKemailvarcharUNQcreated_attimestampordersidintPKuser_idintFKstatusvarchartotal_centsintorder_itemsorder_idintPK, FKproduct_idintPK, FKqtyintproductsidintPKnamevarcharprice_centsint
vars: {
  d2-config: {
    # ELK and TALA point foreign keys at the exact row
    layout-engine: elk
  }
}
direction: right

users: {
  shape: sql_table
  id: int {constraint: primary_key}
  email: varchar {constraint: unique}
  created_at: timestamp
}

orders: {
  shape: sql_table
  id: int {constraint: primary_key}
  user_id: int {constraint: foreign_key}
  status: varchar
  total_cents: int
}

order_items: {
  shape: sql_table
  order_id: int {constraint: [primary_key; foreign_key]}
  product_id: int {constraint: [primary_key; foreign_key]}
  qty: int
}

products: {
  shape: sql_table
  id: int {constraint: primary_key}
  name: varchar
  price_cents: int
}

# <-> so that both ends can carry an arrowhead
orders.user_id <-> users.id
order_items.order_id <-> orders.id
order_items.product_id <-> products.id

# Crow's feet on every relationship. ** reaches nested rows,
# a plain * would only match top-level tables
(** <-> **)[*]: {
  source-arrowhead.shape: cf-many
  target-arrowhead.shape: cf-one-required
}

Layered stack with grids

Docs

Use for: tech stacks, layered architectures, capability maps.

grid-rows on the outer container stacks the layers; each layer sets its own grid-columns. grid-gap: 0 makes the layers touch like a block diagram.

stack.d2Open in Playground
Product platformClientsAPI gatewayServicesDataWebiOSAndroidOrdersBillingSearchNotificationsPostgresKafka
platform: Product platform {
  grid-rows: 4
  grid-gap: 0

  clients: Clients {
    grid-columns: 3
    Web; iOS; Android
  }
  gateway: API gateway
  services: Services {
    grid-columns: 4
    Orders; Billing; Search; Notifications
  }
  data: Data {
    grid-columns: 2
    Postgres: {shape: cylinder}
    Kafka: {shape: queue}
  }
}

Step-by-step story

Composition docs

Use for: walkthroughs, incident timelines, presentations.

Boards declared under steps each inherit from the previous step. Export them as an animated SVG with d2 --animate-interval 1400 in.d2 out.svg (shown here), as a GIF, or as one slide per step with out.pptx. scenarios inherit from the base instead, and layers start blank.

deploy-steps.d2Open in Playground
DeveloperCIRegistryCluster DeveloperCIRegistryCluster git push DeveloperCIRegistryCluster git pushimage DeveloperCIRegistryCluster git pushimagedeploy
direction: right
dev: Developer {shape: person}
ci: CI
registry: Registry
cluster: Cluster

# Each step inherits everything from the step before it
steps: {
  1: {
    dev -> ci: git push
  }
  2: {
    ci -> registry: image
  }
  3: {
    registry -> cluster: deploy
    cluster.style.fill: "#DCFCE7"
  }
}

One model, many views

Models docs

Use for: big systems where each audience needs a different slice.

Define everything once, hide it all with suspend globs, then unsuspend what a view needs. Put the model in its own file and import it into each view file. This is more advanced; come back to it once the basics feel natural.

access-view.d2Open in Playground
awsapidb
# models: define everything once...
aws: {
  api; db; cache
  api -> db
  api -> cache
}
users -> aws.api

# ...hide everything...
**: suspend
(** -> **)[*]: suspend

# ...then bring back only what this view needs
aws.api: unsuspend
aws.db: unsuspend
(aws.api -> aws.db)[*]: unsuspend

Gotchas

What you'll see, and what to do about it. Several of these came up while preparing this page.

You get duplicate shapes
A connection used a label instead of a key (Backend -> db when the key is be). Connect keys. Docs
A style or line after # vanished
# starts a comment. Quote hex colors: fill: "#FFF4D6". Docs
"substitutions must begin on {"
A $ inside double quotes starts a ${…} substitution. Use single quotes: 'Price: $5'. Docs
A source arrowhead doesn't show
a -> b has no source end. Use <-> when both ends need an arrowhead. Docs
A glob ignores nested shapes
* matches only the current level. Use ** to recurse, *** to also reach layers and imports. Docs
v1.2 became a container
Dots are paths. Quote keys that contain dots: "v1.2". Docs
label or shape used as a column name
Reserved words need quotes: "label": string. Docs
API and api are the same shape
Keys are case-insensitive. Change the label, not the key's case. Docs
A sequence group can't find an actor
In sequence diagrams, actors referenced in a group must be declared at the top level first. Docs
direction on a container does nothing
Per-container direction works only with TALA. Other engines use the root direction. Docs
Container-to-child edge fails
Dagre can't connect a container to its own descendant. Switch to ELK or TALA. Docs
The TALA layout changed completely
TALA has randomness, so small edits can reshuffle it. Pin key shapes with top + left or tune --tala-seeds. Docs
A repeated connection didn't update the first
Repeating a -> b creates a new edge. Edit an existing one with (a -> b)[0].style…. Docs

CLI and config

CommandWhat it does
d2 -w in.d2 out.svgWatch mode: render and live-reload in the browser on every save.
d2 in.d2 out.pngRender once. Format from extension: svg, png, pdf, pptx, gif, txt (ASCII).
d2 fmt in.d2Auto-format in place (indentation, import paths).
d2 validate in.d2Check syntax without rendering. Useful in CI or before committing.
d2 play in.d2Open the file in the online Playground, for sharing.
d2 themesList theme IDs (0 is the default; 200 is a dark theme).
d2 layout [engine]List engines, or one engine's options.
FlagEnv variableIn d2-configEffect
-l, --layoutD2_LAYOUTlayout-enginedagre, elk or tala
-t, --themeD2_THEMEtheme-idColor theme
--dark-themeD2_DARK_THEMEdark-theme-idTheme used when the viewer is in dark mode (SVG)
-s, --sketchD2_SKETCHsketchHand-drawn look
--padD2_PADpadPadding around the diagram (default 100)
-c, --centerD2_CENTERcenterCenter the SVG in its viewbox
--animate-intervalD2_ANIMATE_INTERVALPack boards into one animated SVG or GIF (ms)
--targetRender one board, e.g. --target='layers.x'
--scaleSCALE1 for natural size; default fits SVG to screen

Precedence: flags and environment variables beat d2-config. Putting the config in the file is still the best default, because the diagram then renders the same for everyone. Full list: CLI manual or man d2.

Where to go next

Once this page has you started, pick the path that matches what you need.

I want to…Look intoDocs
Make every diagram share one lookClasses in a shared file plus imports; globs to change defaults; themesclasses, globs, themes
Present a diagram in steps or variantssteps, scenarios, layers; animated SVG, GIF or PPTX exportcomposition, exports
Show slices of a big systemModels with suspend/unsuspend, model-view across filesmodels, model view
Control placement more preciselynear, width/height, grids, TALA's top/leftpositions, dimensions, tala
Draw UML class diagramsshape: class with fields and methodsuml classes
Add math, tables or code to a diagram|latex|, ||md tables, code blockstext
Embed diagrams in docs or notesObsidian plugin, MkDocs, mdBook, Pandoc and moreextensions
Generate D2 from code or a databaseGo API, community builders (Python, JS, Postgres, MySQL)api, extensions
Understand an error or odd layoutTroubleshooting, FAQ, then the Discordtroubleshoot, faq

Asking an agent

D2 is younger than Mermaid or PlantUML, so models sometimes mix in syntax from those, or features from older versions. Naming the version and the engine up front avoids most of that, and running d2 validate on the answer catches the rest.

prompt starter
I'm writing a D2 diagram (D2 v0.9, rendered with the CLI).
Layout engine: [dagre | elk | tala]
Diagram type: [architecture | flowchart | sequence | ERD | grid]

What it should show:
[describe the parts and how they connect]

Constraints:
- Short keys, human-readable labels; connections use keys.
- Put styling in `classes`, not on each shape.
- Quote hex colors; put config in vars.d2-config.
- Only use features supported by my layout engine.

Return the complete .d2 file, then list any keyword
you used that isn't basic syntax and what it does.