DWSD.md

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.orgchart
mode: declared
individuals:
- { 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 →

Inferred from the org folder · click a box for its details, +/− to collapse, scroll to zoom, drag to pan

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 employment key, no vacancy key), no status or vacancy colors and no view switch (the org chart and the units view are two kinds, not two views of one document). The shared title: / 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: an individual named NA1, NA2, … with the open position in its role: and a normal reports-to. The renderer has no special case for NA — 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:

LayerHow it reaches a person
Interactionsinteraction-of: "[[Person]]" on an interaction / meeting (a list names several people)
Agreementsagreement-of: "[[Person]]" on an agreement; or for-domain: "[[Role]]" via a kept role
AI Agentsbound-to: "[[Person]]" on an ai-agent; or for-domain: "[[Role]]"
Rolesa 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 conversationobserves: "[[Person]]" on a signal / insight (a pin); a driver / proposal via for-domain: "[[Role]]"
Organizational decision recordsfor-domain: "[[Role]]" on an organizational decision record
Notesdoc-level notes: - on: "[[Person]]" in the chart itself (notes)
Tagsthe 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.

ModeMeaning
inferDerive 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.
declaredThe 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.
mixedThe 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

FieldValues / shapeDefaultDiagnostic on misuse
modeinfer | declared | mixedinferorgchart.unknown-mode (warning; falls back to infer)
titlestring(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.
tagsstring 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)
radiusa number | inf | { up, down } (each a number or inf){ up: 1, down: 1 }orgchart.unsupported-radius (unparseable value → 1; negative → clamped to 0)
individualslist 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)
noteslist 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)

FieldValues / shapeDefaultDiagnostic on misuse
namestring (required) — the person’s entity name, the basename of the individual file—orgchart.invalid-individual (entry skipped when name is missing or empty)
rolestring — the free-text job title, painted as the second line(none — a one-line box)(none — a non-string value is ignored)
deltaadd | change | remove(none)orgchart.unknown-delta (warning; the value is omitted)
tagsstring 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).
anchorsmap 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)

FieldValues / shapeDefaultDiagnostic on misuse
fromstring (required) — the report—orgchart.invalid-edge (entry skipped when missing)
tostring (required) — the manager—orgchart.invalid-edge (entry skipped when missing)
deltaadd | 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:

DiagnosticSeverityMeaning
orgchart.multiple-managersinfoA person reports to several managers; the first manager by name is kept and the rest are not drawn.
orgchart.cyclewarningThe 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:

DiagnosticSeverityMeaning
orgchart.for-domain-names-individualinfoA 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:

RowSource
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
levelthe person’s level: frontmatter (from the index)
reports-tothe manager — as indexed, or the declared manager in mode: declared — as a navigable wikilink
member-ofthe units the person belongs to, navigable
Interactionsevery interaction / meeting whose interaction-of: names [[Person]]
Agreementsevery agreement whose agreement-of: names [[Person]], or whose for-domain: names a role the person keeps (the row says via )
AI Agentsevery ai-agent whose bound-to: names [[Person]] (or via a role)
Change conversationthe signal / insight pins that observe: "[[Person]]", and the driver / proposal pins that reach the person via a role
Organizational Decision Recordsorganizational decision records whose for-domain: names a role the person keeps — via
Notesthe inline notes: on the person
Tagsthe 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 with the role glyph. There is deliberately no direct-reports list — the chart already shows the reports.

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: infer
focus: "[[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: mixed
individuals:
- { 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; the for-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

index.md
# 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](../units/index.md); for the flow between
work systems see the [Topology DSL](../topology/index.md).
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:
````markdown
```dwsd.orgchart
mode: declared
individuals:
- { 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.
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 `employment` key, no `vacancy` key), **no status or
> vacancy colors** and **no `view` switch** (the org chart and the units view are two
> kinds, not two views of one document). The shared `title:` / `tags:` **are**
> painted, as the same document header the units view and the topology paint
> (see [Header](#header) below). A **vacancy** is a
> *modeling convention*, transparent to the renderer: an `individual` named
> `NA1`, `NA2`, … with the open position in its `role:` and a normal
> `reports-to`. The renderer has no special case for `NA` — 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](#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](../units/index.md) 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](../document-header.md)
above the chart — the title on the left, the tag chips on the right — exactly as
the [Units DSL](../units/index.md) 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](./notes.md)) |
| 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](../locator-grammar.md#entity-wikilinks--resolution-by-target-type-the-role-bridge-the-path-form-phase-42).
## 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](./notes.md)) |
**`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](../units/index.md#collapse) (`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 <Role>*) |
| **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 <Role>* |
| **Notes** | the inline [`notes:`](./notes.md) 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 <Role>* with the role glyph. There is deliberately **no direct-reports
list** — the chart already shows the reports.
**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`.
```dwsd.orgchart
# 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.
```dwsd.orgchart
# 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: infer
focus: "[[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.
```dwsd.orgchart
# 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: mixed
individuals:
- { 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](./notes.md) — the inline `notes:` surface and its `[[Name]]` locator.
- [Units DSL](../units/index.md) — the sibling structure view over `member-of`.
- [Locator grammar](../locator-grammar.md) — the bare `[[Name]]` wikilink as an addressable place for signals, agreements, interactions, agents and notes; the `for-domain: [[Role]]` bridge.
- [The document header](../document-header.md) — `title:` / `tags:`, the shared header every DSL paints.