Skip to content

fix(automation): put a screen field's visibleWhen on the wire (#3528) - #3771

Merged
os-zhuang merged 2 commits into
mainfrom
claude/console-screen-flow-submit-2roins
Jul 28, 2026
Merged

fix(automation): put a screen field's visibleWhen on the wire (#3528)#3771
os-zhuang merged 2 commits into
mainfrom
claude/console-screen-flow-submit-2roins

Conversation

@os-zhuang

@os-zhuang os-zhuang commented Jul 28, 2026

Copy link
Copy Markdown
Contributor

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-form lazy-chunk teardown fixed in objectui#2830. That mechanism cannot fire on HotCRM's flows:

Claim in triage HotCRM's actual metadata
"a lead-conversion object-form wizard" lead_conversion / schedule_followup are flat-field screens — no object-form step
lazy field widgets suspend → host remounts the flat path (ScreenView.FieldInput) renders statically-imported Input/Select/Checkbox/Textarea — nothing suspends
refreshAfter: true racing the pause already guarded at the 16.1.0 console pin cf2d56e: flowHandler returns early on paused without refreshing

So 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:

→ POST /api/v1/automation/lead_conversion/trigger      200 {status: paused, screen}
   dialog renders: ["Create Opportunity? *", "Opportunity Name *", "Opportunity Amount"]
   click Submit (checkbox untouched)
→ (nothing)
RESUME calls: 0        dialog still open: true

Zero network requests on Submit, run paused forever — the reported symptom exactly.

Root cause: a declared property that never reached the wire

visibleWhen has been on the screen node's designer form since #3304screen-nodes.ts:66, typed xExpression, documented one comment above as bare CEL, offered to authors in Studio. The executor's field mapper built the paused payload from name / label / type / required / options / defaultValue / placeholder and dropped it. ScreenFieldSpec in the contract stopped at placeholder too — 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: required is 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:

{ name: 'createOpportunity', type: 'boolean', required: true },
{ name: 'opportunityName',   type: 'text', required: true,
  visibleWhen: 'createOpportunity == true' },

Leave the checkbox unticked and opportunityName — which should not be on screen at all — dead-ends the flagship lead-conversion journey.

This PR

  • ScreenFieldSpec.visibleWhen joins 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 as required. Honouring one without the other is the dead-end above.
  • The screen executor 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).
  • Tests for the screen wire payload, which had none for this key.

No engine, route, or resume-path changes — those halves work, and automation.resume has been on the SDK since #3552.

Test plan

  • packages/services/service-automation368 passed (33 files), 3 new.
  • packages/spec contracts + automation — 523 passed (45 files).
  • Both new forwarding assertions verified to fail without the fix: deleting the one added line yields expected [undefined, undefined, undefined] and expected undefined to be 'tier == "gold" && "{recordId}" != ""'.
  • ESLint clean on all changed files.

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:

→ POST .../lead_conversion/trigger                 (paused)
   rendered: ["Create Opportunity? *"]              ← conditional fields correctly hidden
   click Submit
→ POST .../runs/run_63d43c4c.../resume  {"inputs":{"createOpportunity":false}}
   toast "Done", dialog closed, 0 server errors     ← flow ran to completion

Follow-ups, not in this PR

  • objectui must evaluate the predicate and skip hidden fields when enforcing required — objectui#2899. Until it ships, this change is inert on that surface: the field arrives but nothing reads it.
  • HotCRM authored the wrong dialect'{createOpportunity} == true' (flow {var} interpolation) rather than the bare CEL this key declares — and its create_account node copies industry into an enum that holds only 6 of the lead's 15 values, aborting conversion for nine industries. Both fixed in a HotCRM PR.
  • FlowNodeSchema.config is z.record(z.unknown()), so nothing validates screen field keys at author time. A visibleWhen typo — 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

