DWSD.md

Board DSL — Anchors, Agreements & Interactions

This page covers how a board reaches outward (agreements: / interactions:) and how external entities point into board positions via a locator, the unified render surface (grouped buckets + shared drawer), inline markdown-lite, and the overflow affordances. See the reference index for the board header and the other constructs.

Anchors, agreements & interactions

A board node reaches outward (agreements: / interactions: declared on the node — the inside-out direction), and external entities (meetings, agreements) point into board positions via a locator (the outside-in direction). Both directions resolve through one shared surface. For the mental model see Inside-Out / Outside-In; for the authoritative locator syntax (including how it works — the four-part shape and the exact-one resolution walk) see the Locator grammar (this section does not re-derive it — it summarizes how the board renders the result).

The locator (outside-in), in brief

An external entity addresses a board position with a locator string:

locator := head ('#' boardId)? ('/' path)? ('::' position)?
  • head — [[file]] (canonical, resolved by lowercased basename). An optional #boardId suffix selects a board by its title when one Markdown file hosts multiple board blocks (D-14/D-15 — e.g. [[Sprint Board]]#Sales picks the board titled Sales); omit the head entirely for a relative locator that inherits its container’s frontmatter head.
  • typed path — a /-chained set of kind:Name segments, case-insensitive: col:Name, lane:Name, group:Name, split:Name (e.g. lane:Alice/col:Doing, col:Build/split:QA). The resolver returns exactly one node (or a synthetic lane×column cell), or degrades to not-found / ambiguous — no wildcards.
  • position — a trailing ::before / ::in / ::after; bare = ::in.

See the Board Locator grammar for head forms, the value-polymorphic interaction-of: / agreement-of: keys, the for-domain: role-scope key, the container body-table convention (one entity file → N anchors), and the ## Outcome(s) channel (a per-anchor outcome line in the drawer, allowlisted to ## Outcome / ## Outcomes / ## What is the outcome?, truncated to 280 chars).

The unified render surface — buckets, drawer & disclosure tiers

Per board unit, internal declarations + external markers merge into at most two grouped buckets per side plus an optional fallback marker. A unit that is anchored only in (the common case) therefore shows at most two buckets; anchors on other sides add a set of their own (see “One bucket set per side” below):

