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, Skip Logic, and Calculations.
If you are just getting started, read Form Logic Overview first. This page is a reference, not a tutorial.
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
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
| 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
| 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)
- Unary:
-,not,! - Multiplication / division:
*,/,div,mod - Addition / subtraction:
+,- - Comparison:
=,!=,<,>,<=,>= - Logical AND:
and - 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:
(${age} >= 15 and ${age} <= 49) and ${pregnant} = 'yes'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
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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
| 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
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)
- Web:
days-between(d1, d2)computesd2 − d1(second minus first). - Mobile & backend:
days-between(d1, d2)computesd1 − d2(first minus second).
Recommendation: use date-diff('days', start, end) instead — it is frozen,
consistent on every platform, and signed.
substring / substr indexing
- Web:
substring(s, start, end?)— 0-basedstart;endis an end index (JavaScript semantics). - Mobile:
substr(s, start, length?)— 0-basedstart; third argument is a length. - Backend:
substring(s, start, length?)— XPath-style 1-basedstart; third argument is a length.
Recommendation: prefer contains(), starts-with(), and
string-length() for cross-platform text logic.
Ternary ? :
Only the Flutter expression engine parses ? :. Use if() everywhere.
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
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
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
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
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).
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.
