ObjectStackObjectStack

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 props envelope into the node) is permanently rejected (same 08-17 ruling) — nothing here re-legalizes the envelope shape the objectui#4786 teaching rewrite retired.
  • content is 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 / props config 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:

typerenderer read points (top level, within the closed set)
statisticschema.label, schema.value, schema.description (data-display/statistic.tsx)
cardschema.title, schema.description (layout/card.tsx)
buttonschema.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

  • title
  • label
  • value
  • description

On this page