# Expression Reference {#expression-reference}


This page is the complete reference for expression syntax: **operators** and
**every verified public function**. It covers the shared grammar used by
[Validation](/user-manual/validation.md), [Skip Logic](/user-manual/skip-logic.md), and
[Calculations](/user-manual/calculation-expressions.md).

If you are just getting started, read [Form Logic Overview](/user-manual/form-logic-overview.md)
first. This page is a reference, not a tutorial.

---


## Literals {#literals}


| Literal | Examples | Notes |
|---|---|---|
| Number | `42`, `-3.14`, `0` | Integer and decimal |
| String | `'hello'`, `"hello"` | Single or double quotes |
| Boolean | `true()`, `false()` | Also accepted bare: `true`, `false` |
| Null | `null` | Empty/missing value |
| Current value | `.` | Only meaningful inside constraints |
| Question reference | `${age}` | Another question's answer |


## Expression operators {#expression-operators}



### Comparison {#comparison}


| Operator | Meaning | Example |
|---|---|---|
| `=` | equals | `${sex} = 'female'` |
| `==` | equals (alias) | `${sex} == 'female'` |
| `!=` | not equals | `${sex} != 'female'` |
| `<` | less than | `${age} < 18` |
| `<=` | less than or equal | `${age} <= 65` |
| `>` | greater than | `${age} > 18` |
| `>=` | greater than or equal | `${age} >= 18` |

There is **no `<>` operator**. Use `!=`.


### Logical {#logical}


| Operator | Meaning | Example |
|---|---|---|
| `and` | both must be true | `${pregnant} = 'yes' and ${age} >= 18` |
| `or` | at least one true | `${district} = 'A' or ${district} = 'B'` |
| `not(...)` | negates | `not(${pregnant} = 'yes')` |
| `!` | negates (web shorthand) | `!${pregnant}` |


### Arithmetic {#arithmetic}


| Operator | Meaning | Example |
|---|---|---|
| `+` | addition | `${q1} + ${q2}` |
| `-` | subtraction | `${total} - ${discount}` |
| `*` | multiplication | `${qty} * ${price}` |
| `div` | division | `${total} div ${count}` |
| `/` | division | `${total} / ${count}` |
| `mod` | remainder | `${items} mod 2` |


### Precedence (highest to lowest) {#precedence-highest-to-lowest}


1. Unary: `-`, `not`, `!`
2. Multiplication / division: `*`, `/`, `div`, `mod`
3. Addition / subtraction: `+`, `-`
4. Comparison: `=`, `!=`, `<`, `>`, `<=`, `>=`
5. Logical AND: `and`
6. Logical OR: `or` (lowest)

**Parentheses override precedence.** For complex expressions, add parentheses
even when not strictly required — they make the intent clear and guard against
precedence mistakes:

```text
(${age} >= 15 and ${age} <= 49) and ${pregnant} = 'yes'
```


### Ternary {#ternary}


MQUAD uses the `if()` function for conditional values — see below. The
`? :` ternary syntax exists **only** in the Flutter expression engine and does
not parse on the web runtime, so it is not part of the supported public
grammar.

---


## Function reference {#function-reference}


All functions listed here are verified in the MQUAD expression engines. The
**Platforms** column shows where each function is implemented:

- **Web** — web designer validation, web form runtime
- **Mobile** — Flutter field app
- **Backend** — survey-service validation whitelist / form-service evaluation

Unless noted, a function is available on all three. Functions marked
*MQUAD extension* are MQUAD-specific additions; the rest are standard
XLSForm/ODK functions.


### Date {#date}


