RegistryAggregate

Aggregate

{% aggregate %} queries the entity registry and projects numbers — counts (and, in time, sums / averages) over the same field:value grammar collection and relationships use. It's the third sibling in that family:

  • collection projects items — entities.
  • relationships projects edges — graph links.
  • aggregate projects numbers — counts and per-group breakdowns.

Two modes from one rune. With no body it's a single inline integer ({% aggregate type="work" filter="status:done" /%} renders one number in prose). With a body, it iterates groups and binds a per-group projection to $item — same body-zone convention as collection, with the iteration moved from entities to groups.

Because aggregate resolves in post-process — like collection and relationships — it sees the full cross-page registry. It works on file-backed pages and dynamic entity routes alike, and on every page in the site, not only the route a particular plugin generates.

Single-number form

The self-closing form emits one integer: the count of entities matching the query.

Of {% aggregate type="work" /%} work items, {% aggregate type="work" filter="status:done" /%} are done.
<p>
  Of
  <span data-rune="aggregate">
    <meta data-field="aggregate-type" content="work">
    <meta data-field="aggregate-filter" content="">
    <meta data-field="aggregate-value" content="">
    <meta data-field="aggregate-group" content="">
    <meta data-field="aggregate-sort" content="">
    <meta data-field="aggregate-limit" content="">
    <meta data-field="aggregate-empty" content="">
    <meta data-field="aggregate-layout" content="">
    <meta data-field="aggregate-chart-type" content="bar">
    <meta data-field="aggregate-chart-title" content="">
    <meta data-field="__aggregate-sentinel" content="true">
  </span>
  work items,
  <span data-rune="aggregate">
    <meta data-field="aggregate-type" content="work">
    <meta data-field="aggregate-filter" content="status:done">
    <meta data-field="aggregate-value" content="">
    <meta data-field="aggregate-group" content="">
    <meta data-field="aggregate-sort" content="">
    <meta data-field="aggregate-limit" content="">
    <meta data-field="aggregate-empty" content="">
    <meta data-field="aggregate-layout" content="">
    <meta data-field="aggregate-chart-type" content="bar">
    <meta data-field="aggregate-chart-title" content="">
    <meta data-field="__aggregate-sentinel" content="true">
  </span>
  are done.
</p>

Of 555 work items, 499 are done.

<p>
  Of 
  <span class="rf-aggregate" data-aggregate="count" data-count="555" data-rune="aggregate" data-density="full">555</span>
   work items, 
  <span class="rf-aggregate" data-aggregate="count" data-count="499" data-rune="aggregate" data-density="full">499</span>
   are done.
</p>

Same type + filter attributes as collection (and the same field-match grammar). Multiple types compose via comma:

{% aggregate type="work,bug" filter="status:done" /%}

The output is a <span class="rf-aggregate" data-aggregate="count">N</span> — inline-safe in prose.

Body-zoned form

Give aggregate a body and it iterates — but unlike collection, the iteration runs over groups, not entities. The body splits on a top-level --- into up to three zones (same convention as card and collection), and $item binds differently in each:

ZoneWhen rendered$item
preambleonce, above the breakdown, only when the query is non-emptytotals projection — { count, value, percent, total }
templateonce per group — the rune's main outputper-group projection — { key, count, value, percent, total, shown }
fallbackonce, in place of the breakdown, only when the query is emptyall zeros

A canonical body — totals progress bar above, one badge per status below, fallback when empty:

{% aggregate type="work" value="status:done" group="status" %}
{% progress value=$item.value max=$item.count %}Progress{% /progress %}
---
{% badge data-status=$item.key %}{% $item.count %} of {% $item.total %} {% humanize($item.key) %}{% /badge %}
---
Nothing to report.
{% /aggregate %}

The zone positioning is the same as collection: 1 zone → template, 2 → preamble + template, 3 → preamble + template + fallback. A --- inside a nested rune isn't a delimiter — only top-level rules split.

The $item projection

$item is a read-only bound variable in every zone. The keys depend on the zone:

FieldPreambleTemplateFallbackMeaning
count0entities in this zone's context (preamble: the whole primary set; template: this group)
value0entities matching both filter and value (see below); falls back to count when value is unset
percent0(value / count) × 100, integer 0–100; 100 when value is unset
total0the all-groups total — equal to count in the preamble, constant across template iterations
key''the group field value (template only)
shown0post-limit group count (template only)

