Org Chart DSL — Syntax Reference
The Org Chart DSL describes the formal line organization: individual
entities and their reports-to chains, top-down, up to the top. It renders a
tree of person boxes — the root (the person nobody reports to) on top, every
report below its manager, and several roots side by side when the
organization has more than one person without a manager. Nothing softer than the
reporting line is drawn: no team membership, no flow, no work systems. For units
and their nesting see the Units DSL; for the flow between
work systems see the Topology DSL.
The DSL is plain YAML. A standalone document is a .dwsd-orgchart file
(language id dwsd-orgchart); inside Markdown the live-rendering fence is
```dwsd.orgchart — the spellings dwsd-orgchart, wsd.orgchart
and wsd-orgchart are tolerated as well:
```dwsd.orgchartmode: declaredindividuals: - { name: Mara Holt, role: Owner & Head Baker } - { name: Jonas Keller, role: Head of Operations }reports-to: - { from: Jonas Keller, to: Mara Holt }```A viewer that recognizes the fence (the dwsd-vscode extension preview, VS
Code’s built-in Markdown preview, the website) renders the block live; the
headless CLI (dwsd-dsl export) writes the same picture as SVG or PNG.
Live example · Org chart of the whole company — Voltfeld example → · The same people in the units tree →
The parser is tolerant: it never throws on author input. Unknown or
malformed fields produce an orgchart.* diagnostic (a warning or info) and the
offending value is clamped to a default or skipped — the rest of the document
still parses. The diagnostic codes are listed per field below.
Fields that do not exist. The Org Chart DSL has no employment status and no vacancy key (no
employmentkey, novacancykey), no status or vacancy colors and noviewswitch (the org chart and the units view are two kinds, not two views of one document). The sharedtitle:/tags:are painted, as the same document header the units view and the topology paint (see Header below). A vacancy is a modeling convention, transparent to the renderer: anindividualnamedNA1,NA2, … with the open position in itsrole:and a normalreports-to. The renderer has no special case forNA— such a box looks like any other box, with the position as its second line.
Design notes
Top-down, roots on top. The layout reduces the reports-to edges to a
forest: every person keeps exactly one manager (the first manager by name when
several are declared — orgchart.multiple-managers), an edge that would close a
cycle is not drawn (orgchart.cycle), and every person without a surviving
manager is a root. Roots are laid out side by side; each subtree is placed by
its own width so siblings never overlap.
Leaf stacking above three leaves. A manager with more than three
leaf reports (people who have no reports of their own) does not fan them out into
a row that grows with the department. The leaves are stacked vertically in one
column, each connected by a short tick from one spine. Ten direct reports become a
tall, narrow list rather than a 1,500-pixel-wide row. A stacked leaf that carries
a navigation-pin or decision-record tab sits 21 px lower, so the tab never
covers the leaf above it (UNITS-POLISH-02); documents without tabs are
unchanged, and the units view’s tree stacks its leaves by the same rule.
The spine hangs from the manager’s centre (STRUCT-32). When every child is a
leaf, the column’s vertical line drops straight through the manager’s own
centre and the leaves are indented 24 px to its right — so the stack reads as one
straight line under the box rather than as a line with a sideways jog to reach it.
A manager with branch subtrees and a leaf stack keeps the column on its own
vertical under the bus, beside the branches. The same geometry drives the units
view’s layout: tree, from one shared module, so a department drawn in both views
has the same shape in both.
Orthogonal reporting lines, no arrowheads. Edges are made of horizontal and vertical segments only — a bus below the manager, one drop per child — with rounded corners and no arrowheads; the direction is the vertical position.
Fixed box geometry. Every box in a chart is 200 px wide and 56 px tall —
fixed, with or without a role:, so a chart reads as a grid of equal boxes.
The text is fitted to the box, never the box to the text: a name wider than the
176 px inner width wraps onto two lines at a word boundary, and a line that
still does not fit ends in an ellipsis; a role wider than 176 px ends in an
ellipsis. Nothing is ever clipped mid-word. A person without role: keeps the
name at the same height as everyone else, with an empty second line. When any
person carries a marker (an anchored entity, a tag, a note), one marker row of
uniform height is added to every box, so the chart stays aligned.
The role line. A person’s role: is a free-text job title (the
dwsd-markdown individual.role string), painted as a smaller second line under
the name. The modeled type: role entities a person keeps are not on the box —
they are in the drawer (see Drawer). The role line stays (decided in
the Phase 41 close-out walk, 2026-09-17); it is the reason an NA vacancy is not
an empty box.
Focus ring. In a focus + radius cut-out the focal person’s box wears the topology’s dashed focus ring and soft shadow — not a thicker stroke, so the box itself looks like every other box.
One delta language: one dash, the colour carries the meaning (STRUCT-28).
A delta paints the box stroke and the reporting edge in the add (green) / change
(orange) / remove (red) colour, with one dash pattern for all three (10 5, a
15 px period) on the whole manager-to-box piece. There used to be a pattern per
delta — 6 3, 4 4 and a 2 4 dotted — three rhythms distinguishing what the
colour already said, at periods too fine to resolve. The box also takes a light
tint of its delta colour (#ddf0ea add, #fdeee3 change, #fbe5e0 remove),
which is what makes a delta visible at normal zoom; a person box’s background
carries no other meaning, so it was free to take it. The Units
DSL does the same and the topology deliberately does not —
a topology node’s fill IS its Flight-Level band colour, so there the delta rides
the stroke alone. A removed person stays in place, drawn as removed.
Header. title: and tags: paint the shared document header
above the chart — the title on the left, the tag chips on the right — exactly as
the Units DSL and the topology do; with neither key there is
no header band. title: also names the export file.
The canvas follows the editor in the preview and stays white in the export
(STRUCT-29). In the editor preview there is no canvas rectangle behind the
chart — the editor’s own background shows through — and the header title takes the
editor’s foreground colour, so the chart reads in a dark theme without a white slab
under it. Every export (PNG, SVG, clipboard image), the CLI render and VS Code’s
Markdown preview still paint a white canvas with a #333333 title: the renderer
writes those as presentation attributes and no export path carries a stylesheet. Box
fills and chip colours are identical in both — only the canvas and the header text
differ.
Annotations on a person. The eight annotation layers of the topology —
Interactions · Agreements · AI Agents · Roles · Change conversation · Organizational
decision records · Notes · Tags — land on a person box the same way they land on a work-system
node: in the bottom marker row, in the drawer, under the layer filter. An entity
reaches a person through a bare wikilink on one of the value-polymorphic
keys: observes: "[[Jonas Keller]]" on a signal, interaction-of: "[[Jonas Keller]]"
on a 1:1, agreement-of: "[[Jonas Keller]]" on a personal working agreement,
bound-to: "[[Jonas Keller]]" on an AI agent — the wikilink names the person,
and every org chart that draws them renders the marker. A decision record
(or any other for-domain type) reaches a person through a role the person
keeps: for-domain: "[[Head of Operations]]" lands on every keeper of that role
(one hop — the role’s own for-domain is not followed), and the drawer row says
via Head of Operations. A for-domain that names a person directly lands
nowhere; the validator says so (orgchart.for-domain-names-individual). In
mode: infer a person’s tags come from the individual’s frontmatter
tags:; a declared tags: on the same person in the chart overlays (the union
is shown). What lands where, per layer:
| Layer | How it reaches a person |
|---|---|
| Interactions | interaction-of: "[[Person]]" on an interaction / meeting (a list names several people) |
| Agreements | agreement-of: "[[Person]]" on an agreement; or for-domain: "[[Role]]" via a kept role |
| AI Agents | bound-to: "[[Person]]" on an ai-agent; or for-domain: "[[Role]]" |
| Roles | a type: role file whose role-keeper: names the person — a single wikilink or a list, so one role with four keepers marks all four boxes (in infer and mixed; a declared chart takes its role markers from its own anchors:) |
| Change conversation | observes: "[[Person]]" on a signal / insight (a pin); a driver / proposal via for-domain: "[[Role]]" |
| Organizational decision records | for-domain: "[[Role]]" on an organizational decision record |
| Notes | doc-level notes: - on: "[[Person]]" in the chart itself (notes) |
| Tags | the individual’s frontmatter tags: (infer) ∪ the chart’s declared tags: |
Roles are their own marker type (ROLE-03). The role marker — a small hat-glasses
glyph and a count, in the existing bottom marker row, so every box keeps one uniform
height — shows the roles a person keeps. A person who keeps two roles carries one
marker with the count 2, and an agreement scoped to a role (for-domain:) still
shows on every keeper of that role, side by side with the role marker. The marker
follows the Roles entry of the filter menu (the eighth layer, key roles), in
exports and with --hide roles on the command line. On the canvas the glyph takes
the marker row’s neutral tint like every other glyph; the Soft Coral of the role
color shows on the filter-menu dot.
The full contract — resolution by the target’s type, the path form for a same-named person and unit, the role bridge — is the locator grammar.
Modes
mode: selects how the chart is assembled. Values: infer, declared,
mixed. The default (when mode: is absent) is infer.
| Mode | Meaning |
|---|---|
infer | Derive the chart from the workspace: every type: individual file the indexer knows, with its reports-to frontmatter as the edges. Without focus: the whole organization is drawn — roots are the people without reports-to. With focus: (or an embedding file that is itself a person) the cut-out bounded by radius: is drawn. The author lists nothing. |
declared | The author lists the individuals and the reports-to edges explicitly. Nothing is read from the workspace, so the document renders identically in the editor preview, the Markdown preview and the headless CLI. |
mixed | The author’s entries overlay the inferred chart as a diff — each person or edge carries a delta (add / change / remove) applied on top of what the indexer found. A person listed without a delta merges with the inferred one (the declared role: wins). |
An unrecognized mode: value emits orgchart.unknown-mode (warning) and
falls back to infer.
Focus and radius. focus: "[[Name]]" centers the chart on one person;
radius: { up: N, down: M } bounds how many reporting hops are drawn above
(managers) and below (reports). Either value may be inf — unbounded. The
classic question “who is this person’s manager, and everybody under them?” is
radius: { up: 1, down: inf }. In infer mode the radius applies whenever a
focal person exists (the default is one hop each way); in declared and mixed
mode it prunes the chart only when radius: is written explicitly — a
hand-authored chart is never silently cut down by the default.
The embedded-focus rule. An org chart embedded in another file gets that
file’s basename as its default focus only when that file is a person — an
indexed individual or a name the chart itself declares. Embedded in a unit, a
work system or a tour page, the chart has no focal and shows the whole
organization. focus: self means the same as leaving focus: out: the
containing file, if it is a person.
Large views. A chart with more than 40 people is still laid out completely;
the editor preview adds an orgchart.large-view info to its diagnostics so the
author knows the picture is big by design, not by mistake.
Headless rendering. mode: infer needs the workspace index; the CLI has
none and refuses such a standalone document with a one-line message and exit
code 1. Fenced inside a Markdown file, such a block (or one with no mode: key,
which defaults to infer) is skipped with that same message and the file’s other
fences still export — exit 1 only when nothing in the file was exportable.
Author declared or mixed for anything that must export from the command
line (a mixed document renders its declared part alone there).
Syntax reference
Every field below is implemented by the parser (src/orgchart/parser.ts) and
validated by src/orgchart/validate.ts. The Diagnostic column names the
orgchart.* code an author hits on misuse.
Top-level fields
| Field | Values / shape | Default | Diagnostic on misuse |
|---|---|---|---|
mode | infer | declared | mixed | infer | orgchart.unknown-mode (warning; falls back to infer) |
title | string | (none — omitted when absent) | (none — a non-string or empty value is silently ignored). Painted as the document heading in the shared header when present; also names the export file. |
tags | string list (a scalar string is accepted as a one-element list) | (none) | tolerant — non-string entries dropped, empties trimmed. Painted as chips in the shared header when present. |
focus | [[Name]] | [[Name|Alias]] | self | (none — whole org, or the embedding person) | orgchart.invalid-focus (warning, parse); orgchart.unresolved-focus (warning, validate — an explicit target that is neither indexed nor declared) |
radius | a number | inf | { up, down } (each a number or inf) | { up: 1, down: 1 } | orgchart.unsupported-radius (unparseable value → 1; negative → clamped to 0) |
individuals | list of person objects | [] | orgchart.invalid-individual (a non-object or name-less entry is skipped) |
reports-to (alias reports_to) | list of edge objects | [] | orgchart.invalid-edge (a non-object or from/to-less entry is skipped); orgchart.duplicate-edge (info; the second occurrence is ignored) |
notes | list of { on, text, resolved? } | (none) | orgchart.note-orphan (warning — see notes) |
radius also accepts a lateral key for symmetry with the topology; it has
no effect — a reporting tree has no lateral relation. A bare number applies to
both directions; the object form fills a missing direction with 1.
Person fields (each entry of individuals)
| Field | Values / shape | Default | Diagnostic on misuse |
|---|---|---|---|
name | string (required) — the person’s entity name, the basename of the individual file | — | orgchart.invalid-individual (entry skipped when name is missing or empty) |
role | string — the free-text job title, painted as the second line | (none — a one-line box) | (none — a non-string value is ignored) |
delta | add | change | remove | (none) | orgchart.unknown-delta (warning; the value is omitted) |
tags | string list (a scalar string is accepted as a one-element list) | (none) | tolerant — non-string entries are dropped. Unioned with the individual’s frontmatter tags: when an index is present (infer / mixed). |
anchors | map of typeKey → string list — declared anchored entities, the no-index authoring form | (none) | orgchart.invalid-anchors (the value is ignored when it is not a map) |
Any other key on a person — an employment key, level:, member-of: and every other
dwsd-markdown frontmatter key — is ignored without a diagnostic. The
drawer reads level and member-of from the workspace index, not from the
chart.
Edge fields (each entry of reports-to)
| Field | Values / shape | Default | Diagnostic on misuse |
|---|---|---|---|
from | string (required) — the report | — | orgchart.invalid-edge (entry skipped when missing) |
to | string (required) — the manager | — | orgchart.invalid-edge (entry skipped when missing) |
delta | add | change | remove | (none) | orgchart.unknown-delta (warning; the value is omitted) |
A reports-to edge points from the report up to the manager — the same
direction as the reports-to: frontmatter key. An edge whose endpoint is not a
drawn person is dropped silently. Two structural diagnostics come from the
layout’s forest reduction and are raised at validate time:
| Diagnostic | Severity | Meaning |
|---|---|---|
orgchart.multiple-managers | info | A person reports to several managers; the first manager by name is kept and the rest are not drawn. |
orgchart.cycle | warning | The edge closes a reporting cycle (A → B → A, or a self-edge) and is not drawn. Among the edges of one cycle, the one whose from sorts last loses, so the outcome does not depend on authoring order. |
One annotation diagnostic is raised at validate time when an index is present:
| Diagnostic | Severity | Meaning |
|---|---|---|
orgchart.for-domain-names-individual | info | A for-domain: in some entity file names a drawn person. for-domain names a domain — a work system or a role — so the entity lands nowhere. Message: `Jonas Keller Leads Operations.md` names the individual `Jonas Keller` in its `for-domain:` — point it at the person's role instead (the entity lands nowhere). It is a statement about the TARGET, not a quotation of the anchoring file: the spelling shown is the INDEXED one, which is not necessarily what that file wrote (Phase 43, IN-11). Placed on the mode line, one per (person, file). |
Collapse
Every person who has at least one report in the rendered chart carries a small
+/− toggle at the top-right corner of the box — the same toggle as on a unit box
in the Units DSL (STRUCT-41). A click folds that
person’s reports away: the box stays where the layout puts it, the reports and
everyone below them disappear, the rest of the chart closes ranks, and the box shows
a +N badge on its role line, right-aligned, where N counts every hidden
descendant once. A collapsed person’s role line is shortened so it stays centred and
clear of the badge; the drawer still shows the full role. A second click unfolds it.
The radius decides who can appear at all. focus: + radius: cut the chart
first; collapse only hides within what is left. A person whose reports were all cut
away by the radius has no reports in the chart and therefore no toggle, and a
+N never counts — and a + never reveals — anyone beyond the authored radius.
The toggle is not part of the drawer’s click model. Folding a subtree while a drawer is open leaves that drawer as it is; a click on the box body still opens or switches the drawer.
Transient, and export is what you see. The collapsed set lives in the preview
session only — never in the document, never in the workspace state — and is reset
when another document is rendered; a reload opens everything again. There is no
key for it. The editor preview’s Export menu honors it: a collapsed chart exports
collapsed, badge included (PNG, SVG, clipboard image). The command-line export and
the example pictures in the published examples/ folder are always fully open
and show no toggle. VS Code’s built-in Markdown preview is fully open too and draws
the toggle, which does nothing there (the preview page has no channel back to the
extension). The editor preview keeps the working toggle.
A toggle does not move the picture, and children grow out of their parent
(STRUCT-42). The layout extent is pinned to the fully-open chart, so the <svg>’s
viewBox, the zoom width and both scroll offsets stay exactly where they were and
only the boxes change. On a collapse the reports shrink back into their manager’s
box and fade while their reporting lines retract into the manager; on an expand
they grow out of it — starting small and transparent at the manager and moving
into place — and their lines extend with them. Every other box glides to its new
place. All of it runs in parallel on one clock of about 200 ms, ease-out; a
second click during the motion carries on from where the boxes are rather than
jumping to the end. prefers-reduced-motion: reduce skips the frames. The motion ships in the package
and is on by default in every host that uses the package’s collapse handler
(attachOrgchartCollapseHandlers) — the VS Code preview, the dwsd.md site and any
other host get it with no glue; pass animate: false to switch it off. It is never
part of an export: the Markdown preview and the CLI are static, and an export taken
while a transition is running captures the settled picture. The units view runs the
same motion from the same module.
A jump does not expand a collapsed subtree. A jump to a person who is hidden inside a collapsed subtree does not unfold it; unfold the manager by hand.
Drawer
A click on a person box opens the shared drawer for that person (a second click
closes it; hovering a box pulses its reporting lines). The title of the drawer is a
link to the person: it carries the link color and a trailing ”›” at rest, is underlined
only on hover and keyboard focus, and opens the person’s file beside the preview (the
drawer stays open; titleAction: 'select' or 'none' give a host the other models).
The drawer opens with one merged Roles section, then the entity rows, then the
annotation bands in the topology drawer’s order and wording:
| Row | Source |
|---|---|
| Roles (first) | the free-text job title (role:) and the modeled type: role entities this person keeps (role-keeper:), merged. Entries are de-duplicated by name (case-insensitive, trimmed): a job title that names a role file shows once, as that role. An entry is a link where the role file resolves and plain text where it does not |
level | the person’s level: frontmatter (from the index) |
reports-to | the manager — as indexed, or the declared manager in mode: declared — as a navigable wikilink |
member-of | the units the person belongs to, navigable |
| Interactions | every interaction / meeting whose interaction-of: names [[Person]] |
| Agreements | every agreement whose agreement-of: names [[Person]], or whose for-domain: names a role the person keeps (the row says via |
| AI Agents | every ai-agent whose bound-to: names [[Person]] (or via a role) |
| Change conversation | the signal / insight pins that observe: "[[Person]]", and the driver / proposal pins that reach the person via a role |
| Organizational Decision Records | organizational decision records whose for-domain: names a role the person keeps — via |
| Notes | the inline notes: on the person |
| Tags | the person’s tags — frontmatter (infer) ∪ declared |
There is no separate Job title section any more: the free-text job title is an entry
of Roles, and the drawer’s job-title category and the roles category are folded
into the one section at render time (the source data still carries both groups).
The Roles section follows the Roles layer: with Roles hidden the modeled entries go and a free-text job title stays as a plain row; a person with no job title and no roles has no Roles section. The change-conversation pins read in chain order — Signal, Insight, Driver, Proposal — whatever order they arrive in, and the decision records stay their own section.
Every band row is the shared anchor row of the topology drawer: a navigable
wikilink when the entity is indexed, a plain label when it is a name-only
declared anchor, the same provenance dots. A role-bridged row carries a second
line via
A row is a link exactly when its target resolves (STRUCT-34). That holds for
the entity rows too, declared mode included: reports-to, member-of and a role
entry in Roles are navigable only when the workspace can actually open what they name. A manager
declared in the chart with no entity file behind him renders as plain text — no
link styling, no click handler, nothing happens on click. It is not a “not in
workspace” hint; a dead affordance is worse than no affordance. A row whose target
does resolve opens it in the editor you have configured for that file type (your
workbench.editorAssociations), as a preview tab in the source group, and a DWSD
file’s preview renders immediately (STRUCT-30). In declared mode without an
index only the rows the chart itself carries (the job title in Roles, declared
anchors, tags, notes) appear.
Examples
Each example is also shipped verbatim as a file under examples-src/orgchart/
and is parsed clean (0 diagnostics) by the publish-preview drift gate. Names are
from the Goldcrust Bakery example organization.
1. Declared line organization
The smallest useful chart: an owner, a head of operations, the two shop managers
and a driver — plus one vacancy modeled as NA1.
# 1 — declared line organization: the Goldcrust Bakery reporting lines, one root # (Mara Holt), one vacancy (NA1) modeled as an individual with the position as role. mode: declared individuals: - { name: Mara Holt, role: Owner & Head Baker } - { name: Tom Reuben, role: Baker } - { name: Jonas Keller, role: Head of Operations } - { name: Lena Frei, role: Shop Manager } - { name: Selin Aydin, role: Shop Manager } - { name: Pia Brandt, role: Driver } - { name: NA1, role: Delivery Coordinator } reports-to: - { from: Tom Reuben, to: Mara Holt } - { from: Jonas Keller, to: Mara Holt } - { from: Lena Frei, to: Jonas Keller } - { from: Selin Aydin, to: Jonas Keller } - { from: Pia Brandt, to: Jonas Keller } - { from: NA1, to: Jonas Keller }
2. Focus + radius (a manager and all their reports)
The whole organization lives in the workspace; the chart cuts out one person, the
one manager above and everything below without limit. Renders in the editor
preview (it needs the index); the CLI refuses it. The picture in the published
examples/orgchart/ folder is drawn against the Goldcrust Bakery example
organization.
# 2 — focus + radius (boss + all reports): one hop up to the manager, the entire# report subtree down. mode: infer reads the individuals from the workspace.mode: inferfocus: "[[Jonas Keller]]"radius: { up: 1, down: inf }3. Mixed with delta
A staffing change drawn over the inferred organization: the NA1 seat goes away
and its successor arrives on the same reporting line, and a driver leaves.
# 3 — mixed with delta: the NEXT stage of the organization as an overlay on the# inferred base. A removed person stays in place, drawn as removed.mode: mixedindividuals: - { name: NA1, delta: remove } - { name: Rafael Costa, role: Delivery Coordinator, delta: add } - { name: Pia Brandt, delta: remove }reports-to: - { from: NA1, to: Jonas Keller, delta: remove } - { from: Rafael Costa, to: Jonas Keller, delta: add } - { from: Pia Brandt, to: Jonas Keller, delta: remove }See also
- Org chart notes — the inline
notes:surface and its[[Name]]locator. - Units DSL — the sibling structure view over
member-of. - Locator grammar — the bare
[[Name]]wikilink as an addressable place for signals, agreements, interactions, agents and notes; thefor-domain: [[Role]]bridge. - The document header —
title:/tags:, the shared header every DSL paints.
Like what you see? → Start — build your own org folder in ~2 min