Skip to content

Computed Felder

Doku-Pfleger edited this page Aug 27, 2026 · 2 revisions

Computed Fields

Consulting work breathes derivations: budget variance (plan − actual), days left until a deadline, percentage complete, risk score (likelihood × impact). Storing any of these is a quiet lie — the moment one of its inputs changes, the stored value is wrong, and nothing in the file tells you. A field whose value should follow its inputs should not be a number someone maintains by hand.

A computed field expresses exactly that: a function (record) => value that runs on every render, never writes back, and behaves like any other field everywhere it appears. This page is the dedicated reference; the building guide shows the schema snippet in context.

Declaration

{
  key: 'budgetDelta',
  label: 'Budget delta',
  type: 'computed',
  compute: (r) => r.plan - r.actual,
}

Three things matter:

  • key works like any other field key — it appears in list, in column lookups, in the AI context.
  • compute receives the record as it sits at render time, including all other computed fields that have already run for that record in this pass.
  • The result is the value. There is no separate stored value to fall back to; if compute throws, the field is empty in this render (see below).

Where the value lives

compute(record) runs once per record per render pass and the result is memoised on the record for the lifetime of the page. The same record rendered twice does not invoke compute twice — the second render reads the memo. The value is never written into the record, and that is the whole point: a stored derivation is wrong the moment one of its inputs changes, and nothing tells you. The test suite asserts that a saved file contains no trace of a computed field's key.

A computation is as trusted as isDone or isOverdue — it ships in src/domain.js alongside the rest of the schema. If you need a guard against a hostile value, write it inside compute the way you would inside any other helper.

Where the field appears

Everywhere a normal field appears, with one row of exceptions:

Works Doesn't
table column, with numeric alignment when the value is numeric facets — those must be enum
sorting (numerically, if it returns numbers) editing in the form — shown read-only
searching — every field is searched being set by the AI — described as read-only, rejected by name
totalField, metrics, CSV export, AI context CSV import — not offered as a mapping target

A compute whose result is a Date makes the field sortable chronologically; a string makes it sort lexically; a number makes it sort numerically. Sorting follows the field's return type the same way it follows a stored field's declared type.

Numbers in the dashboard

A computed field whose compute returns a number stands in for a real number field in the closed metrics catalogsum(field) and avg(field) both accept it, exactly the way totalField already accepts one. Invalid metric declarations are rejected by name in the toast, the same as for stored fields; there is no silent fallback.

The dueDate widget on the dashboard (details) likewise accepts either a date field or a computed one returning a date — a Days left field that counts down to the due date and goes negative once it lapses is the canonical example.

When a formula throws

A compute that throws yields an empty cell, rendered as a dash () in the table and the form, and does not break the rest of the app. A typo in a formula is annoying; a typo that takes the whole tool down is worse.

The console sees exactly one warning per unique combination of entity, field, record id and error message. The same combination does not warn twice — the renderer carries a Set per singular, keyed on the field name, the record id and the error text. A second render of the same broken record does not flood the console.

Typical uses

A few shapes that come up again and again in consulting tools:

  • Days left(r) => Math.ceil((new Date(r.dueDate) - Date.now()) / 86_400_000). Goes negative once the deadline lapses, sorts chronologically, lands in the due-date widget.
  • Risk score(r) => r.likelihood * r.impact. Numeric, sorts descending into "highest first", sums into an overview tile without storing the product anywhere.
  • Budget variance(r) => r.plan - r.actual. Summed across an entity, it tells you the room left in the budget without anyone maintaining a running balance.
  • Percentage complete(r) => r.total > 0 ? (r.done / r.total) * 100 : null. Returns null rather than NaN so the empty cell renders as a dash rather than as text.

The shipped portfolio example uses Days left on every project.

What this is not

A computed field is per-record and per-render. It does not aggregate across records itself — that is what metrics is for. It does not chain across records, it does not subscribe to other entities' changes, and it does not write back into a stored field. If you need any of those, the answer is one more line in domain.js, not a new field type.

Clone this wiki locally