The two count-shaped fields read differently:

  • count is the in-context denominator — the entire primary set in the preamble, the current group's count in the template. It's the natural max for a progress bar.
  • total is the all-groups constant — the same number in every template iteration. Useful for share-of-total ratios in a per-group template ("3 of 12"), without a second query.

In the preamble there's no in-context vs global distinction (you're not iterating), so count === total in that zone.

The value sub-filter — driving progress ratios

The value attribute is a secondary field:value clause within the primary set defined by filter — the "achieved" subset that powers a progress-bar ratio without a second query.

{% aggregate type="work" value="status:done" %}
{% progress value=$item.value max=$item.count %}Done so far{% /progress %}
{% /aggregate %}
<section data-rune="aggregate">
  <meta data-field="aggregate-type" content="work">
  <meta data-field="aggregate-filter" content="">
  <meta data-field="aggregate-value" content="status:done">
  <meta data-field="aggregate-group" content="">
  <meta data-field="aggregate-sort" content="">
  <meta data-field="aggregate-limit" content="">
  <meta data-field="aggregate-empty" content="">
  <meta data-field="aggregate-layout" content="">
  <meta data-field="aggregate-chart-type" content="bar">
  <meta data-field="aggregate-chart-title" content="">
  <meta data-field="__aggregate-sentinel" content="true">
  <meta data-field="aggregate-body" content="{% progress value=$item.value max=$item.count %}Done so far{% /progress %}
">
</section>

Done so far499/555

<section class="rf-aggregate" data-aggregate="breakdown" data-rune="aggregate" data-density="full">
  <div data-name="items" class="rf-aggregate__items rf-aggregate__items">
    <p>
      <div class="rf-progress" role="progressbar" aria-valuemin="0" aria-valuenow="499" aria-valuemax="555" aria-label="Done so far" style="--rf-progress: 90%" data-rune="progress" data-density="full">
        <span data-name="label" class="rf-progress__label">Done so far</span>
        <span data-name="value" class="rf-progress__value">499/555</span>
        <span data-name="track" class="rf-progress__track">
          <span data-name="fill" class="rf-progress__fill"></span>
        </span>
      </div>
    </p>
  </div>
</section>

The primary set here is "every work entity"; the achieved subset is "those whose status is done". The progress bar reads $item.value (achieved) over $item.count (primary), with $item.percent available as the rounded integer 0–100.

Without a value attribute the rune is a pure count + breakdown — no progress semantics — and $item.value falls back to count (so a template using $item.value keeps working; $item.percent reads 100).

Grouping — group

Set group and the template is rendered once per distinct value of that field, with the group projection on $item:

{% aggregate type="work" value="status:done" group="status" %}
---
{% badge data-status=$item.key %}{% $item.count %} {% humanize($item.key) %}{% /badge %}
{% /aggregate %}
<section data-rune="aggregate">
  <meta data-field="aggregate-type" content="work">
  <meta data-field="aggregate-filter" content="">
  <meta data-field="aggregate-value" content="status:done">
  <meta data-field="aggregate-group" content="status">
  <meta data-field="aggregate-sort" content="">
  <meta data-field="aggregate-limit" content="">
  <meta data-field="aggregate-empty" content="">
  <meta data-field="aggregate-layout" content="">
  <meta data-field="aggregate-chart-type" content="bar">
  <meta data-field="aggregate-chart-title" content="">
  <meta data-field="__aggregate-sentinel" content="true">
  <meta data-field="aggregate-body" content="---

{% badge data-status=$item.key %}{% $item.count %} {% humanize($item.key) %}{% /badge %}
">
</section>

1 Blocked

4 In Progress

3 Review

27 Ready

14 Pending

7 Draft

499 Done

