fix(automation): put a screen field's visibleWhen on the wire (#3528) - #3771
Merged
Conversation
`visibleWhen` has been on the `screen` node's designer form since #3304 — declared `xExpression`, documented as bare CEL, offered in Studio — but the executor dropped it when building the paused payload. `ScreenFieldSpec` stopped at `placeholder`, so no client ever received the predicate and none could honour it. Authors wrote conditional visibility; every field rendered unconditionally; nothing errored. `required` *is* honoured, which is what makes this fatal rather than cosmetic. A field that is optional-by-design but required when shown becomes permanently required once its predicate is dropped, and a runner validating the full field list then blocks Submit on input the user was never asked for — no resume request is issued and the run stays paused. That is the reported #3528 symptom, reproduced in a browser against HotCRM's lead-conversion screen on both the console shipped with 16.1.0 and current objectui main: trigger fires, the screen renders all three fields, Submit issues zero network requests. - `ScreenFieldSpec.visibleWhen` joins the contract, documented as client-evaluated bare CEL over the screen's own field names, and states the rule an implementor needs: a hidden field must not be enforced as required. - The executor forwards it raw. Interpolating here would resolve it once against flow variables, freezing a predicate the client must re-evaluate against values only it can see. - Tests for the screen wire payload, which had none for this key; both new assertions verified to fail without the forwarding line. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0122xvfanLmniNbsAPtg5hk3
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
Contributor
📓 Docs Drift CheckThis PR changes 2 package(s): 106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
The flows guide covered the `object-form` screen step but never the default
flat `fields` list, so the shape most screens actually use was undocumented —
including `visibleWhen`, which this branch just made live on the wire.
Records the three things an author cannot infer from the type: the predicate is
bare CEL over the screen's own field names (NOT the `{var}` template dialect the
rest of a node's config uses), `required` on a conditional field means "required
when shown", and a `required` boolean needs `defaultValue: false` or the user
cannot express "no" by leaving the box clear. Also notes that a broken predicate
fails open, since nothing validates it at author time yet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0122xvfanLmniNbsAPtg5hk3
os-zhuang
marked this pull request as ready for review
July 28, 2026 05:18
os-zhuang
added a commit
to objectstack-ai/objectui
that referenced
this pull request
Jul 28, 2026
…alidation (framework#3528) (#2899) A paused screen-flow rendered every declared field regardless of its `visibleWhen` predicate while still enforcing `required` over the full list. Where a field is optional-by-design but required *when shown*, that dead-ends the run: Submit blocks on an input the user was never shown, issues zero network requests, and the flow stays paused. The predicate never reached the client — the framework declared `visibleWhen` on the screen node's designer form but dropped it when building the paused payload (fixed in objectstack-ai/objectstack#3771). This is the consumer half. - `visibleScreenFields(screen, values)` is the single source of truth: ScreenView renders from it, FlowRunner.submit() validates from it. Splitting render from enforcement is precisely the defect. - Predicates are bare CEL over the screen's own field names, evaluated by the canonical @objectstack/formula engine — the same verdict the server reaches for field rules. - Declared fields are seeded before evaluation: an untouched checkbox holds `undefined`, which CEL reads as an unknown identifier, so the evaluation errors and falls open, leaving the dependent field visible in exactly the state where it should be hidden. - Fail-open survives for genuinely broken predicates — hiding an input on a typo would silently drop data the flow is waiting for. Both new regression assertions verified to fail with either half reverted.
os-zhuang
added a commit
that referenced
this pull request
Jul 28, 2026
…ough) (#3788) Ships the #3528 fix to actual users. `@objectstack/console` publishes a PREBUILT objectui SPA frozen at `.objectui-sha`, so a merged frontend fix reaches nobody until the pin moves: #3771 was sending a field the shipped console could not read, and objectui#2899 taught a console no user was running. That is the same trap that made the first #3528 repro look like the bug was somewhere else entirely — the console source was already correct while /_console served an older build (documented in #3558). Pin: 09c6a177bb4a → 1bb77aa24514 (objectui main), carrying the #3528 console half plus 24 other frontend commits merged since the last refresh. The changeset lists the full range. Produced by scripts/bump-objectui.sh, which stamped `minor` on its own — the range contains feat commits, and the pipefail/SIGPIPE bug that used to force every refresh to `patch` was fixed in #3558. `pnpm check:console-sha` passes.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Server half of #3528. Console half: objectui#2899.
The reported issue was triaged as explained, but the explanation did not fit the reporter's app
#3528 was attributed to the
object-formlazy-chunk teardown fixed in objectui#2830. That mechanism cannot fire on HotCRM's flows:object-formwizard"lead_conversion/schedule_followupare flat-field screens — noobject-formstepScreenView.FieldInput) renders statically-importedInput/Select/Checkbox/Textarea— nothing suspendsrefreshAfter: trueracing the pausecf2d56e:flowHandlerreturns early onpausedwithout refreshingSo the reporter's path had a different cause, still live. I reproduced it in Chromium against a real HotCRM dev server — on both the console shipped with 16.1.0 and current objectui main (HMR console pointed at the same backend). Identical:
Zero network requests on Submit, run paused forever — the reported symptom exactly.
Root cause: a declared property that never reached the wire
visibleWhenhas been on thescreennode's designer form since #3304 —screen-nodes.ts:66, typedxExpression, documented one comment above as bare CEL, offered to authors in Studio. The executor's field mapper built the paused payload fromname/label/type/required/options/defaultValue/placeholderand dropped it.ScreenFieldSpecin the contract stopped atplaceholdertoo — objectui's client-side type mirrors it exactly, because it is a faithful copy of what the server actually sends.So no client was ignoring the predicate; none ever received it. Authors wrote conditional visibility, every field rendered unconditionally, and nothing errored anywhere. The key had zero test coverage.
Why this is fatal rather than cosmetic:
requiredis honoured. A field that is optional-by-design but required when shown becomes permanently required once its predicate is dropped, and a runner validating the full field list blocks Submit on input the user was never asked for. HotCRM's screen is precisely that shape:Leave the checkbox unticked and
opportunityName— which should not be on screen at all — dead-ends the flagship lead-conversion journey.This PR
ScreenFieldSpec.visibleWhenjoins the contract (@objectstack/spec), documented as client-evaluated bare CEL over the screen's own field names, and states the rule an implementor needs: a hidden field is not collected, so it must not be enforced asrequired. Honouring one without the other is the dead-end above.screenexecutor forwards it raw — deliberately uninterpolated. The predicate is re-evaluated on every keystroke against values only the client holds; resolving it here against flow variables would bake in one verdict and freeze the field for the life of the screen. A test pins that (a predicate embedding a live{recordId}token comes back verbatim).No engine, route, or resume-path changes — those halves work, and
automation.resumehas been on the SDK since #3552.Test plan
packages/services/service-automation— 368 passed (33 files), 3 new.packages/speccontracts + automation — 523 passed (45 files).expected [undefined, undefined, undefined]andexpected undefined to be 'tier == "gold" && "{recordId}" != ""'.End-to-end, with this change applied to the running server (equivalent patch into HotCRM's installed 16.1.0 dist) plus objectui#2899 and the HotCRM authoring fixes:
Follow-ups, not in this PR
required— objectui#2899. Until it ships, this change is inert on that surface: the field arrives but nothing reads it.'{createOpportunity} == true'(flow{var}interpolation) rather than the bare CEL this key declares — and itscreate_accountnode copiesindustryinto an enum that holds only 6 of the lead's 15 values, aborting conversion for nine industries. Both fixed in a HotCRM PR.FlowNodeSchema.configisz.record(z.unknown()), so nothing validates screen field keys at author time. AvisibleWhentypo — or the wrong dialect above — is silently accepted today. That is the gap that let HotCRM ship a predicate no layer would ever have complained about.Correction
An earlier revision of this description claimed the blocked-Submit path surfaces no toast. That was wrong — a measurement artifact. My probe polled 5s after the click and sonner's error toast lasts ~4s. Polling tightly shows it at ~250ms:
"Please fill: What is the next step?, Due Date". There is no separate silent-block defect; the real signal was always the zero network requests.🤖 Generated with Claude Code
https://claude.ai/code/session_0122xvfanLmniNbsAPtg5hk3