| Function | Arguments | Returns | Example | Notes | Platforms |
|---|---|---|---|---|---|
| `date-diff` | `(unit, start, end)` | integer | `date-diff('months', ${enroll}, ${visit})` | Completed calendar units, signed. *MQUAD extension* | Web, Mobile, Backend |
| `date-add` | `(date, amount, unit)` | string `YYYY-MM-DD` | `date-add(${visit}, 14, 'days')` | EOM clamping. *MQUAD extension* | Web, Mobile, Backend |
| `date-subtract` | `(date, amount, unit)` | string `YYYY-MM-DD` | `date-subtract(${visit}, 14, 'days')` | EOM clamping. *MQUAD extension* | Web, Mobile, Backend |
| `day` | `(date)` | integer 1–31 | `day(${visit_date})` | *MQUAD extension* | Web, Mobile, Backend |
| `month` | `(date)` | integer 1–12 | `month(${visit_date})` | *MQUAD extension* | Web, Mobile, Backend |
| `year` | `(date)` | integer | `year(${visit_date})` | *MQUAD extension* | Web, Mobile, Backend |
| `day-of-week` | `(date)` | integer 1–7 | `day-of-week(${visit_date})` | ISO 1=Mon…7=Sun. *MQUAD extension* | Web, Mobile, Backend |
| `today` | `()` | string | `today()` | Device-local calendar date | Web, Mobile, Backend |
| `now` | `()` | string | `now()` | ISO 8601 datetime | Web, Mobile, Backend |
| `date` | `(value)` or `(y, m, d)` | string | `date('2026-08-14')`, `date(2026, 8, 14)` | Parse or construct | Web, Mobile, Backend |
| `days-between` | `(d1, d2)` | integer | `days-between(${visit_1}, ${visit_2})` | ⚠ **Arg order differs by platform** — see note | Web, Mobile, Backend |
| `duration` | `(end, start, unit?)` | number | `duration(${end}, ${start}, 'hours')` | `end − start`; default `'hours'` | Web, Mobile, Backend |
| `today-offset` | `(days)` | string | `today-offset(-10)` | Date `days` from today | Web, Mobile, Backend |
| `format-date` | `(value, fmt)` | string | `format-date(${visit}, '%d/%m/%Y')` | Web/mobile: `%Y %y %m %d %b %B` (mobile also `%H %M %S`); backend uses full strftime | Web, Mobile, Backend |


### Time {#time}


| Function | Arguments | Returns | Example | Notes | Platforms |
|---|---|---|---|---|---|
| `time-diff` | `(unit, start, end)` | number | `time-diff('hours', ${start}, ${end})` | Sec/min integer, hours/days float. Mixed plain-time + datetime → null. *MQUAD extension* | Web, Mobile, Backend |


### Age {#age}


| Function | Arguments | Returns | Example | Notes | Platforms |
|---|---|---|---|---|---|
| `age` | `(dob, ref)` | integer ≥ 0 | `age(${dob}, ${interview_date})` | Completed years. *MQUAD extension* | Web, Mobile, Backend |
| `age-months` | `(dob, ref)` | integer ≥ 0 | `age-months(${dob}, ${visit})` | Completed months. *MQUAD extension* | Web, Mobile, Backend |
| `age-days` | `(dob, ref)` | integer ≥ 0 | `age-days(${dob}, ${visit})` | Calendar days. *MQUAD extension* | Web, Mobile, Backend |


### Health / pregnancy {#health--pregnancy}


| Function | Arguments | Returns | Example | Notes | Platforms |
|---|---|---|---|---|---|
| `gestational-weeks` | `(lmp, ref)` | integer ≥ 0 | `gestational-weeks(${lmp}, ${visit})` | Weeks since LMP. *MQUAD extension* | Web, Mobile, Backend |
| `gestational-days` | `(lmp, ref)` | integer 0–6 | `gestational-days(${lmp}, ${visit})` | Remainder days. *MQUAD extension* | Web, Mobile, Backend |
| `gestational-total-days` | `(lmp, ref)` | integer ≥ 0 | `gestational-total-days(${lmp}, ${visit})` | Total days. *MQUAD extension* | Web, Mobile, Backend |
| `bmi` | `(weightKg, heightCm)` | number | `bmi(${weight_kg}, ${height_cm})` | Height in **cm**. *MQUAD extension* | Web, Mobile, Backend |


### Numeric {#numeric}


