Unify Logo Footer.svg
Unify Applications
Logo
Conditions & Visibility

Conditions & Visibility

Logo

9 mins READ

How Conditions Work

The platform never stores show/hide or run/skip logic as a raw JavaScript expression. Instead it stores a condition object — a structured, typed filter evaluated by the runtime's operator engine. This makes conditions predictable, inspectable, and consistent across visibility, editability, and action gating.

The same condition builder powers three distinct features:

  • Visibility — show or hide a component based on data, roles, or variables.

  • Editable / Disabled / Required — control whether a user can interact with a component.

  • Only run when (runCondition) — gate an action so it only executes when the condition holds.

Learn the condition builder once and you can drive all three consistently.

Condition Object Shape

A condition object is always one of two forms:

Form 1 — boolean literal

{ "type": "boolean", "payload": true } { "type": "boolean", "payload": false }

Form 2 — filter group

{ "type": "filter", "payload": { "operator": "AND", "filters": [ { "property": "{{ user.role }}", "filter": { "operator": "EQUAL", "value": "admin" } } ] } }

Warning: Always wrap leaves in a group. Even for a single condition, write {"operator":"AND","filters":[<leaf>]} — this matches the builder's stored shape. A bare leaf at the root is parsed but is non-standard.

Component Visibility

Every component has a Visibility setting on its Appearance tab with three modes:

ModeBehavior
VisibleThe component is always visible in the app.
HiddenThe component is hidden from users but still appears in the builder's hierarchy panel for editing.
ConditionsVisibility is controlled by a condition object. The component only renders when the condition evaluates to true.

The stored shape for conditional visibility wraps the condition one level deeper:

JSON — block visibility with condition

"visibility": { "value": "conditions", "conditions": { "type": "filter", "payload": { "operator": "AND", "filters": [ { "property": "{{ user.role }}", "filter": { "operator": "EQUAL", "value": "admin" } } ] } } }

Note: Per-device visibility: Visibility settings can be different per device breakpoint. You can show a component on desktop and hide it on mobile by configuring the setting separately for each device in the builder.

Editable and Disabled

Interactive components (inputs, buttons, selects) also have an Editable setting (sometimes labeled Disabled or Required) with the same three-way choice:

  • On — the component is always editable.

  • Off — the component is always disabled/read-only.

  • Conditions — editability is driven by a condition. Useful for role-based access or data-driven form control.

Example — enable Save button only when form is valid

"disabled": { "type": "filter", "payload": { "operator": "AND", "filters": [ { "property": "{{ form_create.isValid }}", "filter": { "operator": "EQUAL", "value": "false" } } ] } }

Operator Reference

The condition engine supports a rich set of operators organized by data type. Each operator compares a property (a binding expression) against a value.

Group Operators

OperatorBehavior
ANDAll child conditions must be true
ORAt least one child condition must be true

Equality Operators

OperatorBehavior
EQUALStrict equality (===) with per-type coercion (numbers via Number(), objects via deep equals)
NOT_EQUALStrict inequality
INProperty value is in the provided list
NOT_INProperty value is not in the provided list

Presence Operators

OperatorBehaviorImportant Edge Cases
EXISTSProperty is not null/undefinedEmpty string, 0, false, [], {} all return true
MISSINGProperty is null or undefinedEmpty string, 0, false, [] all return false. MISSING does not detect empty arrays.

Warning: Common trap: MISSING only catches null and undefined. An API returning an empty array [] is NOT missing. Use a nested OR on ['length'] to detect empty arrays — see the recipe below.

Text Operators

OperatorBehavior
CONTAINSString includes value (case-sensitive). Not for arrays — use IN for array membership.
ICONTAINSCase-insensitive CONTAINS
NOT_CONTAINS / NOT_ICONTAINSNegations of the above
STARTS_WITH / NOT_STARTS_WITHString starts with value
ENDS_WITH / NOT_ENDS_WITHString ends with value
REGEX / NOT_REGEXRegular expression match (case-sensitive)
IREGEX / NOT_IREGEXRegular expression match (case-insensitive)
MIN_LENGTHString length is at least n. String-only — arrays return false.
MAX_LENGTHString length is at most n. String-only — arrays return false.

Number Operators

OperatorBehavior
GTGreater than
GTEGreater than or equal to
LTLess than
LTELess than or equal to

Date & Time Operators

OperatorBehavior
DATE_IS_BEFOREDate is strictly before value
DATE_IS_AFTERDate is strictly after value
DATE_IS_ON_OR_BEFOREDate is on or before value
DATE_IS_ON_OR_AFTERDate is on or after value
TIME_IS_BEFORE / TIME_IS_AFTERTime comparison
TIME_IS_EQUAL_TOExact time match
TIME_IS_EQUAL_TO_OR_BEFORE / _AFTERTime comparison with equality

Nesting Condition Groups

Groups can be nested recursively to express complex logic. An OR inside an AND is valid:

JSON — nested AND / OR group

{ "type": "filter", "payload": { "operator": "AND", "filters": [ { "property": "{{ user.isActive }}", "filter": { "operator": "EQUAL", "value": "true" } }, { "operator": "OR", "filters": [ { "property": "{{ user.role }}", "filter": { "operator": "EQUAL", "value": "admin" } }, { "property": "{{ user.role }}", "filter": { "operator": "EQUAL", "value": "manager" } } ] } ] } }

Recipe: Detect an Empty Data Source Result

