Flight Route DSL — Syntax Reference
The Flight Route DSL describes a Flight-Levels flight route: the abstract
design layer of a delivery — the bands work moves through, the stages
(an item type at a band) it passes, and the glyph-typed edges that connect
them. A route is authored unbound: its path: is a pure design, and a separate
bound-to: layer maps its bands onto concrete boards. A route carries header
metadata (its title:, descriptive tags:, the primary item type it is for:),
a bands: declaration, optional external triggers:, the abstract path:, and
an optional bound-to: binding.
The title: + tags: half of that header is shared with the board and
topology DSLs — same keys, same meaning, same shape in all three. See
The document header.
The DSL is plain YAML. dwsd-flightroute is the VS Code language id for
standalone .dwsd-flightroute files. In Markdown, the fence tag that the
extension’s preview renders live is dwsd.flightroute (the dot form):
```dwsd.flightroutetitle: Minimal Routefor: featurebands: - name: Delivery fl: 1path: - epic @ Delivery -> story @ Delivery```In plain Markdown / GitHub the same example reads as a YAML document either way.
Live example · Field Issue Route — Voltfeld example →
title: Field Issue Route for: "[[Field Issue]]" for-domain: "[[Charger ART]]" bands: - name: Field - name: Train fl: 2 - name: Product Lines fl: 2 - name: Teams fl: 1 triggers: - name: a charger fails on site, an installer calls, telemetry flags a pattern generates: - issue @ Field path: # Field Service brings the week's issues to triage - issue @ Field --> issue @ Train # safety or fleet-down: the issue becomes a hotfix in the expedite lane - issue @ Train -> hotfix @ Train # everything else becomes a feature for the next PI - "issue @ Train -> [[Feature]] @ Train" # the product line takes the hotfix into its expedite lane - hotfix @ Train --> hotfix @ Product Lines # one of its teams fixes it immediately - hotfix @ Product Lines --> hotfix @ Teams # the verified fix flows back into the issue - hotfix @ Teams ..> issue @ Train # the hotfix reaches the fleet over the air - hotfix @ Train ..> () # Field Service hears back what happened to what it reported - issue @ Train ..> issue @ Field bound-to: - band: Train boards: - board: "[[Charger ART Board]]" stages: - issue: "lane:Field Issue/col:Triage" - hotfix: "lane:Field Issue/col:Fix-in-Progress" - Feature: "lane:Feature/group:Preparation/col:Program Backlog" - band: Product Lines boards: - board: "[[Wallbox Board]]" stages: - hotfix: "lane:Hotfix/col:In Progress"
Fence-tag caveat — the examples on the topic pages use
dwsd-flightroute. Syntax highlighting accepts four spellings (wsd-flightroute,dwsd-flightroute,wsd.flightroute,dwsd.flightroute— the markdown-injection grammar), but the preview extractor currently accepts onlydwsd.flightroute. So a```dwsd-flightroutefence highlights but does not render. The topology DSL already resolved this asymmetry by accepting all four spellings in both places; board and route have not yet. Until they do, use the dot form for a fence you want to see rendered.
The parser is tolerant: it never throws on author input. A malformed bands:,
triggers:, path:, or bound-to: entry produces a route.* diagnostic (mostly
a warning) and the offending entry is skipped — the rest of the document still
parses. A small set of violations are deliberately hard errors instead: the
retired document-title key route:, the four retired compat keys
(fl-triggers:, fl-path:, flow:, fl-bands: — see below), and the
structural Flow ⊆ Path rule (route.flow-not-subset-path). The diagnostic codes
are listed per construct on each topic page and summarized under
Diagnostics (route.*) below.
When prose and code disagree, the code wins. Every key, value set, and diagnostic on these pages was authored from the parser (
src/route/yaml-parser.ts), the AST types (src/route/types.ts), and the validator rules (src/route/rules/*). If a future change makes this page inaccurate, the shipped parser is the authority.
Note on field names that do not exist. The Flight Route DSL has no inline sugar beyond the edge glyphs — every construct is a spelled-out YAML key (
bands:,triggers:,path:,bound-to:,for:). The only string sugar is the edge line itself (epic @ Strategy -> story @ Delivery) and the stage formitemType @ Band.The following are not keys — do not write them:
bind:/realizes:— the band→board binding section is the singlebound-to:key (workspace-consistent with the AI-agentbound-to:frontmatter). These spellings are rejected, not aliased.fl-triggers:/fl-path:/flow:/fl-bands:— retired. The old compat form is gone. The current keys aretriggers:,path:,bound-to:andbands:(in that order). A document that carries any of the four gets oneroute.legacy-syntaxerror that names every old key found and its replacement, and the canvas stays empty — a current key beside an old one is not drawn. Files in the old form must be rewritten by hand; there is no alias. See Legacy keys.deliveredge — retired. Delivery is a dotted edge to the reserved outside-band sink:stage ..> ().route:header — retired. The document title istitle:(see The document header). A root-levelroute:is a hardroute.retired-keyerror that names its own fix; the value is dropped, not read.The tolerant parser silently ignores any non-key (no diagnostic, no field), so an example that writes one would parse “clean” while dropping the value — which is exactly why
route:and the compat keys were given real errors instead of joining that list. Two repo gates hold the line on the examples below:pnpm check:dsl-corpus-sweepparses every tracked unit and live fence and requires 0 diagnostics, andpnpm check:retired-key-auditfails on any surviving retired spelling.
Topic pages
The reference is split across per-topic pages:
- The document header —
title:+tags:, the header shared by all three DSLs, the missing-value convention, and the retired keys. - The locator grammar — the cross-DSL locator that
bound-to:stage refinements and route refs address boards and columns with. - Path & bands — the abstract
path:model,bands:declaration (with the multi-FL span), stage identity (itemType @ Band, inferred from edge endpoints), and externaltriggers:. - Edges — the three canonical edges (
generate/copy/feedback), the()/⊙ outside-band delivery, and the band constraints. - Binding — the
bound-to:layer that maps a route band onto a concrete board via the shared locator, and the Flow ⊆ Path rule. - Anchors — route refs via the shared locator, stage markers, and the drawer.
- Examples — full worked
dwsd-flightrouteexamples. - Examples gallery — the generated SVG gallery (Source fence + rendered SVG per example) for GitHub / handover reading.
Design notes — YAML, humans, and machines
The Flight Route DSL is YAML by design. Every example in this reference is
also a valid YAML document — block style (bands: as an indented list) and flow
style ({ name: Delivery, fl: 1 }) both parse — so any YAML-aware editor, linter,
or formatter can read and write it. The package ships its own tolerant parser
(src/route/yaml-parser.ts) modeled verbatim on the board and topology parsers.
The syntax optimizes for readability and round-trippability, not brevity.
Field names are spelled out (bands, triggers, bound-to — not
abbreviations), values are explicit, and the parser never throws: unknown or
malformed input yields a route.* diagnostic and a skip rather than a hard crash.
That verbosity is deliberate — the format is meant to be read and written by
humans, but above all by machines. The closed, documented set of keys and the
per-construct diagnostics give a coding agent everything it needs to author or
repair a route without a UI; the live fence preview is then the way a person
checks what the machine produced.
Route header fields
Every field below is implemented by the parser (src/route/yaml-parser.ts,
normalizeRouteYaml) and produces the frozen RouteAst contract
(src/route/types.ts). Each optional field is omitted when absent — never a
placeholder '' / []. The Diagnostic column names the route.* code an
author hits on misuse.
| Field | Values / shape | AST field | Default | Diagnostic on misuse |
|---|---|---|---|---|
title | string (the route name) | title? | (none — omitted when absent) | (none — a non-string or empty value is silently ignored) |
tags | string list — document scope only (a scalar string is accepted as a one-element list) | tags? | (none) | tolerant — non-string entries dropped, empties trimmed |
for | a quoted wikilink ("[[Feature]]") or plain text (feature) — the primary item type the route describes; painted as the first header chip, before the for-domain chips | primaryItemType | null | route.invalid-for (warn) on an unquoted for: [[Feature]]; route.unresolved-item-type-ref (warn, needs a workspace) on a wikilink that resolves to nothing; route.item-type-ref-kind (warn, needs a workspace) on a wikilink that is not a flight-item-type — plain text is never checked |
for-domain | wikilink or list of wikilinks ("[[Delivery Ops]]") — the domain (a work system or a role) responsible for the route | forDomain? | (none — omitted when absent) | route.invalid-for-domain (warn) on a malformed shape; route.unresolved-domain-ref (warn, needs a workspace) on an unknown target |
bands | list of band records { name, fl?: number | number[] } | routeBands? | (none) | route.invalid-band (skip); route.invalid-band-fl (warn) |
triggers | list of { name, generates } — external notes OUTSIDE bands | routeTriggers? | (none) | route.invalid-trigger (entry skipped) |
path | glyph-typed edge list (see Edges) | pathEdges? | (none) | route.invalid-edge (skip); route.edge-band-constraint (info) |
bound-to | band→board bindings (see Binding) | boundTo? | (none) | route.invalid-bound-to (skip); route.flow-not-subset-path (error) |
title is the optional route name — the header text. It is the same key
the board and topology DSLs use; when it is absent the field is omitted from
the AST (never ''). The type-named route: key it replaced is retired and now
errors — see The document header. tags are
descriptive chips at document level only — deliberately not on bands or stages
(“Color and information belong only where they carry meaning” — at a stage a tag
would only repeat what the band position already says; see
The document header).
for names the primary item type the route describes (kept verbatim as
primaryItemType): a quoted wikilink when it has a file, plain text when it has
none. The preview paints it as the first chip of the header (before the
for-domain chips) with the flight-item-type glyph — see
Responsibility and references.
for-domain names who is responsible for
the route — see Responsibility and references.
bands, triggers, path, and
bound-to are documented in full on their topic pages.
Legacy keys — one error that names the fix
The pre-Phase-24 compat form used four top-level keys: fl-triggers:, fl-path:,
flow: and fl-bands:. They are gone. A route whose YAML root carries any of them
shows one route.legacy-syntax error, ranged on the line of the first old key,
and an empty canvas — never a partial render of the current keys that sit beside
it. A file with two or more old keys still gets one error, naming all of them in
source order:
| Old key | Write instead |
|---|---|
fl-triggers: | triggers: |
fl-path: | path: |
flow: | bound-to: |
fl-bands: | bands: |
The message reads Legacy Flight Route key is no longer supported — replace `fl-bands:` with `bands:` (see the route reference).
for one key and Legacy Flight Route keys are no longer supported — replace `fl-triggers:` with `triggers:`, `fl-path:` with `path:` (see the route reference).
for several. The values are never echoed. The old record shapes differ from the
current ones (an fl-bands: entry { id, label, levels } becomes a bands: entry
{ name, fl }; the deliver edge becomes stage ..> ()), so the rename is not a
find-and-replace: rewrite the section in the current form described on the topic
pages. In a live preview, a key typed into a route that was drawing correctly keeps
the last good picture next to the error until the key is removed.
Empty routes
A route without bands is not a blank page. When a document has a title:, tags:,
for-domain: or for: (or notes) but no bands and no stages, the preview shows
its header — the title, the for: chip, the for-domain chips and the tags —
and below it one muted hint, No bands or stages yet — add `bands:` and `path:`.
No bands are invented. A route that has only for: shows the header strip with the
for: chip, like any other header content. A document
with none of the route keys at all keeps the original hint,
Type `title: name` to begin, on the small empty canvas.
Diagnostics (route.*)
The parser is tolerant and never throws; most malformed input yields a route.*
warning and a skip, while a few violations are deliberate errors. The full
diagnostic family — every code below is emitted by the shipped parser
(src/route/yaml-parser.ts) or a validator rule (src/route/rules/*):
| Code | Origin | Severity | Trigger |
|---|---|---|---|
route.syntax-error | yaml-parser | error | The YAML itself does not parse (including any attempt at an executable !!-tag, which is rejected, never run). A path line or generates: entry holding an unquoted wikilink (- [[Epic]] @ Core -> …, a YAML flow sequence) lands here, and the message names the fix: quote the line |
route.legacy-syntax | parser / yaml-parser | error | A legacy route "Name" header with a non-mapping root — migrate to title:; or a retired compat key at the YAML root (fl-triggers: / fl-path: / flow: / fl-bands:) — one error per file on the first such key, every key found named with its replacement, the canvas stays empty (Legacy keys) |
route.retired-key | yaml-parser | error | The retired root route: document-title key was authored — rename to title: |
route.internal-error / parse.internal-error | yaml-parser / parser | error | Belt-and-suspenders catch for an unexpected throw |
route.invalid-band | yaml-parser | tolerant (warn) | A malformed bands: entry (not an object, missing name, or an invalid fl) is skipped |
route.invalid-trigger | yaml-parser | tolerant (warn) | A malformed triggers: entry or generate target (not "itemType @ Band" or "[[ItemType]] @ Band") is skipped |
route.invalid-edge | yaml-parser | tolerant (warn) / error | A malformed path: edge is skipped; a wrong-glyph -> () delivery or a multi-hop line is an error naming the fix |
route.invalid-bound-to | yaml-parser | tolerant (warn) | A malformed bound-to: entry is skipped |
route.invalid-for | yaml-parser | tolerant (warn) | An unquoted for: [[Feature]], which YAML reads as a nested list; the value is dropped and the message names the quoting fix |
route.invalid-for-domain | yaml-parser | tolerant (warn) | A for-domain: value that is neither a quoted wikilink nor a list of them (an unquoted [[Name]], which YAML reads as a nested list, gets a message naming the quoting fix); the malformed entries are dropped |
route.empty-route | r5 | info/warn | An empty route |
route.flow-not-subset-path | r12 | error | The Flow ⊆ Path rule is violated, or a bound-to: band is not declared in bands: (AST-internal — no lookup) |
route.invalid-band-fl | r13 | warn | A new-model bands: fl outside the canonical 1..3 |
route.edge-band-constraint | r14 | info | The SEED-004 / Klaus-#20 band constraint (a teaching nudge — cross-band generate or same-band copy) |
route.unresolved-board-ref | xref/r4 | warn | A bound-to: board not found in the workspace index |
route.unresolved-domain-ref | xref/r5 | warn | A for-domain target that is not found in the workspace index (needs a workspace; without one nothing is reported). The route still renders |
route.unresolved-item-type-ref | xref/r6 | warn | A stage item type or a for: written as a wikilink that is not found in the workspace (needs a workspace). Plain text is never looked up and never reported. The route still renders |
route.item-type-ref-kind | xref/r7 | warn | A stage item type or a for: written as a wikilink that resolves, but to something that is not a flight-item-type (a work system, a board, a role, a note, …). The message names the kind found. The link keeps its look and its drawer row; text stages and a text for: are never checked, a wikilink that resolves to nothing is route.unresolved-item-type-ref instead (needs a workspace) |
route.ambiguous-stage-name | r15 | warn | One band holds two stage names that differ only by case ([[Feature]] @ Train beside feature @ Train), or one name written both as a wikilink and as text |
route.stage-name-spelling | r16 | warn | A bound-to stage key or a note on: locator names a stage by a different case than the stage’s name (feature: for [[Feature]] @ Train). The old spelling still works, for one release |
Security: the parser uses the default core YAML schema — no executable
tags (no RCE via !!js/function / !!python/object; an unknown !!-tag becomes
a route.syntax-error, never a side effect), ReDoS-safe edge-glyph regexes, and
it never throws past the public boundary.
Responsibility and references
for-domain names who takes care when the route changes. Its value is a
work system or a role — a responsibility area, not a place where work flows. The
work systems a route flows through are derived, never authored: bound-to names
a board, and the board’s visualization-of names its work system.
The bound route worked example carries
one. A list of quoted wikilinks names more than one domain. A malformed value is a
route.invalid-for-domain warning; a target the workspace does not know is a
route.unresolved-domain-ref warning. Neither blocks the route.
The preview shows each domain as a chip next to the title. When the
workspace knows the work system or role, the chip label shows in the link ink
with a pointer and a heavier outline while you hover it, and a click opens the
entity. When it does not, the chip is plain. When more domains
are named than fit, the header shows as many as fit and a +N chip for the rest.
A published picture renders without a workspace, so its chip is always the plain
state.
Every header chip carries the glyph of its target’s type. A domain chip
shows the work-system glyph, or the role glyph when its target is a role; a name
the workspace does not know keeps the work-system glyph. The glyph is the same
size as on the other chips and sits at the left of the label. A link written in
another case ([[delivery ops]]) opens the real file (Delivery Ops): the chip
keeps the text as written and the click uses the entity’s own name.
for: is the first chip of the header, in front of the domain chips, with
the flight-item-type glyph — also when it is plain text, when it is a broken link
and when it names something that is not a flight item type. A quoted wikilink
(for: "[[Feature]]", with an alias "[[Feature|the feature]]" the chip shows
the alias) is a link like a domain chip: link ink, a pointer and a heavier outline on
hover when the workspace knows it, a click opens the file. A plain-text for: feature
is a chip without a link and is never looked up, even when a flight-item-type of
that name exists. A broken wikilink stays a plain neutral chip; the warning is
route.unresolved-item-type-ref. A wikilink that resolves to something other than
a flight-item-type stays a link and gets a route.item-type-ref-kind warning. for:
and the domains share one run: when they do not all fit, the header shows as many
as fit and one +N chip for the rest. The route’s drawer (the . doc-note bubble in
the header opens it) lists the for: value as a Flight Item Type row, in the
same three looks as a stage row (link, not-found, plain).
A stage’s item type is text or a wikilink — nothing is matched by name.
Text (order @ Shops) describes the item locally to the route: it is never
looked up, never linked and never flagged, even when a flight-item-type of that
name exists. A quoted wikilink ("[[Feature]] @ Train") is a real
connection to a flight-item-type entity: it resolves like any wikilink, and one
that resolves to nothing is a broken link. An alias keeps a short label
([[Customer Order|order]] @ ERP shows order and links to Customer Order).
Both forms mix freely, also on one line, in path lines, in the expanded
{ from, to, kind } form and in a trigger’s generates:. A line that holds a
wikilink is quoted, as everywhere in YAML:
path: - "[[Epic]] @ Train -> [[Feature]] @ Train" - "[[Feature]] @ Train -> story @ Teams"A resolved wikilink stage label shows in the link ink with a pointer, the stage box
takes the hover tone, and a click opens the entity. Route links carry no underline,
not even on hover. A broken wikilink
stays a plain neutral label on the canvas; the warning is a
route.unresolved-item-type-ref in the Problems panel and the not-found row in
the stage drawer. A wikilink that resolves, but to something that is not a
flight-item-type (a work system, a board, a role), stays a link with the same look
and gets a route.item-type-ref-kind warning. A link written in another case
([[customer order]] @ ERP) opens the real file (Customer Order); the label stays
as written. Static pictures (an exported SVG or PNG, the Markdown preview,
the example gallery) show the link ink only, and a host without a workspace shows every
wikilink stage as a plain label. Path endpoints and trigger generates: targets
are both stages. Item types inside a route are part of the route itself and are
never filtered. What the Filter menu does hide closes up: when one of the pills on a
stage side (agreement, interaction, AI agent, …) is hidden, the others move into
its place with no gap; bands, stages and arrows never move.
A stage is named by its token, or for a wikilink by its link target (not the
alias). stage:Feature @ Train in a note locator, Feature: as a bound-to
stage key. The match is exact within the route, and a locator never nests
[[…]]. Switching a stage from text to a wikilink therefore changes its name
(feature → Feature): update the locators and keys with it. The old
lowercase spelling still pins and anchors the stage for one release, with a
route.stage-name-spelling warning (and anchor.route-stage-spelling on the
Markdown line of a cross-file locator in the editor). Two names in one band that
differ only by case get a route.ambiguous-stage-name warning.
A click on the stage box opens the stage drawer. The box, and any of its markers, open the one drawer of that stage (one drawer per stage, across its before / in / after sides). The drawer lists the stage’s Flight Item Type first, then everything anchored to the stage (agreements, interactions, AI agents, change entries) and the stage’s notes. A stage with nothing anchored still opens, with the Flight Item Type row alone. The row follows the item type’s form:
- a wikilink stage the workspace knows: the row is a link and opens the item type (the row shows the alias, when there is one);
- a wikilink stage the workspace does not know: the not-found row every broken wikilink gets (the alert glyph and “Target not found in workspace”), with no link;
- a text stage, and a wikilink stage in a picture laid out without a workspace (a published picture, a host with no workspace index): the plain name, no link and no warning. Text is a description local to the route, not a reference.
The stage label of a wikilink keeps its own action and opens the item
type directly, without a drawer. Pills, the change pill, the bound-board pill and the
note marker keep their own actions too. With the keyboard, Tab moves to a stage
box (in stage order), Enter or Space opens the drawer and, pressed again, closes
it; the focus ring follows the box border. A host can keep the older model, where only
the markers open the drawer, with openOn: 'marker-zone' on renderRoute.
Like what you see? → Start — build your own org folder in ~2 min