<section class="rf-aggregate" data-aggregate="breakdown" data-rune="aggregate" data-density="full">
  <div data-name="items" class="rf-aggregate__items rf-aggregate__items">
    <div class="rf-aggregate__group" data-group="blocked" data-block="">
      <p>
        <span class="rf-badge rf-badge" data-meta-sentiment="neutral" data-meta-type="tag" data-rune="badge" data-density="full">
          1
           
          Blocked
        </span>
      </p>
    </div>
    <div class="rf-aggregate__group" data-group="in-progress" data-block="">
      <p>
        <span class="rf-badge rf-badge" data-meta-sentiment="neutral" data-meta-type="tag" data-rune="badge" data-density="full">
          4
           
          In Progress
        </span>
      </p>
    </div>
    <div class="rf-aggregate__group" data-group="review" data-block="">
      <p>
        <span class="rf-badge rf-badge" data-meta-sentiment="neutral" data-meta-type="tag" data-rune="badge" data-density="full">
          3
           
          Review
        </span>
      </p>
    </div>
    <div class="rf-aggregate__group" data-group="ready" data-block="">
      <p>
        <span class="rf-badge rf-badge" data-meta-sentiment="neutral" data-meta-type="tag" data-rune="badge" data-density="full">
          27
           
          Ready
        </span>
      </p>
    </div>
    <div class="rf-aggregate__group" data-group="pending" data-block="">
      <p>
        <span class="rf-badge rf-badge" data-meta-sentiment="neutral" data-meta-type="tag" data-rune="badge" data-density="full">
          14
           
          Pending
        </span>
      </p>
    </div>
    <div class="rf-aggregate__group" data-group="draft" data-block="">
      <p>
        <span class="rf-badge rf-badge" data-meta-sentiment="neutral" data-meta-type="tag" data-rune="badge" data-density="full">
          7
           
          Draft
        </span>
      </p>
    </div>
    <div class="rf-aggregate__group" data-group="done" data-block="">
      <p>
        <span class="rf-badge rf-badge" data-meta-sentiment="neutral" data-meta-type="tag" data-rune="badge" data-density="full">
          499
           
          Done
        </span>
      </p>
    </div>
  </div>
</section>

Group order follows the same domain-aware ordering collection uses — enum fields like status come out in their declared order, not alphabetically. Across mixed types (type="work,bug"), each entity is ranked within its own type's ordering, so the groups still compose.

Omit group and the body renders once with the totals projection on $item — the ungrouped form is what you want when the body is a single composed element from a totals-only query (e.g. just a progress bar).

Sort and limit — operate on groups

{% aggregate type="work" group="status" sort="-count" limit=3 /%}
  • sort — orders groups. Accepts key (the group value; honors domain ordering when the group field has one), count, value, or percent; prefix - (or suffix -desc) for descending.
  • limit — a positive integer cap on the number of groups, applied after sort.

In the per-group template, $item.shown is the post-limit group count — the same number in every iteration.

Chart layout — layout="chart"

Add layout="chart" and the grouped counts render as a chart instead of a body — aggregate builds the data and hands it to the chart rune's pipeline (an SVG, with a no-JS <table> fallback). One bar (or point) per group, in the same domain-aware order:

{% aggregate type="work" group="status" layout="chart" chart-title="Work by status" /%}
<section data-rune="aggregate">
  <meta data-field="aggregate-type" content="work">
  <meta data-field="aggregate-filter" content="">
  <meta data-field="aggregate-value" content="">
  <meta data-field="aggregate-group" content="status">
  <meta data-field="aggregate-sort" content="">
  <meta data-field="aggregate-limit" content="">
  <meta data-field="aggregate-empty" content="">
  <meta data-field="aggregate-layout" content="chart">
  <meta data-field="aggregate-chart-type" content="bar">
  <meta data-field="aggregate-chart-title" content="Work by status">
  <meta data-field="__aggregate-sentinel" content="true">
</section>
Work by status
StatusCount
Blocked1
In Progress4
Review3
Ready27
Pending14
Draft7
Done499
<rf-chart data-type="bar" data-stacked="false" data-elevation="sunken" class="rf-chart" data-rune="chart" data-density="compact">
  <table data-name="data" class="rf-chart__data">
    <caption>Work by status</caption>
    <thead>
      <tr>
        <th>Status</th>
        <th>Count</th>
      </tr>
    </thead>
    <tbody>
      <tr>
        <td data-meta-sentiment="negative">Blocked</td>
        <td>1</td>
      </tr>
      <tr>
        <td data-meta-sentiment="neutral">In Progress</td>
        <td>4</td>
      </tr>
      <tr>
        <td data-meta-sentiment="caution">Review</td>
        <td>3</td>
      </tr>
      <tr>
        <td data-meta-sentiment="neutral">Ready</td>
        <td>27</td>
      </tr>
      <tr>
        <td data-meta-sentiment="neutral">Pending</td>
        <td>14</td>
      </tr>
      <tr>
        <td data-meta-sentiment="neutral">Draft</td>
        <td>7</td>
      </tr>
      <tr>
        <td data-meta-sentiment="positive">Done</td>
        <td>499</td>
      </tr>
    </tbody>
  </table>
