> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getflowpilot.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Conditions and expressions

> The condition operators for dynamic values, the property-value model, and the limited expression subset the runtime evaluates.

FlowPilot has three small evaluation systems: **conditions** (used by dynamic values), **property values** (static or conditional), and **expressions** (the `{{ }}` interpolation and the `assign` action). Each is deliberately limited. This page documents exactly what is supported so you do not write something the runtime silently drops.

## Conditions

A `Condition` is a pure, read-only test against a variable. It never mutates state and must be deterministic. Conditions read variables only, through a `VarRef`:

```json theme={null}
{ "var": "user_type" }
```

Each operator names a `left` (a `VarRef`) and, for most operators, a `right` literal.

### Operators by type

| Type    | Operators                                                                                                  |
| ------- | ---------------------------------------------------------------------------------------------------------- |
| String  | `equals`, `not_equals`, `contains`, `not_contains`, `starts_with`, `ends_with`, `is_empty`, `is_not_empty` |
| Number  | `equals`, `not_equals`, `greater_than`, `less_than`, `greater_than_or_equals`, `less_than_or_equals`       |
| Boolean | `equals`, `not_equals` (shown as "is ON" / "is OFF" in the editor)                                         |
| List    | `contains`, `not_contains`, `is_empty`, `is_not_empty` (operates on array elements), `is_full`             |

The empty-check operators (`is_empty`, `is_not_empty`) take no `right` operand.

### `is_full` — a list's selection limit

`is_full` is `true` when a **List** variable has reached the **Selection limit**
(`maxSelections`) declared on the variable itself. It takes no `right` operand:
the limit is never repeated in the condition, so re-capping a question is a
one-number edit on the variable and the conditions can't drift out of sync
with it.

A list with no limit is never full, so `is_full` is inert — always `false` —
until a limit is declared. It is `false` for every non-list type.

This is what greys out the remaining answers of a capped multi-select question.
The editor compiles this onto each option automatically when you set a
**Max selections** — you rarely write it by hand:

```json theme={null}
{
  "op": "and",
  "conditions": [
    { "op": "is_full", "left": { "var": "goal" } },
    { "op": "not_contains", "left": { "var": "goal" }, "right": "option_3" }
  ]
}
```

Both halves matter: without the second, a full question would freeze the user's
own picks and strand them at the limit with no way back. See
[Limiting how many answers can be picked](/editor/variables#limiting-how-many-answers-can-be-picked).

### Logical operators

Compose conditions with:

| Operator | Shape                                    | Notes          |
| -------- | ---------------------------------------- | -------------- |
| `and`    | `{ op: "and", conditions: Condition[] }` | All must hold. |
| `or`     | `{ op: "or", conditions: Condition[] }`  | Any must hold. |
| `not`    | `{ op: "not", condition: Condition }`    | Negate.        |

### Shape examples

```json theme={null}
{ "op": "equals", "left": { "var": "user_type" }, "right": "premium" }
```

```json theme={null}
{
  "op": "and",
  "conditions": [
    { "op": "greater_than_or_equals", "left": { "var": "age" }, "right": 18 },
    { "op": "equals", "left": { "var": "agreed" }, "right": true }
  ]
}
```

### Evaluation notes

* A type mismatch fails the comparison rather than throwing. A numeric operator on a non-number returns `false`. A `starts_with`/`ends_with`/`contains` on a non-string returns `false` (for `not_contains`, an absent/non-string value returns `true`).
* A missing variable is treated as empty: `is_empty` is `true`, `is_not_empty` is `false`.
* An unknown operator fails safely to `false`.
* The dashboard renderer (`variable-runtime.ts`) and both SDKs (each ships a `ConditionEvaluator`, kept in lockstep) implement the same operator set.

## Property values

Component props can be a plain value or a `PropertyValue<T>`:

```ts theme={null}
type PropertyValue<T> =
  | { type: "static"; value: T }
  | { type: "conditional"; cases: ConditionalCase<T>[]; else?: PropertyValue<T> };
```

Resolution rules:

1. `static`: return the value directly.
2. `conditional`: evaluate `cases` in order. The first case whose `when` condition is true wins.
3. If no case matches, fall back to `else`.
4. If nothing matches and there is no `else`, the result is undefined (the renderer uses its own default).

A `ConditionalCase<T>` is `{ when: Condition; value: PropertyValue<T>; label?: string }`. Case values can themselves be conditional, so they nest.

Example: a color that depends on a variable.

```json theme={null}
{
  "type": "conditional",
  "cases": [
    { "when": { "op": "equals", "left": { "var": "user_type" }, "right": "premium" },
      "value": { "type": "static", "value": "#FFD700" } }
  ],
  "else": { "type": "static", "value": "#808080" }
}
```

## Interpolation: `{{ }}`

String props support `{{variableName}}` interpolation. The braces are replaced with the variable's current value, converted to a string.

* Only the variable's value is substituted. The braces hold a bare variable name, nothing else.
* A missing variable resolves to an empty string.
* Interpolation re-runs when the variable changes.

<Warning>
  `{{ }}` interpolation does string substitution only. There is no ternary or conditional syntax inside `{{ }}`. To switch a value based on a condition, use a conditional property value (case/else against a condition) instead.
</Warning>

To switch a value on a condition, use a conditional `PropertyValue` (shown above), not an expression inside `{{ }}`.

## Expressions (the `assign` subset)

The `assign` action and node use a `FlowExpression`: a string in a small "JS-like" safe subset. The SDKs' `ExpressionEvaluator` (the iOS and Expo implementations are kept in lockstep) defines the supported contract:

**Supported**

* Literals: `true`, `false`, `null`, numbers (decimal, negative, leading dot), and quoted strings (`"..."` or `'...'`).
* Variable references: a bare identifier (`userName`) or a `vars.` prefix (`vars.userName`).
* A single binary arithmetic operator (`+`, `-`, `*`, `/`, `%`) when both operands resolve to numbers.
* String concatenation with `+` when either operand resolves to a string (the other operand is coerced).

**Not supported**

* Parentheses.
* Comparison operators (`>`, `<`, `==`, and so on). Use a [condition](#conditions) for comparisons.
* Function calls.
* Multi-operator chains (for example `a + b + c`). Only one top-level operator is parsed.

**Failure handling**

An expression that cannot be evaluated returns nothing, and the runtime treats that as "skip this assignment". The variable is left unchanged. A misconfigured expression never writes an empty, zero, or false value by accident. Division or modulo by zero also skips. Use a [condition](#conditions) and a `setVariable` action when you need comparisons or branching, since expressions cannot compare.

### Examples

```text theme={null}
vars.goalWeight - vars.currentWeight   // number subtraction
"Hi, " + vars.userName                 // string concat
vars.count + 1                          // increment via expression
(vars.a + vars.b) * 2                   // NOT supported: parentheses
vars.age > 30                           // NOT supported: comparison, use a condition
```

## Related pages

* [Dynamic values (editor)](/editor/dynamic-values)
* [Variables (editor)](/editor/variables)
* [Actions reference](/reference/actions-reference)
