> For the complete documentation index, see [llms.txt](https://flapjax.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://flapjax.gitbook.io/docs/core-concepts/rules-and-operators.md).

# Rules & Operators

**Audience:** everyone who builds filters, segments, smart labels, list membership rules or automation conditions. This page covers every comparison operator, per data type, with examples, plus how rules combine.

One rule engine runs everywhere in Flapjax. People and Stacks filters, [segments](/docs/your-data/segments.md), [smart labels](/docs/your-data/labels/smart-labels.md), [list membership rules](/docs/your-data/lists/list-settings.md#membership-rules), automation triggers, branches and Find actions all share it. Learn it once and use it everywhere.

***

## 1. Anatomy of a rule

Every rule is three parts:

> **Field** (what to look at) · **Operator** (how to compare) · **Value** (what to compare against)

Two examples: `Country` **is** `DE`, and `Total deposits` **greater than** `1000`.

Two shape variations:

* **Some operators need no value.** *Is empty*, *is not empty*, *is true* and *is false* stand alone.
* ***Between*****&#x20;and&#x20;*****not between*****&#x20;take two values**, a from and a to. The range **includes both ends**. Enter them backwards and the platform still reads the range you meant.

The field's **data type** decides which operators you get. A date field offers *is on or after*, a text field offers *contains*. The full type-by-type catalogue sits below.

## 2. Combining rules: groups, AND/OR

Rules live in **groups**:

* **Inside a group**, rules combine with **AND**, where all must match, or **OR**, where any may. One choice per group.
* **Multiple groups always combine with AND.** A person or record must satisfy *every* group.
* Groups stay flat. No group nests inside another.

That two-level shape covers most logic. For "(A or B) and (C or D)", make group 1 `A OR B` and group 2 `C OR D`.

**Example: a re-engagement audience**

* Group 1 (OR): `Engagement tier` **is** `Low` OR `Last active` **is before** `30 days ago`
* Group 2 (AND): `Email consent` **is true** AND `Email` **is not empty**

That matches people who are *either* low-engagement *or* inactive, and only where you can email them.

***

## 3. Operators by data type

### Text

Text comparisons ignore case, so `is "VIP"` matches `vip`, `Vip` and `VIP` alike.

| Operator                | Matches when the value…                  | Example (`City`)                                                        |
| ----------------------- | ---------------------------------------- | ----------------------------------------------------------------------- |
| **is**                  | equals your text exactly (ignoring case) | *is* `Berlin` matches "Berlin" and "berlin", and never "Berlin East"    |
| **is not**              | is anything other than your text         | `Plan` *is not* `Free` → paying customers only                          |
| **contains**            | has your text anywhere inside it         | *contains* `berg` → "Nuremberg", "Bergen"                               |
| **does not contain**    | never has your text inside it            | *does not contain* `test` → filters out "test-account"                  |
| **begins with**         | starts with your text                    | *begins with* `New` → "New York", "Newport"                             |
| **does not begin with** | starts with anything else                | `External ID` *does not begin with* `test-` → hide seeded test accounts |
| **ends with**           | ends with your text                      | `Email` *ends with* `@gmail.com`                                        |
| **does not end with**   | ends with anything else                  | `Email` *does not end with* `@mycompany.com` → exclude your own staff   |
| **is empty**            | is missing **or blank**                  | people with no city recorded                                            |
| **is not empty**        | has any text at all                      | `Referral code` *is not empty* → everyone who came through a referral   |

### Number (integer & decimal)

| Operator                  | Matches when the number…                             | Example (`Total deposits`)                                                       |
| ------------------------- | ---------------------------------------------------- | -------------------------------------------------------------------------------- |
| **is**                    | equals the value                                     | *is* `0`                                                                         |
| **is not**                | is anything else                                     | `Login count` *is not* `0` → has logged in at least once                         |
| **greater than**          | is strictly above                                    | *greater than* `1000` → 1000 does **not** match                                  |
| **greater than or equal** | is above **or exactly** the value                    | *≥* `1000` → 1000 matches                                                        |
| **less than**             | is strictly below                                    | *less than* `18` on an age field → minors, blocked from gambling content         |
| **less than or equal**    | is below **or exactly** the value                    | `Loyalty points` *≤* `99` → everyone below the 100-point reward tier             |
| **between**               | falls in the range, **ends included**                | *between* `100` and `500` → 100 and 500 both match                               |
| **not between**           | falls outside the range                              | `Order total` *not between* `10` and `1000` → suspiciously small or large orders |
| **is empty**              | was never set, note that **0 is a value, not empty** | someone with `Total deposits = 0` is *not empty*                                 |
| **is not empty**          | has any number, including 0                          | `Deposit total` *is not empty* → the field has been tracked for this person      |

### True / False

| Operator                        | Matches when                                      |
| ------------------------------- | ------------------------------------------------- |
| **is true**                     | the field is true, e.g. `Email consent` *is true* |
| **is false**                    | the field is false                                |
| **is empty** / **is not empty** | the field was never set / has either value        |

An unset boolean is neither true nor false. Use *is empty* to find those.

### Date & Date-Time

A date rule has two parts: the **operator**, which sets the direction of the comparison, and the **kind of value** you compare against.

**First, the older and newer intuition.** A date further in the past is *earlier*, which makes the person or record **older**. A date closer to today, or in the future, is *later*, which makes them **newer**.

* "**Older than** March 1" means the date falls ***before*** March 1.
* "**Newer than** March 1", or since March 1, means the date falls ***after*** March 1, or ***on or after*** it.

**Operators.** The example field below is `Signup date`, compared against March 1.

| Operator                        | Matches signups…                            | Mar 1 itself? | In plain words                                   |
| ------------------------------- | ------------------------------------------- | ------------- | ------------------------------------------------ |
| **is**                          | on that calendar day                        | ✔ matches     | "signed up *that day*"                           |
| **is not**                      | on any other day                            | ✘             | "any day *except* that one"                      |
| **is after**                    | strictly later                              | ✘ excluded    | "signed up *later than* Mar 1", newer accounts   |
| **is on or after**              | that day **or** later                       | ✔ included    | "signed up *Mar 1 or since*"                     |
| **is before**                   | strictly earlier                            | ✘ excluded    | "signed up *earlier than* Mar 1", older accounts |
| **is on or before**             | that day **or** earlier                     | ✔ included    | "signed up *by* Mar 1 at the latest"             |
| **between**                     | from day A through day B, **both included** | ✔             | "signed up *during* that window"                 |
| **not between**                 | outside that span                           |               | "*not during* that window"                       |
| **is empty** / **is not empty** | date never set / set to anything            |               |                                                  |

Each pair differs on one point: whether the boundary day counts. *After* against *on or after*, *before* against *on or before*. Say it out loud when in doubt. "Signed up **on or after** March 1" plainly includes March 1.

**Real-life examples, one per operator:**

* **is.** A birthday campaign matches `Date of birth` where *day and month* **is** today's. On a stack, `Delivery date` **is** `2026-08-15` shows that day's deliveries.
* **is not.** Exclude a bad data day with `Created` **is not** `2026-07-03`, the day a faulty feed double-imported.
* **is after.** Post-launch signups alone: `Signup date` **is after** `2026-06-30` finds people who joined strictly *after* the June 30 launch day closed, meaning July 1 onward. Launch-day signups stay out.
* **is on or after.** "Everyone since the new terms took effect" becomes `Signup date` **is on or after** `2026-07-01`, which includes people who joined *on* July 1. It is also the standard "recently active" shape with a rolling value: `Last active` **is on or after** `30 days ago`.
* **is before.** Accounts *older than* a cutoff: `Signup date` **is before** `2025-01-01` finds everyone who joined in 2024 or earlier. Use it for a loyalty reward to long-standing customers, or as `Last order date` **is before** `2026-01-01` for "has not ordered this year".
* **is on or before.** Deadline semantics: `Documents submitted` **is on or before** `2026-07-31` means submitted *by the end of July*, with deadline day counting. A rolling value gives the standard inactivity shape: `Last active` **is on or before** `90 days ago` means dormant for at least three months, since their last activity is *that old or older*.
* **between.** A campaign window: `Signup date` **between** `2026-06-01` and `2026-06-30` gives the June cohort, first and last day included. It suits measuring one promotion's intake.
* **not between.** Everyone *outside* the promo window, say to compare organic signups against the campaign cohort.
* **is empty.** Data hygiene: `Date of birth` **is empty** finds people you can neither age-verify nor send birthday offers to.
* **is not empty.** `First deposit date` **is not empty** finds everyone who ever deposited, whenever it happened.

*Before* and *on or before* with a rolling "N days ago" value read backwards at first glance. `Last active` **is on or before** `90 days ago` means the *last* activity is at least 90 days old, so an **inactive** person. `Last active` **is on or after** `30 days ago` means activity inside the last month, so an **active** person.

One shorthand: before means older, which means quieter. After means newer, which means fresher.

Comparing against **an exact date** works at **day precision**, date-time fields included. `Last active` *is* `2026-03-01` matches activity at any time that day. Pick an **exact date and time** value where the hour matters.

**Kinds of value.** The value side of a date rule can be:

* **An exact date**, or an exact date and time. A fixed point, picked from the calendar.
* **A relative distance**: *N days ago*, *N days from now*, and the finer *N hours or minutes ago and from now*. These move with the clock, which keeps segments and smart labels current. `Last active` **is on or before** `30 days ago` is a rolling "inactive for a month" rule that stays true day after day, with no editing.
* **A preset period**: *yesterday*, *now*, *tomorrow*, *last week*, *next week*, *last 30 days*, *next 30 days*.
* **A date part**, matching the **weekday**, **day of month**, **month**, **year** or **time of day** alone. `Birthday` where *month* **is** `December`, or `Created` where *weekday* **is** `Saturday`.

**Choosing an anniversary window:** `Signup date` *is on or after* `365 days ago` AND *is on or before* `335 days ago` finds people whose first year arrives within the next month. It rolls daily.

### Hour

Hour fields, holding 0 to 23, use the same comparison set as dates at hour precision: *is*, *is not*, *is after*, *is on or after*, *is on or before*, *is before*, *between* and *not between*. For example, `Preferred contact hour` *between* `9` and `17`.

### Single choice & choice-like fields

Single-choice select attributes compare by picking from their option list. So do the built-in choice-like fields: **country, currency, language, source type, engagement tier, communication frequency, channel, related person and related record**.

| Operator                        | Matches when                                                 | Example                                                                                      |
| ------------------------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------- |
| **is**                          | the field holds exactly that option                          | `Country` *is* `Germany`, or `Source type` *is* `Import`                                     |
| **is not**                      | the field holds any other option (or, for most fields, none) | `Source type` *is not* `Import` → exclude bulk-loaded contacts from a re-permission campaign |
| **is empty** / **is not empty** | no option chosen / any option chosen                         | `Assigned account manager` *is empty* → unassigned people                                    |

To match several options, put several *is* rules in one **OR** group: `Country` *is* `DE` OR *is* `AT` OR *is* `CH`.

On **related person** and **related record** fields, *is* matches a specific linked target, such as "orders whose `Customer` is this person". The rule matches the **link itself** and reaches no further into the linked entity's own fields. See [Relationships → traversal limits](/docs/core-concepts/relationships.md#5-traversing-relationships-in-filters--actions--and-its-limits).

### Multiple choice & sets

Multi-select attributes, **labels** and **related people** fields hold a *set* of values, so their operators cover membership.

| Operator             | Matches when the set…                            | Example                                                           |
| -------------------- | ------------------------------------------------ | ----------------------------------------------------------------- |
| **contains**         | includes that option (others may be present too) | `Labels` *contains* `VIP`, whatever other labels the person holds |
| **does not contain** | does not include it                              | `Interests` *does not contain* `Poker`                            |
| **is empty**         | has no values at all                             | people with no labels                                             |
| **is not empty**     | has at least one value                           | `Labels` *is not empty* → people already triaged by your team     |

"Has both A and B" means two *contains* rules in an **AND** group. "Has A or B" means the same rules in an **OR** group.

### A note on encrypted fields

With PII encryption turned on, the built-in **email** and **mobile number** fields offer **equality-style operators alone**: *is*, *is not*, *is empty* and *is not empty*. Substring matching such as *contains* and *ends with* cannot run over encrypted values. With encryption off they behave as ordinary text. See [Data Types → special types](/docs/core-concepts/data-types.md#6-special-types).

***

## 4. What "empty" means, precisely

*Is empty* adapts to the type, which is worth knowing when you lean on it.

| Type                                              | Empty means                             |
| ------------------------------------------------- | --------------------------------------- |
| Text                                              | never set, **or** set to a blank string |
| Number                                            | never set, **0 counts as a value**      |
| Date                                              | never set                               |
| Boolean                                           | never set, neither true nor false       |
| Multi-value (multiselect, labels, related people) | never set, **or** an empty set          |

**Blank values in numeric comparisons.** A numeric field can hold a blank value, say an attribute created and never populated, or a rule saved with an empty threshold. The comparison then evaluates to **false** rather than erroring, exactly as a missing value does.

A rule reading `Total deposits is less than 200` matches nobody without a deposit figure. Add an explicit *is empty* rule to include them.

**Dates stored as text.** A date that arrives already formatted, such as a value carried through an automation's context, compares directly as a timestamp. Flapjax never re-reads it as a relative keyword such as *days ago*.

***

## 5. Putting it together: worked setups

**A "high-value, reachable" segment**

* Group 1 (AND): `Total deposits` *greater than or equal* `1000` AND `Last active` *is on or after* `14 days ago`
* Group 2 (OR): `Email consent` *is true* OR `SMS consent` *is true*

Active big spenders you may contact, on either channel.

**A data-quality filter for cleanup**

* Group 1 (OR): `Email` *is empty* OR `Language` *is empty* OR `Country` *is empty*

Anyone missing a critical field. Pair it with a list or an export for the fix-up work.

**A weekend-signup cohort in a record stack**

* Group 1 (AND): `Created` *weekday* **is** `Saturday`. For a fixed campaign window instead, use `Created` *between* `2026-06-01` and `2026-06-30`, both days included.

**Where each setup lives.** Build these as saved segments on People or Stacks, as a smart label's assign and remove rules, as a list's membership rules, or inline as an automation branch. The rule builder is the same in each place. Inside automations, values can also come from [variables](/docs/automations/actions.md#variables) resolved per run.
