Computed values

A model can declare values computed from other pages and from the page’s own fields. Nothing is ever written back: accent serve and accent build both compute from the content they see, so the two always agree, and a hand-maintained total cannot drift.

Declare them on the model

# models/epic.yaml
computed:
  features_total: { query: count, from: feature, via: epic }
  features_done:  { query: count, from: feature, via: epic, where: { state: done } }
  budget:         { query: sum, from: feature, via: epic, field: price }
  by_state:       { query: group, from: feature, via: epic, by: state }
  remaining:      { formula: "features_total - features_done" }

Two kinds:

  • Queries run over the pages of another model. from names the model, via names a field on those pages – when the field is a reference the match follows identity, so a page referenced by alias, retired alias, or uid counts the same – and where filters by field equality. The catalog: count, sum (exact: the result is a decimal string, never a float), and group (a map of value to count).
  • Formulas are expressions over the page’s fields and its sibling computed values: literals, field names, arithmetic, comparisons, and/or/not, and calls to the built-in date and decimal functions (date, days_between, dec, dec_add, dec_sub, dec_mul, dec_div, dec_round, dec_cmp). Filters, tests, and attribute access are outside the subset, and accent validate rejects them when the model loads, as it rejects a cycle between formulas. Note the spelled operators: and, not &&.

Every evaluation is bounded: a formula that runs away exhausts its fuel and reports a validation finding instead of hanging a build.

Read them in templates

<p>{{ page.computed.features_done | default(0) }} of
   {{ page.computed.features_total | default(0) }} done,
   {{ page.computed.budget | default("0") }} committed.</p>

{% set by_state = page.computed.by_state | default({}) %}
<p>{{ by_state.open | default(0) }} still open.</p>

page.computed is always present; a key only exists on pages whose model declares it, so read values through default() – and read a group result’s buckets through a guarded variable as above, because a missing map has no attributes to reach into.

A failed query or formula leaves its key undefined and surfaces in accent validate as a ModelComputedInvalid finding naming the value and the reason.

Show them in the page body

The same values are Markdoc variables, under $computed, so a page can show its own counts without a template change:

{% $computed.features_done %} of {% $computed.features_total %} features
are done.

{% if $computed.over_budget %}
This epic is over budget.
{% /if %}

Only the names the page’s model declares exist; accent validate reports any other $computed read on the page as an undefined variable, with the page and position named. A shared partial reads the values of the page that includes it, so a $computed read inside a partial is not reported – the same rule $frontmatter keys follow.

A shown value stays current on both sides: the server re-renders a cached page whose underlying values changed, and an incremental accent build re-renders exactly the pages whose values moved – change a referenced page’s field and the page showing the count is rebuilt with it. Under accent serve --production the page cache validates nothing, so a value refreshes when the page would: on an admin, agent, or MCP write, or on a config reload (SIGHUP).

Read them from every query surface

The same values answer outside templates, through one resolver:

  • CLI: accent query computed /epics/e044 prints the page’s URL, resolved model, and computed name-value map as JSON. A page whose model declares none prints an empty map; only an unknown URL is an error.
  • REST API: GET <api.prefix>/computed/<url> answers the same JSON when api.enabled is on. A page whose model declares no computed values answers 404 – and accent build writes <api.prefix>/computed/<url>.json for exactly the pages the server answers, so a CDN serving the built output 404s where the server does.
  • MCP: the get_computed tool takes a url and returns the same payload.
  • Agent tools: accent agent sessions carry a read-only get_computed tool with the same contract.

What this is not

Results are never stored, never written to frontmatter, and never served from yesterday’s numbers: a page’s computed values refresh with the content, in dev serve on every change and in a build from the pages the build sees. Sums travel as decimal strings to stay exact; compare them with dec_cmp in formulas rather than >.