Flight Route DSL — Binding
A flight route is authored as the abstract design layer of a delivery: its
path: is a pure design that references no board (see
Path & bands). The bound-to: layer is the separate,
optional concern that maps that abstract route onto one or more concrete
boards — pinning a band (and, optionally, a stage’s item type) to a board
position via the shared locator. This page covers the bound-to: section, the
route→board derivation, and the Flow ⊆ Path invariant that keeps a binding
honest. See the reference index for the route header and the full
diagnostics family, and Path & bands for the bands and
stages a binding refers back to.
When prose and code disagree, the code wins. Every key and diagnostic on this page was authored from
src/route/yaml-parser.ts, the AST types (src/route/types.ts—BindEntry/BoardBinding/StageRefinement), and the validator rules (src/route/rules/r12-flow-subset-path.tsandsrc/route/rules/xref/r4-bound-to-board-ref.ts).
The bound-to: section
bound-to: is a list of band→board bindings. Each entry names one band
(declared in bands:) and a list of boards it binds onto. The section
keyword is exactly bound-to — workspace-consistent with the AI-agent
bound-to: frontmatter from Phase 22.4.
Not keys —
bind:/realizes:/flow:. The band→board binding section is the singlebound-to:key.bind:,realizes:, andflow:are rejected, not aliased.flow:is the retired compat key: a root-levelflow:raises oneroute.legacy-syntaxerror that namesbound-to:as its replacement, and the canvas stays empty.bind:andrealizes:were never keys — the tolerant parser silently ignores them (no field, no binding), so a route that writes one parses “clean” while binding nothing. Writebound-to:.
| Field | Values / shape | AST field | Default | Diagnostic on misuse |
|---|---|---|---|---|
bound-to | list of band→board bindings | boundTo? (BindEntry[]) | (none — absent = fully unbound) | route.invalid-bound-to (entry skipped); route.flow-not-subset-path (error); route.unresolved-board-ref (warn) |
bound-to[].band | string — the band being bound (must be declared in bands:) | BindEntry.band | — | route.flow-not-subset-path (error) when the band is not in bands: |
bound-to[].boards | list of board bindings | BindEntry.boards (BoardBinding[]) | — | route.invalid-bound-to (a malformed board entry is skipped) |
bound-to[].boards[].board | a board locator / wikilink — e.g. "[[team-alpha]]" | BoardBinding.board | — | route.unresolved-board-ref (warn) when the board is not in the workspace index |
bound-to[].boards[].stages | optional per-stage refinement — a list of { <stage name>: locator } pinning a stage to a board position. The key is the stage’s name: its token, or for a wikilink stage its link target (Feature: for [[Feature]] @ Train, never [[Feature]]:); an old lowercase spelling still works for one release with a route.stage-name-spelling warning | BoardBinding.stages? (StageRefinement[]) | (none — a coarse binding omits it) | route.flow-not-subset-path (error) when the pinned itemType is not in that band per path: |
The band→board relation is N:M permissive: one band may bind several boards,
and a coarse binding (a bare board: with no stages:) and a fine
binding (a board: plus per-stage refinements) mix freely within one entry. A
stage refinement maps a stage’s item type to a board position with the same
locator grammar the rest of the workspace uses — lane:Frontend/col:Doing
addresses a lane’s column; see The locator grammar for
the full target syntax.
Route → board derivation — one route, several boards
The point of the split is reuse: the same abstract route (bands, stages,
edges) is designed once and realized on several concrete boards. bound-to:
is the mapping — for each band it names, it lists the board(s) that band lives on
and (optionally) pins each stage’s item type to a concrete column/lane/split on
that board. Because the relation is N:M, one route can drive many boards (a
per-team board each realizing the same delivery route) and one board can host
several bands.
An entirely unbound route is valid. When bound-to: is absent the boundTo
field is omitted from the AST and no binding validation runs at all — the
abstract route stands on its own as a design artifact. Binding is additive:
you author the route first, then bind it when (and only when) you want to project
it onto real boards.
The Flow ⊆ Path invariant — the one hard error
Every bound element must trace back to the path. Concretely, bound-to: may
only name things the abstract route already declares:
- The band in each
bound-to:entry must be declared inbands:. - Each pinned stage (
stages:refinement) must be a stage that actually lives in that band perpath:— i.e. its name must match a(itemType, band)stage inferred from the path’s edge endpoints (exact name first; a stage that became a wikilink also accepts its old lowercase spelling for one release).
A violation of either is route.flow-not-subset-path (rule r12) — the
one hard error in the binding family. It is AST-internal and pure:
deterministic math over the route’s own AST, needing no workspace lookup. It
is the structural guarantee that a binding can never bind a phantom band or a
stage the path never visits.
- A
bound-to:band not declared inbands:→ one error: “bound-to band '' is not declared in bands:” . - A stage refinement whose
itemTypeis not in that band perpath:→ one error: “stage '' is not in band ' .' per path:”
The rule fires only when the route is at least partially bound (bound-to:
present); a fully unbound route and an unbound-but-declared band stay silent.
Delivery-sink endpoints (()/⊙) are skipped when deriving the in-band stage set —
the delivery terminator is not a stage. Even on error, the preview still
renders — a diagnostic never blanks the board (T-23-03-AVAIL).
Stale board refs — a warning, never a blank
The Flow ⊆ Path rule is AST-internal. Resolving a board: locator against
the workspace is a separate axis handled by the external rule
xref/r4-bound-to-board-ref.ts. When a bound-to: board locator does not
resolve to a board in the workspace index, it surfaces
route.unresolved-board-ref — a warning, never an error. A single
missing board referenced under several bands yields exactly one warning (deduped
by locator).
This is deliberate graceful degradation: a stale or not-yet-created board ref is a normal state while authoring (you can bind to a board you have not written yet). It never blanks the preview — the route keeps rendering, and the author sees the last successfully-parsed board (note last-good render). The locator is treated as an opaque string — the parser performs no filesystem access; resolution happens in the host indexer.
Hiding the bindings
The bound-board badges on a band and the refinement pills on a stage form the
Boards layer (key board-bindings; the row was called “Board bindings” until
18.10.2026), the first row of the Filter menu. It is visible by default. On the
topology, units view and org chart the same row hides the board, route, topology and
flight-route markers of a box. Unchecking it hides the badges and the pills (and the binding rows in an
open drawer, which then shows the hidden-layer hint), so you can show a route first
without its boards and then switch them on: “this is how the stages are bound to
their boards”. Nothing on the route moves when the layer is hidden: a refined
stage keeps its reserved lower area. SVG and PNG exports and the CLI’s
--hide board-bindings prune the layer like every other one. The stage’s
Flight Item Type row is never hidden, and the board markers of the topology and
units views do not follow this layer. The layer key is board-bindings.
48.1 D-19 supersedes Phase 34 D-06 for route bindings: Phase 34 had exempted the
route’s bound-board badges from filtering; they are filterable now. The board
fl-badge and the child-count and unresolved badges stay unfiltered.
Worked example — a bound route
The binding.dwsd-flightroute corpus fixture — an abstract two-band route whose
Delivery band is bound to a concrete board, with the story stage pinned to a
lane’s column via the shared locator:
# binding — a `bound-to:` layer maps a route band to a concrete board via the shared locator; `for-domain:` names the domain responsible for the route. title: Bound Route for: feature for-domain: "[[Delivery Ops]]" bands: - name: Coordination fl: 2 - name: Delivery fl: 1 path: - epic @ Coordination --> story @ Delivery bound-to: - band: Delivery boards: - board: "[[team-alpha]]" stages: - story: "lane:Frontend/col:Doing"
The abstract path: copies an epic in Coordination into a story in
Delivery. bound-to: then realizes the Delivery band on the [[team-alpha]]
board, pinning the story stage to that board’s Frontend lane, Doing
column. Flow ⊆ Path holds: Delivery is a declared band, and story is a stage
that lives in Delivery per the path. Had the entry named a band not in bands:,
or pinned a task stage that the path never places in Delivery, it would be a
route.flow-not-subset-path error. Had [[team-alpha]] not existed in the
workspace, it would be a route.unresolved-board-ref warning — and the route
would still render.
for-domain names Delivery Ops as the domain responsible for the route; the
preview shows it as a chip next to the title, navigable when the workspace has
that work system or role.
See The locator grammar for the lane:Frontend/col:Doing
board-locator target syntax, and Path & bands for the path
elements a binding traces back to.
Like what you see? → Start — build your own org folder in ~2 min