Skip to content

ActionDescriptor configSchema: adopt an xExpression marker so designer forms render expression vs template semantics (objectui #2670 Phase 3 counterpart) #3304

Description

@os-zhuang

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:

  • loop / map collection → template (values are {leadList} / {items}).
  • start entry condition, decision branch expression / condition, edge guards, screen visibleWhen → expression.
  • script code body → expression + multiline (bare refs; {x} is illegal in JS/TS).
  • Audit connector / plugin descriptors for expression/template string fields and mark them.
  • Fill the ~11 node types that publish no configSchema at all — use objectui's hardcoded flow-node-config.ts field groups as the spec of record, so online and offline forms converge.

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).

Activity

  1. os-zhuang commented on Jul 20, 2026

    @os-zhuang
    ContributorAuthor

    Complete. Final map of the implementation series:

    group outcome
    loop, map configSchemas + xExpression markers — #3313, #3321
    assignment, create_record / update_record / delete_record / get_record, screen configSchemas (open-map additionalProperties: true shape for fields / filter / assignments / defaults) — #3327
    consumer side objectui #2670 Phase 3 (objectstack-ai/objectui#2699): xExpression mapping + validation gating; objectstack-ai/objectui#2708: keyValue + numberList schema mapping
    decision, wait, script, subflow deliberately schemaless — a node with no configSchema renders identically online/offline (hardcoded fallback), so there is no divergence; a partial schema would drop editors the adapter cannot express (decision's edge-derived Target column, wait's top-level waitEventConfig, script's actionType-conditional form, subflow's top-level timeoutMs). Asserted in config-schemas.test.ts so a partial schema can't slip in silently.

    Every node where server-driven parity is achievable without regression now has it; the rest are documented, tested design decisions. Value schemas for the maps are deliberately permissive (true) — real metadata carries operator objects ({"$ne": null}), {var} templates, and non-string literals.

    Possible follow-ups (not gaps, filed as future work): a generic conditional-visibility marker (e.g. xShowWhen) would unlock script; a cross-repo conformance fixture running published schemas through the documented mapping table would harden the descriptor↔adapter contract.


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions