Conditional visibility — frontend integration
Conditional visibility — frontend integration
Status: backend shipped on stage (2026-07-27). Frontend not yet built.
What's new
A workspace admin can attach a show/hide rule to a single field so the record
form/detail reveals it only when a condition on another field of the same
object is met — e.g. show "VAT number" only when "Customer type = Business".
Visibility is presentation behavior only: no new field type, no column, no
data change. It rides the field's existing options JSON (same blob that holds
conditional_requirement).
The full behavior contract is reproduced in Full behavior contract
at the bottom of this page — source of truth: src/api/v1/object/fields/CONDITIONAL_VISIBILITY.md.
No new endpoints
The rule is saved through the existing field update/create:
PATCH /object/field/:objectSlug/:fieldSlug (and create) with an
options.conditional_visibility block. Shape:
"conditional_visibility": {
"enabled": true,
"action": "show", // or "hide"
"logic": "AND", // AND | OR (multi-condition)
"conditions": [
{ "trigger_field_slug": "customer_type", "operator": "equals", "values": ["business"] }
],
"clear_on_hide": false // default: retain the hidden value
}A single condition may also use the flat shorthand
(trigger_field_slug/operator/values); the server normalises it into
conditions[]. Operators: equals | not_equals | in | not_in | is_empty | is_not_empty (the same enum as conditional_requirement). All references are
slug-first — never a numeric field id. Values for select-family triggers may
be sent as option id/value/label; the server canonicalises them.
What the FE needs to do
- Feature flag: when
CONDITIONAL_FIELD_VISIBILITY_ENABLEDis off, the server ignoresconditional_visibilityon read + write and every field is always visible. Render as today. - Rule builder (field editor → "Visibility" tab): read as a sentence — "[Show/Hide] this field when [trigger] [operator] [value(s)]". The trigger picker must exclude the field itself and any field that would form a cycle.
- Live form eval: evaluate each field's rule against the record's current in-form values and show/hide as the user types. Compare by option id, not label. Depth-cap live re-eval and debounce.
- Server is the source of truth (important): the client's hidden state is a
hint only. On submit the server re-evaluates every rule and:
- a field it computes hidden has its required check skipped and is not validated (so a hidden required field never blocks the save);
- the hidden field's value is retained (or nulled if
clear_on_hide). Do not rely on hiding for correctness or security — a hidden field is still in the API response, export, and PDF.
- Config-time errors to surface:
422 VISIBILITY_SELF_REFERENCE,422 VISIBILITY_CYCLE,400 TRIGGER_NOT_IN_OBJECT,400 INVALID_OPTIONS. - Dangling trigger: if the trigger field was later deleted/deactivated the rule is inert and the field always shows — surface a "trigger missing" warning with a one-click remove.
- List/table/kanban/export: unchanged — visibility is a form/detail concern.
Not in this backend slice (future work)
- Live-preview UI, conditional document-template rendering.
- Field-level permission (restricting who can read a field) — a separate, not-yet-built control. Visibility is not a security boundary.
Full behavior contract
Show or hide a single field in the record form/detail based on a condition
evaluated on another field of the same object. Pure presentation behavior —
no new field type, no physical column, no data mutation. Gated behind
CONDITIONAL_FIELD_VISIBILITY_ENABLED; when off, every field is always visible
and required/validation behave exactly as before.
Where it lives
The rule is stored on the target field's existing options JSON as
conditional_visibility. It reuses the conditional_requirement operator engine
(ValidationEngineService) and the shared operand-resolution util — there is no
new evaluation engine.
"conditional_visibility": {
"enabled": true,
"action": "show", // "show" ⇒ visible when met; "hide" ⇒ hidden when met
"logic": "AND", // AND | OR fold across conditions (default AND)
"conditions": [
{ "trigger_field_slug": "customer_type", "operator": "equals", "values": ["business"] }
],
"clear_on_hide": false // default: retain the hidden value; true ⇒ null it on write
}A single-condition rule may use the flat shorthand
(trigger_field_slug/operator/values); config-time validation folds it into
conditions[] and canonicalises operand values to the runtime-comparable form.
Operators (same enum as conditional_requirement):
equals | not_equals | in | not_in | is_empty | is_not_empty. Select-family
triggers compare by canonical option value/id, never label.
Correctness invariants (server-enforced)
| Rule | Behavior |
|---|---|
| Hidden ⇒ not required (FR-3520) | A field the server computes as hidden has its required check and its conditional_requirement skipped, so a hidden required field can never dead-end a save. Enforced in both DynamicValidationService (static required) and ValidationEngineService (required-if). |
| Hidden value (FR-3521) | Retained un-validated by default; clear_on_hide: true nulls it on write. |
| Server is source of truth (FR-3522) | On every write the server re-evaluates each rule from submitted data; the client's hidden state is a hint only. |
| Cycle / self-reference (FR-3530) | Rejected at configure time: 422 VISIBILITY_SELF_REFERENCE, 422 VISIBILITY_CYCLE. |
| Dangling / deactivated trigger (FR-3531) | Rule inert ⇒ field visible (fail-open); never crashes. |
| Fail-open everywhere | Any eval error, missing trigger, or empty/disabled rule resolves to visible + validated normally — hiding is what creates the dangerous hidden-required dead-end, so we never hide on a rule we could not evaluate. |
Scope
Visibility is a form/detail concern. List/table/kanban/export column layout and the field's physical column + stored value are unaffected — a hidden field still has its column, still holds its value, still exports it.
Hiding is not a security control. A hidden field is still returned by the API, exported, rendered in documents, and writable by workflow/AI. Restricting who can read a field is field-level permission — a separate, not-yet-built control.
Code map
| Concern | File |
|---|---|
| Rule shape | object/shared/interfaces/conditional-visibility.interface.ts |
| Shared operator + operand resolution | object/shared/utils/condition-evaluator.util.ts |
isFieldHidden evaluator | object/shared/utils/conditional-visibility.util.ts |
| Feature flag | object/fields/services/visibility-feature.ts |
| Config-time guard (cycle/self-ref/trigger) | object/fields/services/field/utils/validate-conditional-visibility.ts |
| Write-path skip-required + clear/retain | object/fields/services/validation/dynamic-validation.service.ts |
| Required-if skip when hidden | object/fields/services/validation/validation-engine.service.ts |
| Config wiring | field-create.service.ts / field-update.service.ts |