Expression Bindable Text Keys — UI Protocol
Expression-bindable text keys — the CLOSED vocabulary of top-level text keys a SchemaRenderer evaluation memo evaluates.
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 ruling that commit 8c6a7fc0b records 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
(commit e854a531a); 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