---
title: "Virtual Conditions"
url: https://flexie.io/resources/dynamic-endpoints/virtual-conditions
description: "The decision type that branches on data which is not a stored field, the values that arrived with the request or that earlier steps produced."
---

# Virtual Conditions

Last updated 23 May 2026

![A virtual condition decision with a left token, an operator, and a right value, branching the workflow into Yes and No paths](https://flexie.io/image/resources/dynamic-endpoints-virtual-conditions.png)

A virtual condition is one of the four workflow [decision types](https://flexie.io/resources/workflows/actions-and-decisions). It is the one you use in dynamic-endpoint (and other virtual-entity) workflows, because there are no stored record fields to filter on, only the data on the `__data` notepad and the incoming request body.

## What it does

A virtual condition compares **a value you specify** against **another value**, using an operator, and routes the record to the **Yes** or **No** branch. The values are written as [Flexie Scripting](https://flexie.io/resources/flexie-scripting/overview) tokens, so they are worked out from the live data at the moment the workflow runs.

> "If the incoming `invoice_amount` is greater than 1000, go down the Yes branch."

```
{{ __data.invoice_amount }}   greater than   1000   →  Yes / No
        (left value)          (operator)    (right value)

```

The **left** value is usually a token pointing at incoming data (`{{ __data.invoice_amount }}`, `{{ __data.webhook.status }}`); the **right** value is what you are comparing against, a fixed value or another token.

## The full operator catalogue

| Operator                             | True when…                                         |
| ------------------------------------ | -------------------------------------------------- |
| equal / not\_equal                   | The two values match / do not match                |
| greater / greater\_or\_equal         | Left is larger (or equal)                          |
| less / less\_or\_equal               | Left is smaller (or equal)                         |
| greater\_than\_now / less\_than\_now | A date value is after / before the current moment  |
| between                              | Left falls within a range (you give two endpoints) |
| contains / not\_contains             | Text includes / excludes a substring               |
| begins\_with / not\_begins\_with     | Text starts / does not start with a value          |
| ends\_with / not\_ends\_with         | Text ends / does not end with a value              |
| is\_empty (also empty)               | The value is blank or null                         |
| is\_not\_empty (also not\_empty)     | The value is present                               |

> **Two names for the same thing.** `is_empty` and `empty` are accepted as the same operator at runtime, as are `is_not_empty` and `not_empty`. The builder lists the short form; AI-generated definitions sometimes use the long form. Both work.

### How values are compared, important rules

* **Numbers are compared as numbers.** `10000` is greater than `5000`. Flexie does **not** compare numeric strings character-by-character (which would wrongly make `"10000"` less than `"5000"` because `'1' < '5'`).
* **Dates are compared as dates.** A date string like `2026-05-23` is recognised and compared chronologically. Different date formats normalise to the same moment.
* **Text is compared case-sensitively.** `"Ping"` does not equal `"ping"`. If you need case-insensitive comparison, lower-case both sides with the `lower` filter: `{{ __data.type | lower }}` vs `"ping"`.
* **Tokens are resolved before comparison.** The values inside `{{ … }}` are computed first, so you can put any [Flexie Scripting](https://flexie.io/resources/flexie-scripting/overview) expression in either operand, including functions and filters.

## Building one

In the workflow builder, add a **decision** and choose the **virtual condition** type. Then:

1. **Left value:** the token to evaluate, e.g. `{{ __data.invoice_amount }}` or `{{ __data.webhook.status_code }}`.
2. **Operator:** from the list above.
3. **Right value:** what to compare against (a literal like `1000`, or another token).
4. Connect the steps for **Yes** and the steps for **No** to the matching branches.

> Remember the [tree rules](https://flexie.io/resources/workflows/runtime-parallel-and-tree): a step attached to neither branch never runs.

## Reading the data correctly

Because the left value is almost always incoming data, the [Receiving data](https://flexie.io/resources/dynamic-endpoints/receiving-data) rules apply:

* Body fields are under `__data`, `{{ __data.invoice_amount }}`, not bare `{{ invoice_amount }}`.
* Headers are under `{{ __data.__headers.* }}`.
* Values your earlier steps stored are also under `{{ __data.* }}`.

A condition that is always taking the "No" branch is, nine times out of ten, reading the value from the wrong place (most often missing the `__data.` prefix on a body field). Check this first.

## A note for advanced or AI-built workflows

When a workflow is built by description (via Flexie AI) or imported, a virtual condition is stored with a fixed internal structure: a single rule whose comparison operands are nested together (the value-to-check and the value-to-compare-against), with the operator. The visual builder produces this structure for you, you only ever set the left value, operator, and right value. If you are constructing definitions directly, keep the operands nested as the builder does; a bare scalar in place of the operand pair will not evaluate.

## Next steps

* [Responding to the caller](https://flexie.io/resources/dynamic-endpoints/responding): replying based on the branch you took.
* [End-to-end examples](https://flexie.io/resources/dynamic-endpoints/examples): conditions in a full workflow.
