CONDITIONAL VISIBILITY
CONDITIONAL VISIBILITY
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 |