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) |
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). text reads
back schema.content || schema.value (basic/text.tsx) — unlike the rows
above, its omission is not merely unmeasured: value there is a fallback
spelling for the same slot content already evaluates (see content NOT a
member, above), not a second read point, so a text: ['value'] row would
legitimize the fallback spelling and give one slot two declared evaluation
paths. The objectstack#13670 ruling settled text's intended evaluation
channel as content alone and declared text.value OUT on those grounds —
this omission is deliberate, not pending measurement. Adding a row is
additive and spec-first; do it here, never as a renderer-side inference.
Rows are keyed on the AUTHORED type string, prefix and all
expressionBindableTextKeysFor does an own-property lookup on the
type string exactly as authored, so a namespace-prefixed spelling —
action:button, ui:button, the mcp: family — is a DIFFERENT key from
the bare name and answers the empty set BY CONSTRUCTION, not by oversight.
Nothing strips a prefix between the two, and nothing should: a stripping
rule would in the same motion grant rows to element:button and
page:card, whose renderers take their config from the evaluated
properties bag and never read these keys at the node's top level.
action:button is therefore deliberately OUT, and ui:button with it
(objectstack#13672); the button row above covers the bare button
spelling alone, which is why its citation names form/button.tsx only. Two
measured grounds, the same two kinds every other row runs on — zero pull
(the objectui corpus census of 736 JSON documents / 2747 typed nodes finds 5
action:button nodes, 0 of them carrying an expression in label, and 0
ui:-prefixed spellings of any kind), and this module's own admission rule
that a row arrives with its own measurement while a type string with no row
IS the empty set. Not a judgement that the key is inert:
action/action-button.tsx renders the author-written schema.label
directly (measured 2026-08-31), so the read-back half is real and only the
pull is missing. The reopen path is the ordinary one: a named requirement
for an expression-bound action:button label adds a row here, carrying that
row's own measurement.
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 (Direction 1; carriage per component type is EXPRESSION_BINDABLE_TEXT_KEYS_BY_COMPONENT).
Allowed Values
titlelabelvaluedescription