Expression Bindable Text Keys
Expression Bindable Text Keys protocol schemas
Expression-bindable text keys — the CLOSED vocabulary of top-level text keys a SchemaRenderer evaluation memo evaluates, and the per-component carriage map that says which component types carry which of them.
Provenance (not re-litigable here)
objectui#4795 measured the hole: apart from content, no top-level text key
on a rendered SDUI node was BOTH evaluated by the renderer's expression memo
AND read back by the component renderer — statistic.value: '${data.n}'
rendered the literal ${data.n}, and the props-envelope workaround
rendered blank. The 2026-08-17 maintainer ruling on that card deferred the
fix (Direction 1) behind a restart condition and pre-defined its terms,
verbatim: "the key set is declared in @objectstack/spec/types as a closed
enum, never inferred". On 2026-08-18 the maintainer ruled the restart
condition met by product intent (a metadata-authored live dashboard is a
basic requirement), reopening Direction 1 on those terms. This module is the
spec half (objectstack#9599); the objectui half extends the evaluation memo
to CONSUME these exports (it rides objectui#4795) instead of hard-coding a
twin list.
Related, and deliberately out of this module's reach:
- ⛔ Direction 2 (merging the
propsenvelope into the node) is permanently rejected (same 08-17 ruling) — nothing here re-legalizes the envelope shape the objectui#4786 teaching rewrite retired. contentis NOT a member. It already has its own evaluation leg in the memo and its own read-back contract; adding it here would give one key two declared evaluation paths.- The
properties/propsconfig bags are evaluated per-value by their own memo legs (objectui#4799 / #5122) — this vocabulary is only about keys authored at the NODE'S TOP LEVEL.
The contract
For a component type with a row in
EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT, the renderer's
evaluation memo evaluates exactly the listed keys (when the authored value
is an expression-bearing string); every other key stays inert text. For a
component type with NO row, the answer is the empty set — closed and
mechanically answerable in both directions, never inferred from what a
renderer happens to read.
Why these rows (measured, not inferred)
Rows are a RECORD of what each component renderer already reads back from
the node's top level, measured at the .objectui-sha pin 82a9417
(re-verified identical at objectui origin/main 6c68b13 on
2026-08-18) — declaring a key a renderer does not read back would recreate
the evaluated-but-blank half of the objectui#4795 table:
| type | renderer read points (top level, within the closed set) |
|---|---|
statistic | schema.label, schema.value, schema.description (data-display/statistic.tsx) |
card | schema.title, schema.description (layout/card.tsx) |
button | schema.label (form/button.tsx, action/action-button.tsx) |
These are the measured motivating cases from objectui#4795 (dashboard
workhorses). Other registered renderers also read keys from this closed set
at the top level (alert/empty/dialog title+description, badge
label, form inputs' value/label, …) — those rows are deliberately NOT
declared yet: form-control value is interactive state rather than display
text, and each row is an accept-surface widening that should arrive with its
own measurement, not ride this one (startup scope discipline). Adding a row
is additive and spec-first; do it here, never as a renderer-side inference.
Source: packages/spec/src/ui/expression-bindable-text-keys.zod.ts
TypeScript Usage
import { ExpressionBindableTextKeySchema } from '@objectstack/spec/ui';
import type { ExpressionBindableTextKey } from '@objectstack/spec/ui';
// Validate data
const result = ExpressionBindableTextKeySchema.parse(data);ExpressionBindableTextKey
One of the closed set of expression-bindable text keys — the top-level node keys a SchemaRenderer evaluation memo may evaluate (objectui#4795 Direction 1; carriage per component type is EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT).
Allowed Values
titlelabelvaluedescription