Background (ADR-0018)
The flow designer's node config forms are generated server-first from each node type's published ActionDescriptor.configSchema (the JSON Schema compiled from the executor's Zod), falling back to a hardcoded client form only when no schema is published. objectui already consumes UI hints carried on schema properties via the Zod .meta() channel — xRef (typed reference picker) and xEnumDeprecated (drop a deprecated enum spelling).
There is currently no way for a schema to say whether a string property is a bare-CEL expression or an interpolate() {var} template. So the adapter maps every string to plain text, dropping the mono editor, the data-picker brace mode, and — critically — the correct validation. Now that the engine publishes a loop configSchema, this has become a live split:
- Offline (hardcoded form):
loop.collection renders as an expression field.
- Online (schema→adapter): the same field renders as plain
text.
And before the objectui-side gating fix, the offline expression rendering also false-positived the CEL brace-trap on the legal {leadList} template. objectui #2670 Phase 3 (PR incoming) fixes the consumer side and defines the contract below; this issue is the producer side — teach the descriptors to emit the marker so the online form matches.
Contract: xExpression
A string property MAY carry xExpression: 'expression' | 'template' (Zod .meta({ xExpression }), same channel as xRef):
'expression' — bare CEL (a predicate / value expression). Mono editor, predicate validation (ADR-0032 brace-trap applies), data-picker inserts bare refs.
'template' — an interpolate() single-brace {var} template. Mono editor, data-picker inserts {var}, no predicate validation (its {…} is legal; only a soft unknown-ref check applies).
format: 'multiline' combines: expression + multiline → textarea in bare-ref (script-body) mode; template + multiline → textarea in template mode.
- Unknown value degrades to plain text (forward-compatible).
- Precedence in the adapter:
xRef > enum > xExpression > scalar.
The objectui adapter mapping (the consumer spec) lives in packages/app-shell/src/views/metadata-admin/inspectors/json-schema-to-fields.ts (scalarField / exprModeOf), with the rendering semantics in FlowNodeConfigField and the canvas scan in flow-expr-problems.ts.
Work items
Annotate the executor Zod schemas so z.toJSONSchema emits xExpression:
Non-goals
- No change to engine/runtime behavior — this is designer metadata only.
xExpression is ignored everywhere except the designer form generator.
Acceptance
- With the engine running, the online
loop config form renders collection identically to the offline hardcoded form (mono expression editor, {var} picker), with no false-positive brace-trap on {leadList}.
- A genuine CEL predicate (
start / decision condition) still validates.
Current evidence
The online (schema→text) and offline (hardcoded→expression) loop.collection forms already diverge today; that divergence is what this marker closes.
Ref: objectui #2670 (Phase 3).
Background (ADR-0018)
The flow designer's node config forms are generated server-first from each node type's published
ActionDescriptor.configSchema(the JSON Schema compiled from the executor's Zod), falling back to a hardcoded client form only when no schema is published. objectui already consumes UI hints carried on schema properties via the Zod.meta()channel —xRef(typed reference picker) andxEnumDeprecated(drop a deprecated enum spelling).There is currently no way for a schema to say whether a string property is a bare-CEL expression or an
interpolate(){var}template. So the adapter maps every string to plaintext, dropping the mono editor, the data-picker brace mode, and — critically — the correct validation. Now that the engine publishes aloopconfigSchema, this has become a live split:loop.collectionrenders as anexpressionfield.text.And before the objectui-side gating fix, the offline
expressionrendering also false-positived the CEL brace-trap on the legal{leadList}template. objectui #2670 Phase 3 (PR incoming) fixes the consumer side and defines the contract below; this issue is the producer side — teach the descriptors to emit the marker so the online form matches.Contract:
xExpressionA string property MAY carry
xExpression: 'expression' | 'template'(Zod.meta({ xExpression }), same channel asxRef):'expression'— bare CEL (a predicate / value expression). Mono editor, predicate validation (ADR-0032 brace-trap applies), data-picker inserts bare refs.'template'— aninterpolate()single-brace{var}template. Mono editor, data-picker inserts{var}, no predicate validation (its{…}is legal; only a soft unknown-ref check applies).format: 'multiline'combines:expression + multiline→ textarea in bare-ref (script-body) mode;template + multiline→ textarea in template mode.xRef > enum > xExpression > scalar.The objectui adapter mapping (the consumer spec) lives in
packages/app-shell/src/views/metadata-admin/inspectors/json-schema-to-fields.ts(scalarField/exprModeOf), with the rendering semantics inFlowNodeConfigFieldand the canvas scan inflow-expr-problems.ts.Work items
Annotate the executor Zod schemas so
z.toJSONSchemaemitsxExpression:loop/mapcollection→template(values are{leadList}/{items}).startentrycondition,decisionbranchexpression/condition, edge guards, screenvisibleWhen→expression.scriptcode body →expression+multiline(bare refs;{x}is illegal in JS/TS).configSchemaat all — use objectui's hardcodedflow-node-config.tsfield groups as the spec of record, so online and offline forms converge.Non-goals
xExpressionis ignored everywhere except the designer form generator.Acceptance
loopconfig form renderscollectionidentically to the offline hardcoded form (mono expression editor,{var}picker), with no false-positive brace-trap on{leadList}.start/decisioncondition) still validates.Current evidence
The online (schema→text) and offline (hardcoded→expression)
loop.collectionforms already diverge today; that divergence is what this marker closes.Ref: objectui #2670 (Phase 3).