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.
fromnames the model,vianames a field on those pages – when the field is a reference the match follows identity, so a page referenced by alias, retired alias, oruidcounts the same – andwherefilters by field equality. The catalog:count,sum(exact: the result is a decimal string, never a float), andgroup(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, andaccent validaterejects 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/e044prints 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 whenapi.enabledis on. A page whose model declares no computed values answers 404 – andaccent buildwrites<api.prefix>/computed/<url>.jsonfor exactly the pages the server answers, so a CDN serving the built output 404s where the server does. - MCP: the
get_computedtool takes aurland returns the same payload. - Agent tools:
accent agentsessions carry a read-onlyget_computedtool 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 >.