</rf-chart>
  • chart-typebar (default), line, area, or pie.
  • chart-title — rendered as the chart caption.
  • Add a value sub-filter to chart a second series (the achieved count per group) beside the totals.
  • An empty query renders the empty fallback, never a broken chart.

No body is needed — aggregate supplies the data table. Chart appearance (palette, geometry, sentiment colouring) comes from the chart rune's --rf-chart-* theming contract.

Empty state

When the primary set is empty:

  • A fallback zone (third body zone, or the leading-empty form --- template --- fallback) wins, with $item bound to all zeros.
  • Otherwise the empty attribute is a one-line text fallback for the self-closing form.
  • With neither, the rune renders nothing.
{% aggregate type="work" filter="status:zzz" empty="Nothing yet." /%}

Precedence matches collection: the body fallback zone wins when both are present.

Attributes

AttributeTypeRequiredDescription
typestringEntity type(s), comma-separated.
filterstringfield:value clauses defining the primary set being measured (SPEC-070 grammar).
valuestringOptional secondary field:value clause defining the achieved subset within `filter` (e.g. value="status:done"). When set, $item.value is the count matching both; $item.percent is the ratio. Without it, the rune is a pure count + breakdown — no progress-bar semantics.
groupstringGroup-by field; omit to render the body once with totals.
sortstringSort groups by `key`, `count`, `value`, or `percent` (prefix `-` for descending). Honors SPEC-072 domain-aware ordering when the group field has one.
limitstringCap the number of groups (after sort).
emptystringFallback text shown when the query matches no entities (self-closing form; body form uses a fallback zone). Absent → render nothing.
layout"chart"Render mode. Default: inline integer (no body) or body-zoned template. `chart` draws the grouped counts as an SVG via the `chart` rune's pipeline instead of a body.
chart-type"bar" | "line" | "area" | "pie"Chart visualization type when layout="chart" (default bar).
chart-titlestringTitle for the chart (rendered as the data table caption) when layout="chart".

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.

AttributeTypeRequiredDescription
bgstringBackground preset applied to this block
bg-fromstringGradient 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-tostringGradient end colour — a semantic token name
bg-viastringOptional 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.

AttributeTypeRequiredDescription
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.

AttributeTypeRequiredDescription
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.

AttributeTypeRequiredDescription
reveal"none" | "fade" | "slide" | "scale" | "blur"Scroll-reveal entrance character (none|fade|slide|scale|blur); the theme owns the choreography
staggerbooleanCascade this block's items in as it reveals (no-op on single-child runes)
spacing

Block-level rhythm override.

AttributeTypeRequiredDescription
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.

AttributeTypeRequiredDescription
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.

AttributeTypeRequiredDescription
tintstringColor 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.

AttributeTypeRequiredDescription
width"compact" | "narrow" | "content" | "wide" | "full"Maximum width constraint for this block
Not availableWhy
dropcap, readingthis rune declares no prose body
framethis rune declares neither a `frameTarget` nor a media section
prominencethis rune has no page-section header

Output contract

The body form:

<section class="rf-aggregate" data-rune="aggregate" data-aggregate="breakdown">
  <div class="rf-aggregate__preamble" data-name="preamble">…</div>
  <div class="rf-aggregate__items" data-name="items">
    <div class="rf-aggregate__group" data-group="done" data-block>…</div>
    <div class="rf-aggregate__group" data-group="ready" data-block>…</div>
  </div>
</section>

The single-number form:

<span class="rf-aggregate" data-rune="aggregate" data-aggregate="count" data-count="42">42</span>

When the query is empty, __items is replaced by <div class="rf-aggregate__empty" data-name="empty">…</div> (rendered from the fallback zone or the empty attribute).

See also

  • collection — the sibling that projects items; shares the field-match grammar and zone semantics.
  • relationships — the sibling that projects edges; shares the body-zone semantics.
  • progress — the typical preamble companion; feed it value=$item.value max=$item.count.
  • badge — the typical per-group template companion; the chip per group.
  • humanize — turn raw keys (in-progress) into display labels ("In Progress") inside a template.