| Function | Arguments | Returns | Example | Notes | Platforms |
|---|---|---|---|---|---|
| `sum` | `(a, b, ...)` | number | `sum(${q1}, ${q2})` | | Web, Mobile, Backend |
| `min` | `(a, b, ...)` | number | `min(${q1}, ${q2})` | | Web, Mobile, Backend |
| `max` | `(a, b, ...)` | number | `max(${q1}, ${q2})` | | Web, Mobile, Backend |
| `round` | `(v, n?)` | number | `round(${result}, 2)` | `n` = decimal places | Web, Mobile, Backend |
| `abs` | `(v)` | number | `abs(${change})` | | Web, Mobile, Backend |
| `int` | `(v)` | integer | `int(${value})` | Truncates | Web, Mobile, Backend |
| `number` | `(v)` | number | `number(${text})` | | Web, Mobile, Backend |
| `pow` | `(base, exp)` | number | `pow(${radius}, 2)` | | Web, Mobile, Backend |
| `floor` | `(v)` | integer | `floor(${value})` | Rounds down | Backend only |
| `ceiling` | `(v)` | integer | `ceiling(${value})` | Rounds up | Backend only |
| `random` | `()` | number 0–1 | `random()` | | Web, Mobile, Backend |


### Text / string {#text--string}


| Function | Arguments | Returns | Example | Notes | Platforms |
|---|---|---|---|---|---|
| `concat` | `(a, b, ...)` | string | `concat(${first}, ' ', ${last})` | | Web, Mobile, Backend |
| `join` | `(sep, a, b, ...)` | string | `join('-', ${state}, ${district})` | | Web, Mobile, Backend |
| `string` | `(v)` | string | `string(${age})` | | Web, Mobile, Backend |
| `string-length` | `(v)` | integer | `string-length(${name})` | | Web, Mobile, Backend |
| `substring` | `(s, start, end?)` | string | `substring(${code}, 0, 3)` | ⚠ Indexing differs by platform | Web, Backend |
| `substr` | `(s, start, length?)` | string | `substr(${code}, 0, 3)` | Mobile name for substring | Mobile |
| `contains` | `(s, sub)` | boolean | `contains(${notes}, 'urgent')` | | Web, Mobile, Backend |
| `starts-with` | `(s, prefix)` | boolean | `starts-with(${id}, 'MH-')` | | Web, Mobile, Backend |
| `ends-with` | `(s, suffix)` | boolean | `ends-with(${email}, '@x.com')` | | Web, Mobile |
| `replace` | `(s, search, repl)` | string | `replace(${text}, '-', '')` | Replaces all occurrences | Web, Mobile |
| `regex` | `(v, pattern)` | boolean | `regex(${phone}, '^[0-9]{10}$')` | | Web, Mobile, Backend |
| `normalize-space` | `(s)` | string | `normalize-space(${text})` | Collapses whitespace | Backend only |
| `translate` | `(s, from, to)` | string | `translate(${text}, 'ae', 'AE')` | Character map | Backend only |


### Selection {#selection}


| Function | Arguments | Returns | Example | Notes | Platforms |
|---|---|---|---|---|---|
| `selected` | `(value, choice)` | boolean | `selected(${symptoms}, 'fever')` | | Web, Mobile, Backend |
| `count-selected` | `(value)` | integer | `count-selected(${symptoms})` | | Web, Mobile, Backend |
| `selected-at` | `(value, index)` | string | `selected-at(${symptoms}, 0)` | 0-based index | Web, Mobile, Backend |
| `count` | `(value)` | integer | `count(${multi})` | | Web, Mobile, Backend |


### Conditional {#conditional}


| Function | Arguments | Returns | Example | Notes | Platforms |
|---|---|---|---|---|---|
| `if` | `(cond, then, else)` | any | `if(${age} >= 18, 'adult', 'child')` | | Web, Mobile, Backend |
| `coalesce` | `(a, b, ...)` | any | `coalesce(${phone_2}, ${phone_1})` | First non-empty | Web, Mobile, Backend |
| `boolean` | `(v)` | boolean | `boolean(${pregnant})` | | Mobile, Backend |
| `true` | `()` | boolean | `true()` | | Web, Mobile, Backend |
| `false` | `()` | boolean | `false()` | | Web, Mobile, Backend |


### Utility {#utility}