A data source that returns [] is not MISSING. Use this pattern to detect the "no results" state correctly:

Recipe — show "No results" when data source returns empty array

{ "operator": "AND", "filters": [ { "property": "{{ ds_orders['isLoading'] }}", "filter": { "operator": "EQUAL", "value": "false" } }, { "property": "{{ ds_orders['error'] }}", "filter": { "operator": "MISSING" } }, { "operator": "OR", "filters": [ { "property": "{{ ds_orders['data'] }}", "filter": { "operator": "MISSING" } }, { "property": "{{ ds_orders['data']['length'] }}", "filter": { "operator": "EQUAL", "value": "0" } } ] } ] }

Where Conditions Apply

The same condition object shape is used in three places:

ContextProperty PathShape
Block visibilityblock.visibility{"value": "conditions", "conditions": {type,payload}}
Block disabled stateblock.appearance.disabled{"type": "filter"|"boolean", "payload": ...}
Block loading stateblock.appearance.loading{"type": "filter"|"boolean", "payload": ...}
Form read-onlyblock.readOnly{"type": "filter"|"boolean", "payload": ...}
Action run conditionaction.runCondition{"type": "filter"|"boolean", "payload": ...}
Table cell conditional formattingcolumn.conditions{"type": "filter"|"boolean", "payload": ...}
Data source disableddataSource.disabled{"type": "filter"|"boolean", "payload": ...}

Warning: Do not conflate shapes. The visibility property uses {"value": "conditions", "conditions": {…}}, which is different from disabled and loading, which accept the condition object directly. Using the wrong shape silently does nothing.

Referencing Data in Conditions

The property field in each leaf is a binding expression — any {{ }} expression is valid. This means conditions can reference:

  • Page variables: {{ var_abc['value'] }}

  • Current user: {{ user.role }}, {{ user.email }}

  • Data source output: {{ ds_orders['data']['length'] }}

  • Component state: {{ form_create['isValid'] }}

  • URL parameters: {{ pageInputs['status'] }}

Note: Silent failures: A binding in a condition that fails to resolve is treated as undefined. MISSING will fire; EXISTS will not. A broken binding in a visibility condition makes the block behave as if the condition returned nothing — meaning it may show or hide unexpectedly.

Common Patterns

Role-Based Visibility

Show a component only to users with the admin role:

JSON — admin-only visibility

"visibility": { "value": "conditions", "conditions": { "type": "filter", "payload": { "operator": "AND", "filters": [ { "property": "{{ user.role }}", "filter": { "operator": "EQUAL", "value": "admin" } } ] } } }

Variable-Driven Visibility

Show a panel only when a toggle variable is true:

JSON — show panel when toggle is on

"visibility": { "value": "conditions", "conditions": { "type": "filter", "payload": { "operator": "AND", "filters": [ { "property": "{{ var_showPanel['value'] }}", "filter": { "operator": "EQUAL", "value": "true" } } ] } } }

Gate an Action with runCondition

Only send an email notification when a checkbox is checked:

JSON — action with runCondition

{ "actionType": "controlDataSource", "payload": { "dataSourceId": "ds_sendEmail", "method": "trigger" }, "id": "act_email", "runCondition": { "type": "filter", "payload": { "operator": "AND", "filters": [ { "property": "{{ checkbox_notify['value'] }}", "filter": { "operator": "EQUAL", "value": "true" } } ] } } }

Edge Cases and Gotchas

EQUAL with empty string

EQUAL coerces numbers via Number(). An empty string converted to a number is NaN, which is never equal to anything — so EQUAL will always fail if the property binding resolves to "".

MIN_LENGTH / MAX_LENGTH are string-only

These operators check typeof value !== 'string' first. An array returns false regardless of length. Use EQUAL with the ['length'] suffix instead for arrays.

Empty filter groups

An empty AND group (filters: []) evaluates to true (match all). An empty OR group evaluates to false (match none). Never write empty groups intentionally — always include at least one leaf.

Frequently Asked Questions

What is the difference between Visible, Hidden, and Conditions modes?

Visible always shows the block. Hidden always hides it (but still renders it in the DOM — use this when you want to keep its state intact). Conditions evaluates a filter object at runtime and shows the block only when the result is true. The stored shapes are the values "visible", "hidden", and the object {"value":"conditions","conditions":{...}}.

Can I reference data source results or variables in a visibility condition?

Yes. Condition fields accept binding expressions ({{ ds_user['data']['role'] }}, {{ var_isAdmin['value'] }}). The condition is re-evaluated reactively every time a referenced value changes, so the block shows or hides automatically.

Why should I use EXISTS/MISSING instead of EQUAL to null?

A field can be absent from an object (missing entirely) or explicitly set to null. EQUAL null matches only null, not undefined. MISSING matches both null and undefined — it is the reliable "this field has no value" check.

Can I nest AND/OR groups more than one level deep?

Yes. Each filter entry in a group can itself be an operator: "AND" or operator: "OR" group with its own filters array. There is no enforced depth limit, but conditions are easier to read and debug when kept to two or three levels.

What happens when a condition references a variable that hasn't been set yet?

The referenced value resolves to undefined. Most operators treat undefined as a no-match — so an EQUAL check against an unset variable evaluates to false, hiding the block. If you want the block to be visible by default until the variable is explicitly set to a hiding value, flip the logic: use MISSING to show when the variable is absent.