-
Notifications
You must be signed in to change notification settings - Fork 0
Computed Felder
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.
{
key: 'budgetDelta',
label: 'Budget delta',
type: 'computed',
compute: (r) => r.plan - r.actual,
}Three things matter:
-
keyworks like any other field key — it appears inlist, in column lookups, in the AI context. -
computereceives 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
computethrows, the field is empty in this render (see below).
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.
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.
A computed field whose compute returns a number stands in for a real number field in the
closed metrics catalog — sum(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.
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.
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. Returnsnullrather thanNaNso the empty cell renders as a dash rather than as text.
The shipped portfolio example uses Days left
on every project.
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.