| Function | Arguments | Returns | Example | Notes | Platforms |
|---|---|---|---|---|---|
| `uuid` | `()` | string | `uuid()` | Random UUID v4 | Web, Mobile, Backend |
| `once` | `(v)` | any | `once(now())` | First computed value (page lifetime) | Web, Mobile, Backend |
| `position` | `()` | integer | `position()` | Position in repeat | Mobile, Backend |
| `last` | `()` | integer | `last()` | Last position in repeat | Backend only |
| `empty` | `(v)` | boolean | `empty(${notes})` | True when blank | Backend only |
| `pulldata` | `(file, valueField, keyField, keyValue)` | string | `pulldata('regions.csv', 'name', 'code', ${district})` | External CSV lookup | Mobile |

---


## Cross-platform compatibility notes {#cross-platform-compatibility-notes}


The following functions have **different behaviour across platforms**. They are
verified as they stand; use them with care and prefer the recommended
alternative where one is given.


### `days-between` argument order (⚠ significant) {#days-between-argument-order--significant}


- **Web**: `days-between(d1, d2)` computes `d2 − d1` (second minus first).
- **Mobile & backend**: `days-between(d1, d2)` computes `d1 − d2` (first minus
  second).

**Recommendation:** use `date-diff('days', start, end)` instead — it is frozen,
consistent on every platform, and signed.


### `substring` / `substr` indexing {#substring--substr-indexing}


- **Web**: `substring(s, start, end?)` — 0-based `start`; `end` is an *end
  index* (JavaScript semantics).
- **Mobile**: `substr(s, start, length?)` — 0-based `start`; third argument is
  a *length*.
- **Backend**: `substring(s, start, length?)` — XPath-style **1-based** `start`;
  third argument is a *length*.

**Recommendation:** prefer `contains()`, `starts-with()`, and
`string-length()` for cross-platform text logic.


### Ternary `? :` {#ternary--}


Only the Flutter expression engine parses `? :`. Use `if()` everywhere.


### `regex()` anchoring {#regex-anchoring}


The web engine treats `regex(v, pattern)` as a **full-string match** (the
pattern is anchored: `^(?:pattern)$`). The mobile app and backend use a
**partial match** (`hasMatch` / `search`). The designer's validation builder
generates fully anchored patterns (`regex(., '^[0-9]+$')`), which behave the
same everywhere. If you write `regex()` by hand, include the `^...$` anchors so
results are identical on every platform.


### `selected()` return type on web {#selected-return-type-on-web}


The web calculation engine returns the **string** `'true'` / `'false'` from
`selected()` (for parity with ODK), while mobile and backend return real
booleans. In a boolean context (`and`/`or`/`if`), both strings are truthy on
web — so prefer writing `selected(${multi}, 'x')` as a **constraint or
relevance** expression (where it is handled correctly) rather than nesting it
inside an `if()` calculation on web.


### Backend-only functions {#backend-only-functions}


`floor`, `ceiling`, `normalize-space`, `translate`, `last`, `empty`,
`decimal-date-time` exist in the backend registries but not in the web or
mobile runtimes. They will not produce results in a web or mobile calculation —
avoid them in survey logic.


### Mobile-only functions {#mobile-only-functions}


`pulldata` (external CSV lookup), `substr`, `boolean`, `decimal-round`,
`decimal-log10` are mobile-only. `pulldata` requires a bundled CSV file and is
not available on the web runtime.

---


## Design-time vs runtime validation {#design-time-vs-runtime-validation}


The survey designer validates expressions when you save:

- Unknown question references are flagged.
- Unknown function names are flagged against the backend whitelist.
- Unbalanced quotes/parentheses are flagged.
- Circular dependencies between expressions are detected and blocked.
- Skip-logic scope rules (same-group + top-level references) are enforced.

These same expressions are then evaluated at runtime by the web runtime, the
mobile app, and the form-service backend — a "valid at design time" expression
is guaranteed to run, but its *result* depends on the rules above (see
[Blank and Invalid Values](/user-manual/blank-and-invalid-values.md)).


### Client-side vs server-side evaluation (web) {#client-side-vs-server-side-evaluation-web}


On the **web runtime**, constraint (validation) checking while answering uses
an inline checker that supports simple comparisons (`${age} >= 18`, `. >= 5`),
`selected()`, and `and` / `or` / `not()` — it does **not** evaluate
function-based constraints such as `string-length(.)`, `regex(., ...)`, or
`count-selected(...)`. Those are enforced by the server at submission (and by
the mobile app inline). The **mobile app** evaluates all constraints with the
full expression engine.
