Cross-Reference Management
Intro
A CrossReference is a lightweight typed link between two entities, expressed as a field in the subject's frontmatter rather than as its own file. It is the right choice for simple, unscoped, permanent relationships — "A blocks B", "decision X is related to work item Y". When a relationship needs scope, time, or its own attributes, use a Binding instead.
Overview
CrossReference is not a file
Unlike every other primitive, CrossReference does not get its own file. It is a convention for frontmatter fields — a typed list of pointers.
# In a WorkItem's frontmatter:
spec:
blocks: [BACK-swift-oak]
blocked_by: [BACK-calm-fox]
related_decisions: [DEC-023]
Each field name encodes the relationship type. The fields vary per primitive; the convention is shared.
Conventional cross-reference field names
| Field | Meaning |
|---|---|
parent |
This entity is a child of another |
children |
This entity has sub-items |
blocks |
This entity blocks others until resolved |
blocked_by |
This entity is blocked by others |
related |
Generic "see also" — use only when no better field fits |
related_workitems |
Typed relationship specifically to WorkItems |
related_decisions |
Typed relationship specifically to DecisionRecords |
supersedes |
This entity replaces an older one |
superseded_by |
This entity has been replaced by a newer one |
implements |
This entity implements something (e.g. a decision) |
motivates |
This entity motivated another (e.g. bug → decision) |
Workflow
- Pick the field that best fits the relationship. Prefer specific over generic.
- Add the target ID(s) to that field in the subject's frontmatter.
- For bidirectional relationships (
blocks↔blocked_by), update both sides. - Log a declared event type such as
workitem.notewithdetails.relation+details.target; do not use undeclared legacy aliases such asworkitem.linked.
Cross-reference vs Binding
| Situation | Use |
|---|---|
| "A blocks B" | cross-ref |
| "A is related to B" | cross-ref |
| "A is the parent of B" | cross-ref |
| "A fills role R in scope S for time T" | Binding |
| "A applies to B only on main branch" | Binding |
| "A and B are linked AND the link has its own metadata" | Binding |
If the link has its own properties worth querying, it's a Binding. Otherwise, it's a cross-reference.
Gotchas
Agent-specific failure modes — provider-neutral pause-and-self-check items:
- Using a cross-reference when scope or time matters. If the relationship is "Alice owns this for sprint 42" or "this gate applies on main only", that's a Binding, not a cross-reference. Cross-references can't carry temporal validity or scoping — reaching for one when you need a Binding silently loses the qualification.
- Forgetting to add the inverse side of an asymmetric pair.
blocks/blocked_by,parent/children,supersedes/superseded_byare all bidirectional. If you set one side without setting the other, the index ends up with a broken half-edge. Always set both. - Cross-referencing a non-existent entity ID. A typo in a
reference (
BACK-1234vsBACK-12345) silently breaks navigation and produces a dangling edge. Verify the target exists viaindex-management.get_entitybefore writing the reference. - Treating cross-references as authoritative state. A
cross-reference is metadata about a relationship; the
authoritative state lives on the entity itself. If a workitem's
parentsays it's part of an epic but the epic'schildrendoesn't include it, the parent reference is wrong, not the epic. - Cross-referencing aggressively just because you can. Every cross-reference is graph clutter for queries. Reserve them for relationships a future agent or human will actually need to navigate, not for "these two things vaguely relate".
- Adding cross-references to entities you don't own the schema
for. If you're tempted to put a custom field on a primitive's
frontmatter, consider whether it belongs in
metadata.tagsor in a Binding instead. Schema drift across primitives breaks validation. - Confusing cross-references with discussions/decisions. A cross-reference says "these are related"; it does not capture WHY. If the why matters, write a Discussion or DecisionRecord and cross-reference THAT, not the underlying entities directly.
Full reference
Bidirectional integrity
Some cross-references are inherently bidirectional (blocks/blocked_by,
parent/children, supersedes/superseded_by). Agents should update
both sides when adding or removing. The index MCP server (Phase 3) will
surface one-sided references as lint warnings.
Typed vs generic related
Prefer typed fields (related_decisions, related_workitems) over the
generic related — they make queries and rendering much cleaner. Only fall
back to related when there is no natural typed field.
Field naming convention
Cross-reference field names are lowercase snake_case, use plural form for
lists, and describe the relationship from the subject's perspective. "A
blocks B" is expressed as blocks: [B] on A, not blocker_of: [B].
Resolving a cross-reference
To find all entities that reference X:
- By field scan: grep all frontmatter for X's ID appearing in any list.
- Via index MCP server:
query(target=X)returns all cross-references across all entity kinds.
Migration to Binding
If a cross-reference grows attributes over time ("this block has a reason and an expiry date"), migrate to a Binding:
- Create a Binding with
type: blocking,subject: A,target: B, plus the new attributes. - Remove the cross-reference field from A.
- Log
workitem.link.migrated.
This is the only case where removing a cross-reference is routine; normally you keep them forever.