BucketSourceChip
.anchor-bucket[data-bucket="agreements"]internal agreements: + external type: agreementAgreements (N)
.anchor-bucket[data-bucket="interactions"]internal interactions: + external type: interaction or type: meeting (a meeting is an interaction)Interactions (N)
.anchor-marker (⚓)an external entity with no recognized typegraceful fallback
  • Disclosure tiers (deterministic, no live measurement). Tier 1 names shows inline per-entity labels when count ≤ 2 AND total name length ≤ 24; otherwise Tier 2 collapsed shows a single Agreements (N) / Interactions (N) count. N = 1 always shows the (truncated) label, never a count.
  • Wikilink entries show their name. An internal entry in the quoted wikilink form ("[[ART Sync]]") shows ART Sync on the chip and in the drawer, never the brackets. The name, not the bracketed string, counts toward the 24-character budget.
  • One entity, one entry. When the same entity is both declared internally ("[[ART Sync]]") and anchored at the same unit from outside (the ART Sync document’s locator), the chip counts it once and the drawer lists it once — the external entry is kept, because it carries the link to the source. The match is case-insensitive, on the display name or the link target, within one bucket.
  • Names never wrap. A chip is one line: a name that does not fit ends in …. In a group bar the group name keeps its line; a crowded bar truncates its chips instead.
  • Names read in the chip’s own color. A chip shows its names in the chip’s own color, never as blue link text. The link to an entity’s file is a row in the drawer: click the chip to open it.
  • Shared drawer. Clicking a chip walks to the nearest anchor marker and opens one shared drawer listing every related entity (with its container, outcome, and nav target). The bucket carries a deterministic data-anchor-key per unit (column#name, lane#name, group#name, split#col#idx, cell#lane#col, board) so the drawer routes correctly.
  • One bucket set per side. A unit anchored before, in and after shows all three sets, not only the last one written. Internal entries (declared on the unit itself) have no side and join the in set; an external in anchor still blends into that one in bucket, while an external before or after anchor adds its own agreements / interactions set. The before set sits at the unit’s start edge (the left edge of a column, lane header, group bar, split section or cell), the after set at its end edge (the right edge); the in set stays where it always was. Every bucket carries data-anchor-side.
  • Board-scope anchors show in the board header. An external anchor whose locator addresses the whole board ([[Order Studio Board]]#Order Studio::in: a #Title and no path) shows in the board header as the board’s one agreements bucket (plus one interactions bucket), merged with the board’s own internal entries and with the sides merged — the header has no edges, so every side appears in a single set tagged in. An entity declared both internally and at board scope counts once. A board with such an anchor always gets a header, also an unnamed one (it shows the bucket, no invented title).
  • Board-scope pins show in the header too. A navigation pin (observes: and its family) whose locator addresses the whole board shows in the change pill in the board header, keyed board. Every board pin keeps its side as data-anchor-side; pins do not move by side, and pins of different sides at one place are one pill.
  • One grouped chip per scope. At every scope — board, column, lane, group, and split section — agreements: (and interactions:) render as a single grouped chip → the shared drawer, never as separate per-entry chips.

Mandates on a position

A mandate whose Mandate of cell (a row on a holder’s file) or mandate-of: (a type: mandate file) names a board position shows there as one mandate chip with a key glyph, in its own bucket .anchor-bucket[data-bucket="mandates"] (data-layer="mandates", addressable as chip:mandate). The chip is labeled with the holder, not with the mandate text. Several holders at one place share the bucket and list their names; when the names do not fit it reads Mandates (N). Clicking the chip opens the shared drawer, which lists each holder with the mandate text beside a key glyph. The chip is Terracotta (#C9956B border, #F6ECE2 fill, #6B4423 text), the same hue as the border of a note, and the Mandates layer of the filter menu hides it. Mandates come from outside the board only; a board has no mandates: key. A row without a Mandate of locator draws nothing. See Mandates. Colors and the own layer are the current defaults, not settled.

Graceful degradation

The anchor surface never crashes and never blanks the board. An unresolved locator (missing file, wrong board id, no matching column) renders no marker; the rest of the board renders normally. An unresolved internal [[wikilink]] decorates with .wikilink-unresolved + a !N badge. The parser performs no filesystem access — resolution against the workspace happens in the host indexer.

Beyond agreements/interactions, a board position is anchored by a signal or an insight, two of the four in-flux change entities (signal, insight, driver, proposal; see the vendored in-flux types reference for the full type model, and the change category page for where they sit in the chain). “Navigation pin” is the name of the data (BoardPin) and of the old glyph, kept because it is what the code calls it; the category is change. These are pins, not markers: all pins of one place show up as ONE change pill, a quiet capsule in the header’s own colors at the right end of the header of the column, lane, group bar, split section or cell (and of the board header for a board-scope pin), distinct from the agreement/interaction bucket chips. The pill is a level indicator: four small dots (signal, insight, driver, proposal), filled in the header’s name color up to the highest stage that has an entry and faint above it, then the entry count; its outline is the column border. At a board position only a signal or insight is placed (format 1.3), so for now the driver and proposal dots stay empty there. It has no type colors. Hovering it shows the card “Change story here · n entries” with one grey circle and count per stage; clicking it opens the drawer. See Where the pins render.

  • Anchoring is outside-in via the same locator. A signal / insight uses its observes: field. A driver / proposal is not placed on a board position by observes: it names its domain with for-domain: (a unit or work system) and shows on that domain’s box in the structure views. The observes: value is the same value-polymorphic locator the interaction-of: / agreement-of: keys use — a board-position value (#BoardName and/or a /col:…/lane:…/group:…/split:… path, optional ::before|in|after, bare = ::in) anchors the pin to that node.

    ---
    type: signal
    observes: "[[Sprint Board]]#Sprint Board/col:Build::in"
    ---
  • The four categories share ONE drawer super-group. Clicking the pill opens the drawer; the four pin categories render under a single contiguous “Change conversation” band in stage order (each a per-type sub-row with a (count) badge), ADD-ALONGSIDE — not replacing — the flat Agreements / Interactions / AI-Agents buckets. The band can be folded into a long pill and back, and hovering a row fades the rows outside its derived-from net. One click on a row opens the entity; the drawer has no action that opens a Change Trail. Per-type body richness (insight Category/Severity, proposal status, the driver four-part body) surfaces as drawer sub-row content, not as separate markers.

  • derived-from is metadata only — it links down the chain but is not a typed edge and paints no pin (the drawer reads it to fade rows outside a hovered row’s net). It is the shared emergence key, not a field of these four types.

  • Graceful degradation. An unresolved pin locator renders no pin; the rest of the board renders normally (the shared never blank, never break contract).

  • A change entity does not belong in agreements:. A signal, insight, driver, proposal or organizational decision record listed in a board’s (or column’s, lane’s, group’s) agreements: shows a warning, board.change-entity-in-agreements, on the wikilink: a change entity points at the board from outside and does not become part of the work system. A signal or insight does that with an observes locator on the entity; a driver, proposal or decision record names its domain with for-domain. The entry still renders as a row of the agreements bucket. The editor shows the warning with the workspace index; dwsd-dsl check lists it as skipped.

Inline markdown-lite

Agreement / interaction text supports a small, non-nesting inline markdown subset (first match wins, rendered without innerHTML):

  • **bold**, *italic*, `code`
  • [[wikilink]] → a .wikilink to the target entity
  • [text](url) → a .md-link

No other markdown is interpreted, and there is no nesting.

Overflow & graceful degradation affordances

The renderer follows “show as much as possible, never blank, never break.”

  • tags: chips render single-line with a … ellipsis when they overflow.
  • Anchor chips collapse to a Agreements (N) / Interactions (N) count tier; the full names live in the chip title and the drawer.
  • Outcome text truncates to 280 chars + a single ….

Example

A focused board exercising the inside-out direction — board-level agreements: (a [[wikilink]] linked entity + an inline agreement string) and interactions: (a [[wikilink]] meeting pointer), plus a column-scoped agreements: entry. Each renders as a single grouped bucket chip → the shared drawer, never per-entry chips:

title: Sprint Board
agreements:
  - "[[Team Working Agreements]]"
  - "WIP per column: 3"
interactions:
  - "[[Daily Standup]]"
columns:
  - ToDo
  - name: Doing
    agreements:
      - "Pull only when capacity is free"
  - Done

Like what you see? → Start — build your own org folder in ~2 min

anchors.md
# Board DSL — Anchors, Agreements & Interactions
This page covers how a board reaches outward (`agreements:` / `interactions:`)
and how external entities point into board positions via a locator, the unified
render surface (grouped buckets + shared drawer), inline markdown-lite, and the
overflow affordances. See the [reference index](./index.md) for the board header
and the other constructs.
## Anchors, agreements & interactions
A board node reaches *outward* (`agreements:` / `interactions:` declared on the
node — the **inside-out** direction), and external entities (meetings,
agreements) point *into* board positions via a **locator** (the **outside-in**
direction). Both directions resolve through one shared surface. For the mental
model see [Inside-Out / Outside-In](../../concepts/inside-out-outside-in.md); for
the authoritative locator syntax (including how it works — the four-part shape
and the exact-one resolution walk) see the
[Locator grammar](../locator-grammar.md) (this section does **not**
re-derive it — it summarizes how the board renders the result).
### The locator (outside-in), in brief
An external entity addresses a board position with a locator string:
```
locator := head ('#' boardId)? ('/' path)? ('::' position)?
```
- **head** — `[[file]]` (canonical, resolved by lowercased basename). An optional
`#boardId` suffix selects a board by its **title** when one Markdown file hosts
multiple board blocks (D-14/D-15 — e.g. `[[Sprint Board]]#Sales` picks the board
titled Sales); omit the head entirely for a **relative** locator that inherits
its container's frontmatter head.
- **typed path** — a `/`-chained set of `kind:Name` segments, case-insensitive:
`col:Name`, `lane:Name`, `group:Name`, `split:Name` (e.g.
`lane:Alice/col:Doing`, `col:Build/split:QA`). The resolver returns **exactly
one** node (or a synthetic lane×column `cell`), or degrades to `not-found` /
`ambiguous` — no wildcards.
- **position** — a trailing `::before` / `::in` / `::after`; bare = `::in`.
See the [Board Locator grammar](../locator-grammar.md) for head forms, the
value-polymorphic `interaction-of:` / `agreement-of:` keys, the `for-domain:`
role-scope key, the **container body-table** convention (one entity file → N
anchors), and the **`## Outcome(s)` channel** (a per-anchor outcome line in the
drawer, allowlisted to `## Outcome` / `## Outcomes` / `## What is the outcome?`,
truncated to 280 chars).
### The unified render surface — buckets, drawer & disclosure tiers
Per board unit, internal declarations + external markers merge into **at most two
grouped buckets per side** plus an optional fallback marker. A unit that is anchored
only `in` (the common case) therefore shows at most two buckets; anchors on other
sides add a set of their own (see "One bucket set per side" below):
| Bucket | Source | Chip |
|--------|--------|------|
| `.anchor-bucket[data-bucket="agreements"]` | internal `agreements:` + external `type: agreement` | `Agreements (N)` |
| `.anchor-bucket[data-bucket="interactions"]` | internal `interactions:` + external `type: interaction` **or** `type: meeting` (a meeting *is* an interaction) | `Interactions (N)` |
| `.anchor-marker` (`⚓`) | an external entity with no recognized type | graceful fallback |
- **Disclosure tiers (deterministic, no live measurement).** Tier 1 `names` shows
inline per-entity labels when `count ≤ 2 AND total name length ≤ 24`; otherwise
Tier 2 `collapsed` shows a single `Agreements (N)` / `Interactions (N)` count.
`N = 1` always shows the (truncated) label, never a count.
- **Wikilink entries show their name.** An internal entry in the quoted wikilink form
(`"[[ART Sync]]"`) shows `ART Sync` on the chip and in the drawer, never the
brackets. The name, not the bracketed string, counts toward the 24-character budget.
- **One entity, one entry.** When the same entity is both declared internally
(`"[[ART Sync]]"`) and anchored at the same unit from outside (the `ART Sync`
document's locator), the chip counts it once and the drawer lists it once — the
external entry is kept, because it carries the link to the source. The match is
case-insensitive, on the display name or the link target, within one bucket.
- **Names never wrap.** A chip is one line: a name that does not fit ends in `…`.
In a group bar the group name keeps its line; a crowded bar truncates its chips
instead.
- **Names read in the chip's own color.** A chip shows its names in the chip's
own color, never as blue link text. The link to an entity's file is a row in the
drawer: click the chip to open it.
- **Shared drawer.** Clicking a chip walks to the nearest anchor marker and opens
**one shared drawer** listing every related entity (with its container, outcome,
and nav target). The bucket carries a deterministic `data-anchor-key` per unit
(`column#name`, `lane#name`, `group#name`, `split#col#idx`, `cell#lane#col`,
`board`) so the drawer routes correctly.
- **One bucket set per side.** A unit anchored *before*, *in* and *after* shows all
three sets, not only the last one written. Internal entries (declared on the unit
itself) have no side and join the `in` set; an external `in` anchor still blends
into that **one** `in` bucket, while an external `before` or `after` anchor adds its
own agreements / interactions set. The `before` set sits at the unit's start edge
(the left edge of a column, lane header, group bar, split section or cell), the
`after` set at its end edge (the right edge); the `in` set stays where it always
was. Every bucket carries `data-anchor-side`.
- **Board-scope anchors show in the board header.** An external anchor whose locator
addresses the whole board (`[[Order Studio Board]]#Order Studio::in`: a `#Title`
and no path) shows in the **board header** as the board's one agreements bucket
(plus one interactions bucket), merged with the board's own internal entries and
with the sides merged — the header has no edges, so every side appears in a single
set tagged `in`. An entity declared both internally and at board scope counts
once. A board with such an anchor always gets a header, also an unnamed one (it
shows the bucket, no invented title).
- **Board-scope pins show in the header too.** A navigation pin (`observes:` and its
family) whose locator addresses the whole board shows in the **change pill** in the
board header, keyed `board`. Every board pin keeps its side as
`data-anchor-side`; pins do not move by side, and pins of different sides at one
place are one pill.
- **One grouped chip per scope.** At every scope — board, column, lane, group, and
split section — `agreements:` (and `interactions:`) render as a single grouped
chip → the shared drawer, never as separate per-entry chips.
![Agreements bucket + drawer](../../images/agreements.png)
![Interactions bucket + drawer](../../images/interactions.png)
![Outside-in anchor markers](../../images/anchors.png)
### Mandates on a position
A mandate whose `Mandate of` cell (a row on a holder's file) or `mandate-of:` (a `type: mandate` file) names
a board position shows there as **one mandate chip** with a key glyph, in its own bucket
`.anchor-bucket[data-bucket="mandates"]` (`data-layer="mandates"`, addressable as `chip:mandate`). The chip is
labeled with the **holder**, not with the mandate text. Several holders at one place share the bucket and list their names;
when the names do not fit it reads `Mandates (N)`. Clicking the chip opens the shared drawer, which lists each holder
with the mandate text beside a key glyph. The chip is Terracotta (`#C9956B` border, `#F6ECE2` fill, `#6B4423` text), the
same hue as the border of a note, and the **Mandates** layer of the filter menu hides it. Mandates come from outside
the board only; a board has no `mandates:` key. A row without a `Mandate of` locator draws nothing. See [Mandates](../mandates.md).
Colors and the own layer are the current defaults, not settled.
### Graceful degradation
The anchor surface **never crashes and never blanks the board**. An unresolved
locator (missing file, wrong board id, no matching column) renders **no marker**;
the rest of the board renders normally. An unresolved internal `[[wikilink]]`
decorates with `.wikilink-unresolved` + a `!N` badge. The parser performs **no
filesystem access** — resolution against the workspace happens in the host
indexer.
## Navigation pins (signals, insights, drivers & proposals)
Beyond agreements/interactions, a board position is anchored by a `signal` or an `insight`,
two of the four **in-flux change entities** (`signal`, `insight`, `driver`, `proposal`; see the
vendored [in-flux types reference](../change/in-flux.md) for the full type model, and
the [change category page](../change/README.md) for where they sit in the chain).
"Navigation pin" is the name of the data (`BoardPin`) and of the old glyph, kept because
it is what the code calls it; the *category* is `change`. These are **pins**, not
markers: all pins of one place show up as ONE **change pill**, a quiet capsule in the header's own colors at
the right end of the header of the column, lane, group bar, split section or cell (and
of the board header for a board-scope pin), distinct from the agreement/interaction
bucket chips. The pill is a level indicator: four small dots (signal, insight, driver,
proposal), filled in the header's name color up to the highest stage that has an entry and
faint above it, then the entry count; its outline is the column border. At a board position
only a signal or insight is placed (format 1.3), so for now the driver and proposal dots stay empty there. It has no type colors. Hovering it shows the card "Change story here · n entries"
with one grey circle and count per stage; clicking it opens the drawer. See
[Where the pins render](../change/in-flux.md#where-the-pins-render).
- **Anchoring is outside-in via the same locator.** A `signal` / `insight` uses its
`observes:` field. A `driver` / `proposal` is not placed on a board position by
`observes`: it names its domain with `for-domain:` (a unit or work system) and shows on
that domain's box in the structure views. The `observes:` value is
the **same value-polymorphic locator** the `interaction-of:` / `agreement-of:` keys
use — a board-position value (`#BoardName` and/or a `/col:…/lane:…/group:…/split:…`
path, optional `::before|in|after`, bare = `::in`) anchors the pin to that node.
```yaml
---
type: signal
observes: "[[Sprint Board]]#Sprint Board/col:Build::in"
---
```
- **The four categories share ONE drawer super-group.** Clicking the pill opens the
drawer; the four pin categories render under a single contiguous **"Change
conversation"** band in stage order (each a per-type sub-row with a `(count)`
badge), ADD-ALONGSIDE — not replacing — the flat Agreements / Interactions /
AI-Agents buckets. The band can be folded into a long pill and back, and hovering a
row fades the rows outside its `derived-from` net. One click on a row opens the
entity; the drawer has no action that opens a Change Trail. Per-type body richness
(insight Category/Severity, proposal status, the driver four-part body) surfaces as
drawer sub-row content, not as separate markers.
- **`derived-from` is metadata only** — it links down the chain but is not a typed
edge and paints no pin (the drawer reads it to fade rows outside a hovered row's net). It is the
[shared emergence key](../change/README.md#derived-from--the-shared-emergence-key),
not a field of these four types.
- **Graceful degradation.** An unresolved pin locator renders **no pin**; the
rest of the board renders normally (the shared *never blank, never break* contract).
- **A change entity does not belong in `agreements:`.** A signal, insight,
driver, proposal or organizational decision record listed in a board's (or column's,
lane's, group's) `agreements:` shows a warning, `board.change-entity-in-agreements`,
on the wikilink: a change entity points at the board from outside and does not become
part of the work system. A signal or insight does that with an `observes` locator on the
entity; a driver, proposal or decision record names its domain with `for-domain`. The
entry still renders as a row of the agreements bucket. The editor shows the warning
with the workspace index; `dwsd-dsl check` lists it as skipped.
## Inline markdown-lite
Agreement / interaction text supports a small, **non-nesting** inline markdown
subset (first match wins, rendered without `innerHTML`):
- `**bold**`, `*italic*`, `` `code` ``
- `[[wikilink]]` → a `.wikilink` to the target entity
- `[text](url)` → a `.md-link`
No other markdown is interpreted, and there is no nesting.
## Overflow & graceful degradation affordances
The renderer follows *"show as much as possible, never blank, never break."*
- **`tags:` chips** render single-line with a `…` ellipsis when they overflow.
- **Anchor chips** collapse to a `Agreements (N)` / `Interactions (N)` count tier;
the full names live in the chip `title` and the drawer.
- **Outcome text** truncates to 280 chars + a single `…`.
## Example
A focused board exercising the inside-out direction — board-level `agreements:`
(a `[[wikilink]]` linked entity + an inline agreement string) and `interactions:`
(a `[[wikilink]]` meeting pointer), plus a column-scoped `agreements:` entry. Each
renders as a single grouped bucket chip → the shared drawer, never per-entry chips:
```dwsd-board
title: Sprint Board
agreements:
- "[[Team Working Agreements]]"
- "WIP per column: 3"
interactions:
- "[[Daily Standup]]"
columns:
- ToDo
- name: Doing
agreements:
- "Pull only when capacity is free"
- Done
```