Relationships
{% relationships %} renders the relationship edges of one entity — what it implements, what blocks it, who it allies with — grouped by kind. It's the graph counterpart to collection: where collection selects a set of entities by a field:value query, relationships projects the edges touching a single entity, read from the registry's relationship graph.
It's generic over the edge kind, which is an arbitrary string. Plugins contribute edges with their own vocabulary — plan emits implements / blocked-by / depends-on, a storytelling plugin might emit ally / rival / mentor — and relationships groups and labels whatever it finds. There is one generic rune, not a per-domain *-relationships.
Selecting edges — of + kind
of is the entity to describe, passed explicitly as an id (or a bound entity). On a generated entity page you pass the page entity — the same $item.id you'd hand to expand:
{% relationships of=$item.id /%}
kind narrows to specific edge kinds (comma-separated); type restricts the related entity types:
{% relationships of=$item.id kind="blocks,blocked-by" type="work,bug" /%}
With no body, edges are grouped by kind, each rendered as a title link to the related entity — the zero-config built-in:
Implements → SPEC-001 Auth system
Blocked by → WORK-014 Token store
Related → ADR-003 Session strategy
Group headings are the kinds, humanized via the shared humanize function (blocked-by → "Blocked By") — no per-domain label table.
Per-item templates
Give relationships a body and it becomes the per-edge template, transformed once per edge with $item bound to the related entity (the same contract as collection — id/type/url/data) and $kind bound to the edge kind:
{% relationships of=$item.id kind="blocks,blocked-by" layout="grid" %}
{% card href=$item.url %}
{% humanize($kind) %}
### {% $item.data.title %}
Status: {% $item.data.status %}
{% /card %}
{% /relationships %}
Because $item means the same thing here as in a collection body, the same card partials are reusable across both runes — a work-card.md partial works whether it's fed by a collection query or a relationship edge.
Grouping and ordering
group— defaults tokind(a heading per edge kind). Usegroup="type"to group by the related entity's type, orgroup="none"for a flat list.group-display—headings(default) oraccordion, exactly as incollection:accordionrenders each group as a collapsible native<details>panel styled like theaccordionrune, collapsed, with the group's edge count beside its label.sort— a field on the related entity (-field/field-descfor descending). Enum fields honor the same domain-aware ordering ascollection.limit— a positive integer cap.layout—list(default, stacked) orgrid(responsive multi-column), mirroringcollection. Withgroup, the grid arranges each group's edges into columns (the group title spans the row); item chrome comes from the item — the no-body built-in becomes a card, or use a{% card %}in the body template.
Attributes
| Attribute | Type | Required | Description |
|---|---|---|---|
of | string | — | Entity to describe — an id (e.g. `of=$item.id`) or a bound entity. |
kind | string | — | Edge kinds to include, comma-separated (e.g. "blocks,blocked-by"). |
type | string | — | Restrict related entity types, comma-separated. |
group | string | — | Group-by: kind (default) | type | none. |
group-display | "headings" | "accordion" | — | How groups are presented: headings (default) or accordion (collapsible native <details> panels, styled like the accordion rune, with a per-group count). |
sort | string | — | Sort related entities by a field (prefix - for descending). |
limit | string | — | Max edges. |
fields | string | — | Comma-separated data fields to project in the no-body built-in. |
layout | string | — | Arrangement: list (stacked) | grid (multi-column). Item chrome comes from the item — the no-body built-in, or a rune like {% card %} in the body template. |
empty | string | — | Fallback text shown when there are no edges (no-body form; body form uses a fallback zone). Absent → render nothing. |
Universal attributes
bg
The background layer (SPEC-088): image, video, gradient, flat overlay wash and legibility scrim, in a single injected layer behind the rune's content.
| Attribute | Type | Required | Description |
|---|---|---|---|
bg | string | — | Background preset applied to this block |
bg-from | string | — | Gradient start colour — a semantic token name (→ var(--rf-color-*)) |
bg-gradient | "to-t" | "to-b" | "to-l" | "to-r" | "to-tr" | "to-br" | "to-bl" | "to-tl" | — | Gradient direction (bounded named set) |
bg-gradient-type | "linear" | "radial" | "conic" | — | Gradient type |
bg-to | string | — | Gradient end colour — a semantic token name |
bg-via | string | — | Optional middle gradient stop — a semantic token name |
scrim | "top" | "bottom" | "left" | "right" | "none" | — | Scrim direction (heaviest edge); presence turns the scrim on, "none" opts out of the default cover scrim |
scrim-blur | "none" | "sm" | "md" | "lg" | — | Frost scrim blur amount |
scrim-strength | "sm" | "md" | "lg" | — | Gradient scrim strength |
scrim-tone | "dark" | "light" | — | Whether the scrim darkens (for light text) or lightens (for dark text) |
scrim-type | "gradient" | "frost" | — | Scrim treatment: gradient (default) or frost (backdrop blur) |
elevation
The chrome/depth ladder (SPEC-107). The skin maps each rung to a chrome bundle by attribute, so there is no BEM class.
| Attribute | Type | Required | Description |
|---|---|---|---|
elevation | "sunken" | "flush" | "flat" | "raised" | "floating" | "overlay" | "none" | "sm" | "md" | "lg" | — | Surface depth on the SPEC-107 ladder (sunken→overlay); none/sm/md/lg are deprecated aliases |
inset
Internal padding override.
| Attribute | Type | Required | Description |
|---|---|---|---|
inset | "flush" | "tight" | "default" | "loose" | "breathe" | — | Inner padding of this block |
motion
Scroll-reveal entrance (SPEC-105). The author declares the character, the theme owns the choreography, a behaviour owns the timing.
| Attribute | Type | Required | Description |
|---|---|---|---|
reveal | "none" | "fade" | "slide" | "scale" | "blur" | — | Scroll-reveal entrance character (none|fade|slide|scale|blur); the theme owns the choreography |
stagger | boolean | — | Cascade this block's items in as it reveals (no-op on single-child runes) |
spacing
Block-level rhythm override.
| Attribute | Type | Required | Description |
|---|---|---|---|
spacing | "flush" | "tight" | "default" | "loose" | "breathe" | — | Vertical spacing above and below this block |
substrate
Generated pattern fills (SPEC-087). Markers only — the engine sets the attributes and cell/opacity custom properties, CSS draws the pattern.
| Attribute | Type | Required | Description |
|---|---|---|---|
substrate | "dots" | "grid" | "lines" | "cross" | "checker" | "none" | — | Generated surface pattern |
substrate-fill | "inherit" | "inset" | — | Surface fill the pattern sits on (full colour stays with tint) |
substrate-opacity | "sm" | "md" | "lg" | — | Pattern ink strength |
substrate-size | "sm" | "md" | "lg" | — | Pattern cell size |
substrate-target | "self" | "media" | — | Which surface the pattern fills (overrides the rune/theme default) |
tint
Per-rune colour override (SPEC-053): a named tint from the theme registry, with inline per-token overrides layered on top.
| Attribute | Type | Required | Description |
|---|---|---|---|
tint | string | — | Color tint preset applied to this block |
tint-mode | "auto" | "dark" | "light" | — | Whether the tint adapts to auto, dark, or light mode |
width
The track a block rune occupies.
| Attribute | Type | Required | Description |
|---|---|---|---|
width | "compact" | "narrow" | "content" | "wide" | "full" | — | Maximum width constraint for this block |
| Not available | Why |
|---|---|
| dropcap, reading | this rune declares no prose body |
| frame | this rune declares neither a `frameTarget` nor a media section |
| prominence | this rune has no page-section header |
Empty state and body zones
Like collection, relationships supports an empty state and ----delimited body zones. The self-closing form takes an empty fallback:
{% relationships of=$item.id empty="No relationships yet." /%}
With a body, split on a top-level --- into preamble (rendered above the edges only when there are any), template (the per-edge template), and fallback (shown when there are none) — so a "Relationships" heading can live inside the rune and disappear when the entity has no edges. The preamble and fallback can read $count (edges matched, pre-limit) and $shown (edges rendered, post-limit), just as in collection.
Output contract
<section class="rf-relationships" data-rune="relationships" data-of="WORK-1" data-layout="list">
<div class="rf-relationships__items">
<div class="rf-relationships__group" data-group="implements">
<h3 class="rf-relationships__group-title">Implements</h3>
<div class="rf-relationships__item" data-entity-id="SPEC-1" data-kind="implements">
<a class="rf-relationships__title" href="/specs/SPEC-1/">Auth system</a>
</div>
</div>
<!-- …or, with a body template, each item is the template's own output -->
</div>
</section>
In layout="grid" the no-body built-in is an article.rf-relationships__card (instead of the inline __item row), and each group is itself the grid so its cards flow into columns under a full-width group title.
Where edges come from
relationships only renders the graph — it doesn't build it. Plugins contribute edges during the registry's aggregate phase, so the kinds you see depend on which plugins are installed. See plugin authoring → pipeline for the relate() / getRelated() contract.
See also
- collection — the field-query counterpart; shares
$item, layout, grouping, and ordering. - ref / expand — the singular counterparts (one entity → a link / inlined content).
- card — the generic content card you feed with
$itemin a body template.