`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
@vercel

vercel Bot commented Jul 28, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
objectstack Ignored Ignored Jul 28, 2026 4:40am

Request Review

@github-actions github-actions Bot added documentation Improvements or additions to documentation tests tooling labels Jul 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 2 package(s): packages/services, @objectstack/spec.

106 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:

  • content/docs/ai/agents.mdx (via @objectstack/spec)
  • content/docs/ai/skills-reference.mdx (via @objectstack/spec)
  • content/docs/ai/skills.mdx (via @objectstack/spec)
  • content/docs/api/client-sdk.mdx (via @objectstack/spec)
  • content/docs/api/environment-routing.mdx (via @objectstack/spec)
  • content/docs/api/error-catalog.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-client.mdx (via @objectstack/spec)
  • content/docs/api/error-handling-server.mdx (via @objectstack/spec)
  • content/docs/api/index.mdx (via @objectstack/spec)
  • content/docs/automation/approvals.mdx (via packages/spec)
  • content/docs/automation/flows.mdx (via @objectstack/spec)
  • content/docs/automation/hook-bodies.mdx (via packages/spec)
  • content/docs/automation/hooks.mdx (via @objectstack/spec)
  • content/docs/automation/index.mdx (via @objectstack/spec)
  • content/docs/automation/webhooks.mdx (via packages/services, @objectstack/spec)
  • content/docs/automation/workflows.mdx (via @objectstack/spec)
  • content/docs/concepts/architecture.mdx (via @objectstack/spec)
  • content/docs/concepts/design-principles.mdx (via packages/spec)
  • content/docs/concepts/index.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-driven.mdx (via @objectstack/spec)
  • content/docs/concepts/metadata-lifecycle.mdx (via packages/spec)
  • content/docs/concepts/north-star.mdx (via packages/spec)
  • content/docs/data-modeling/analytics.mdx (via @objectstack/spec)
  • content/docs/data-modeling/drivers.mdx (via @objectstack/spec)
  • content/docs/data-modeling/external-datasources.mdx (via @objectstack/spec)
  • content/docs/data-modeling/field-types.mdx (via @objectstack/spec)
  • content/docs/data-modeling/fields.mdx (via @objectstack/spec)
  • content/docs/data-modeling/formulas.mdx (via @objectstack/spec)
  • content/docs/data-modeling/index.mdx (via @objectstack/spec)
  • content/docs/data-modeling/objects.mdx (via @objectstack/spec)
  • content/docs/data-modeling/queries.mdx (via @objectstack/spec)
  • content/docs/data-modeling/schema-design.mdx (via @objectstack/spec)
  • content/docs/data-modeling/seed-data.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation-rules.mdx (via @objectstack/spec)
  • content/docs/data-modeling/validation.mdx (via @objectstack/spec)
  • content/docs/deployment/cli.mdx (via @objectstack/spec)
  • content/docs/deployment/troubleshooting.mdx (via @objectstack/spec)
  • content/docs/deployment/validating-metadata.mdx (via @objectstack/spec)
  • content/docs/getting-started/build-with-claude-code.mdx (via @objectstack/spec)
  • content/docs/getting-started/common-patterns.mdx (via @objectstack/spec)
  • content/docs/getting-started/examples.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-reference.mdx (via @objectstack/spec)
  • content/docs/getting-started/quick-start.mdx (via @objectstack/spec)
  • content/docs/getting-started/your-first-project.mdx (via @objectstack/spec)
  • content/docs/kernel/cluster.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/auth-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/cache-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/data-engine.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/index.mdx (via @objectstack/spec)
  • content/docs/kernel/contracts/metadata-service.mdx (via packages/spec)
  • content/docs/kernel/contracts/storage-service.mdx (via packages/spec)
  • content/docs/kernel/index.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/audit-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/email-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/index.mdx (via packages/services, packages/spec)
  • content/docs/kernel/runtime-services/queue-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/settings-service.mdx (via packages/services)
  • content/docs/kernel/runtime-services/sharing-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/sms-service.mdx (via packages/spec)
  • content/docs/kernel/runtime-services/storage-service.mdx (via packages/spec)
  • content/docs/kernel/services-checklist.mdx (via @objectstack/spec)
  • content/docs/permissions/authorization.mdx (via @objectstack/spec)
  • content/docs/permissions/permission-sets.mdx (via @objectstack/spec)
  • content/docs/permissions/permissions-matrix.mdx (via @objectstack/spec)
  • content/docs/permissions/positions.mdx (via @objectstack/spec)
  • content/docs/permissions/rls.mdx (via @objectstack/spec)
  • content/docs/permissions/sharing-rules.mdx (via @objectstack/spec)
  • content/docs/plugins/adding-a-metadata-type.mdx (via @objectstack/spec)
  • content/docs/plugins/development.mdx (via @objectstack/spec)
  • content/docs/plugins/index.mdx (via @objectstack/spec)
  • content/docs/plugins/packages.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/backward-compatibility.mdx (via @objectstack/spec)
  • content/docs/protocol/diagram.mdx (via packages/spec)
  • content/docs/protocol/kernel/config-resolution.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/i18n-standard.mdx (via packages/services, @objectstack/spec)
  • content/docs/protocol/kernel/index.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/lifecycle.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/plugin-spec.mdx (via @objectstack/spec)
  • content/docs/protocol/kernel/runtime-capabilities.mdx (via @objectstack/spec)
  • content/docs/protocol/knowledge.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/query-syntax.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/schema.mdx (via @objectstack/spec)
  • content/docs/protocol/objectql/security.mdx (via packages/spec)
  • content/docs/protocol/objectql/state-machine.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/actions.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/concept.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/index.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/layout-dsl.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/record-alert.mdx (via @objectstack/spec)
  • content/docs/protocol/objectui/widget-contract.mdx (via @objectstack/spec)
  • content/docs/releases/implementation-status.mdx (via @objectstack/spec)
  • content/docs/releases/index.mdx (via @objectstack/spec)
  • content/docs/releases/v12.mdx (via @objectstack/spec)
  • content/docs/releases/v13.mdx (via @objectstack/spec)
  • content/docs/releases/v16.mdx (via @objectstack/spec)
  • content/docs/releases/v9.mdx (via @objectstack/spec)
  • content/docs/ui/actions.mdx (via @objectstack/spec)
  • content/docs/ui/create-vs-edit-form.mdx (via @objectstack/spec)
  • content/docs/ui/dashboards.mdx (via @objectstack/spec)
  • content/docs/ui/forms.mdx (via @objectstack/spec)
  • content/docs/ui/index.mdx (via @objectstack/spec)
  • content/docs/ui/public-data-collection.mdx (via @objectstack/spec)
  • content/docs/ui/setup-app.mdx (via @objectstack/spec)
  • content/docs/ui/translations.mdx (via @objectstack/spec)
  • content/docs/ui/views.mdx (via @objectstack/spec)

Advisory only. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs origin/main → pass the list as args.docs.

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
os-zhuang marked this pull request as ready for review July 28, 2026 05:18
@os-zhuang
os-zhuang merged commit 7687f7b into main Jul 28, 2026
17 checks passed
@os-zhuang
os-zhuang deleted the claude/console-screen-flow-submit-2roins branch 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/m tests tooling

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants