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:
| Mode | Behavior |
| Visible | The component is always visible in the app. |
| Hidden | The component is hidden from users but still appears in the builder's hierarchy panel for editing. |
| Conditions | Visibility 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
| Operator | Behavior |
| AND | All child conditions must be true |
| OR | At least one child condition must be true |
Equality Operators
| Operator | Behavior |
| EQUAL | Strict equality (===) with per-type coercion (numbers via Number(), objects via deep equals) |
| NOT_EQUAL | Strict inequality |
| IN | Property value is in the provided list |
| NOT_IN | Property value is not in the provided list |
Presence Operators
| Operator | Behavior | Important Edge Cases |
| EXISTS | Property is not null/undefined | Empty string, 0, false, [], {} all return true |
| MISSING | Property is null or undefined | Empty 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
| Operator | Behavior |
| CONTAINS | String includes value (case-sensitive). Not for arrays — use IN for array membership. |
| ICONTAINS | Case-insensitive CONTAINS |
| NOT_CONTAINS / NOT_ICONTAINS | Negations of the above |
| STARTS_WITH / NOT_STARTS_WITH | String starts with value |
| ENDS_WITH / NOT_ENDS_WITH | String ends with value |
| REGEX / NOT_REGEX | Regular expression match (case-sensitive) |
| IREGEX / NOT_IREGEX | Regular expression match (case-insensitive) |
| MIN_LENGTH | String length is at least n. String-only — arrays return false. |
| MAX_LENGTH | String length is at most n. String-only — arrays return false. |
Number Operators
| Operator | Behavior |
| GT | Greater than |
| GTE | Greater than or equal to |
| LT | Less than |
| LTE | Less than or equal to |
Date & Time Operators
| Operator | Behavior |
| DATE_IS_BEFORE | Date is strictly before value |
| DATE_IS_AFTER | Date is strictly after value |
| DATE_IS_ON_OR_BEFORE | Date is on or before value |
| DATE_IS_ON_OR_AFTER | Date is on or after value |
| TIME_IS_BEFORE / TIME_IS_AFTER | Time comparison |
| TIME_IS_EQUAL_TO | Exact time match |
| TIME_IS_EQUAL_TO_OR_BEFORE / _AFTER | Time 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:
| Context | Property Path | Shape |
| Block visibility | block.visibility | {"value": "conditions", "conditions": {type,payload}} |
| Block disabled state | block.appearance.disabled | {"type": "filter"|"boolean", "payload": ...} |
| Block loading state | block.appearance.loading | {"type": "filter"|"boolean", "payload": ...} |
| Form read-only | block.readOnly | {"type": "filter"|"boolean", "payload": ...} |
| Action run condition | action.runCondition | {"type": "filter"|"boolean", "payload": ...} |
| Table cell conditional formatting | column.conditions | {"type": "filter"|"boolean", "payload": ...} |
| Data source disabled | dataSource.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.