Building forms
Rules and calculations
The four rule kinds
| Kind | What it does | Expression meaning |
|---|---|---|
| Calculate | Derives a field's value from other answers. The respondent sees the result but cannot edit it — the formula owns the value. | The formula to compute. Can reference other fields, arithmetic, date math, and lookups. |
| Show | Shows a question only when a condition is true. While hidden the question's answer is cleared and not stored. | The condition that must be true for the question to appear. |
| Require | Makes a question mandatory only when a condition is true. Complements Show — you can require a follow-up only when it is visible. | The condition under which an answer becomes required. |
| Validate | Blocks submission when a condition is true, showing a message you write next to the field that caused the problem. | Write the condition that is WRONG — the submission is blocked whenever this evaluates to true. |
How expressions work
Expressions are trees built from four kinds of node, composed by choosing an operator and filling its inputs. Inputs can themselves be operators, field references, or constants. There is no text to type; the editor only offers operations the engine can evaluate.
| Node type | What it is | Example |
|---|---|---|
| Literal | A constant value you type in. Can be text, a number, true/false, or a list. | "Male", 18, true, ["a","b"] |
| Field | The current answer to a question on this form, addressed by the question's key. | date_of_birth, school_name |
| Cross-form reference | An answer from another form for the same subject (record-type forms only). Resolved before evaluation — the interpreter never touches a database. | registration@latest.district |
| Operation | Application of a built-in operator to one or more inputs. | yearsBetween(date_of_birth, today()) |
What counts as true
Show, Require, and Validate treat the expression result as a boolean:
| Value | Treated as |
|---|---|
true | True |
false, null (blank), "" (empty text), [] (empty list) | False |
0 | True — the number zero is a real answer, not the same as unanswered |
| Any non-empty string | True |
| Any non-empty list | True |
| Any non-zero number | True |
Comparison operators
Comparison operators return true or false. They work on numbers and on date strings — no need to pick a different operator for dates.
| Operator | Inputs | What it checks |
|---|---|---|
eq(a, b) | 2 | a equals b. Works on text, numbers, booleans, and multi-choice lists (order-insensitive). |
neq(a, b) | 2 | a does not equal b. |
gt(a, b) | 2 | a is greater than b. Works on numbers and date strings. |
gte(a, b) | 2 | a is greater than or equal to b. |
lt(a, b) | 2 | a is less than b. |
lte(a, b) | 2 | a is less than or equal to b. |
between(value, low, high) | 3 | value ≥ low and value ≤ high (inclusive on both ends). Works on numbers and dates. |
Logic operators
| Operator | Inputs | What it does |
|---|---|---|
and(a, b, …) | 1 or more | True when every input is true. Takes any number of inputs. |
or(a, b, …) | 1 or more | True when at least one input is true. |
not(a) | 1 | Inverts the truth value of its input. |
if(condition, then, otherwise) | 3 | Returns the "then" value when condition is true, the "otherwise" value when false. Useful in Calculate rules. |
coalesce(a, b, …) | 1 or more | Returns the first input that is not blank. Use to fall back to a default value. |
Presence operators
These check whether a question has any answer at all, regardless of the value.
| Operator | Inputs | What it checks |
|---|---|---|
isFilled(field) | 1 | True when the question has any answer — including the number 0 or the text "false". |
isBlank(field) | 1 | True when the question has no answer — it was skipped or not yet reached. |
Maths operators
All maths operators return blank when any input cannot be converted to a number. Division and remainder by zero return blank rather than Infinity or NaN.
| Operator | Inputs | What it computes |
|---|---|---|
add(a, b, …) | 2 or more | Sum of all inputs. |
sub(a, b) | 2 | a minus b. |
mul(a, b, …) | 2 or more | Product of all inputs. |
div(a, b) | 2 | a divided by b. Returns blank when b is zero. |
mod(a, b) | 2 | Remainder of a ÷ b. Returns blank when b is zero. |
abs(a) | 1 | Absolute (positive) value of a. |
floor(a) | 1 | Round down to the nearest whole number. |
ceil(a) | 1 | Round up to the nearest whole number. |
round(a) or round(a, digits) | 1–2 | Round to the nearest whole number, or to the specified number of decimal places (0–12). |
min(a, b, …) | 1 or more | Smallest of all inputs. |
max(a, b, …) | 1 or more | Largest of all inputs. |
Date operators
Date operators accept YYYY-MM-DDstrings (what date fields store) and full ISO datetime strings. All arithmetic runs in UTC so server recomputation agrees with the browser regardless of the respondent's timezone.
| Operator | Inputs | What it returns |
|---|---|---|
today() | 0 | Today's date as YYYY-MM-DD. On the server, the submission timestamp is used — not the browser clock. |
yearsBetween(from, to) | 2 | Whole years elapsed from → to (how many birthdays have passed). Pair with today() to compute age. |
monthsBetween(from, to) | 2 | Whole calendar months elapsed from → to. |
daysBetween(from, to) | 2 | Full days elapsed from → to. |
addDays(date, n) | 2 | A date n days after date. n may be negative. |
addMonths(date, n) | 2 | A date n months after date. End-of-month clamped: 31 Jan + 1 month = 28 Feb, never 3 Mar. |
formatDate(date, pattern) | 2 | Format a date for display. Supported tokens: YYYY MM DD HH mm. Example: formatDate(dob, "DD/MM/YYYY") |
Text operators
| Operator | Inputs | What it returns |
|---|---|---|
concat(a, b, …) | 1 or more | Joins all inputs as text. Blank values contribute nothing — not the word "null". |
upper(a) | 1 | Converts text to UPPERCASE. |
lower(a) | 1 | Converts text to lowercase. |
trim(a) | 1 | Removes leading and trailing spaces. |
length(a) | 1 | Number of characters in text, or number of selected options in a multi-choice answer. |
contains(a, b) | 2 | Case-insensitive: does text a contain text b? Also works on multi-choice lists: does the selection include option b? |
startsWith(a, b) | 2 | Case-insensitive: does text a start with text b? |
List operators
These work on multi-choice answers (a list of selected values) and on repeating section answers (a list of per-entry sub-form responses).
| Operator | Inputs | What it returns |
|---|---|---|
count(list) | 1 | Number of items in the list. For a multi-choice question: how many options are selected. |
includes(list, value) | 2 | True when the list contains the given value. |
sumOf(list) | 1 | Adds every numeric item in the list. Useful on repeating sections where each entry has a number field. |
anyOf(list) | 1 | True when at least one item in the list is truthy. |
allOf(list) | 1 | True when every item in the list is truthy. True for an empty list (vacuously). |
Lookup: auto-fill from an option list
When your option list carries extra data in its metadata columns — a UDISE code on a school list, a district code on a village list — a Calculate rule can copy that data into a read-only field the instant the respondent picks an option. No second dropdown, no transcription error.
| Operator | Inputs | What it returns |
|---|---|---|
lookup(list, field, column) | 3 | The value stored in column of whichever item from list the respondent picked in field. Returns blank if the question is unanswered, the item is not in the list, or that column has no value for the item. |
Example: lookup("ng-schools", school_name, "udise_code") — the moment a school is selected, its UDISE code fills the read-only field. The second argument (school_name) must be a plain field reference, not a computed expression. This restriction is enforced at publish time.
Cross-form references
A form bound to a record type can read an answer given on another form for the same subject. A follow-up visit form can pre-fill the district from the registration form; a score form can read the previous baseline.
| When value | Which submission is read |
|---|---|
| Latest | The most recent submission of that form for this subject. |
| First | The earliest submission of that form for this subject. |
| Registration | The submission that created the subject record. |
Calculated values are recomputed on the server
The server uses the same compiled rule set stored in the immutable form version, so it reproduces exactly what the respondent saw — no drift between browser and server.
Limits enforced at publish time
The compiler checks the complete rule set when you publish. Rules that would fail at runtime are rejected before any respondent sees the form.
| Limit | Value | Why |
|---|---|---|
| Rules per form | 200 | Keeps the compiled plan manageable and prevents accidental runaway automation. |
| Nodes per expression | 256 | Prevents trees so deeply nested they are unreadable to authors and slow to evaluate. |
| Expression depth | 24 levels | A limit on nesting, not total node count. |
The following are also caught and reported at publish time:
- Unknown operators — an operator name not in the built-in set is rejected. There is no way to inject custom code or eval.
- Unknown field keys — a reference to a question that does not exist (or was deleted after the rule was written) is caught with the offending rule named.
- Dependency cycles— if field A's Calculate rule references B and B's rule references A, publication fails and both fields are named. Detection runs across the whole rule set, not rule by rule.
- Lookup restriction — the second argument to
lookup()must be a bare field reference. An expression there would require multi-pass evaluation, which the engine does not support.
Rule templates
The rule editor offers ready-made starting points. Each template produces a valid rule that compiles as-is; you then swap a field or a constant to match your form.
| Template | Kind | What it builds |
|---|---|---|
| Age from date of birth | Calculate | yearsBetween(date_field, today()). Swap the date field to match your question. |
| Add two answers together | Calculate | Adds two numeric questions. Replace the second operand with another field or a constant. |
| Show when an answer matches | Show | Reveals a question when another has a specific value. Fill in the target value. |
| Show once something is answered | Show | Reveals a follow-up the moment an earlier question is filled in. |
| Require a follow-up | Require | Makes a question mandatory once another has any answer. |
| Reject a value outside a range | Validate | not(between(field, 0, 100)) — blocks submission when a number falls outside the allowed band. |
Rules vs. conditional logic
The builder has two ways to make questions conditional: Conditional Logic (the simpler panel) and Rules (this page). Both can show or hide questions, but they are not duplicates.
| Capability | Conditional logic | Rules |
|---|---|---|
| Show / hide a question | Yes | Yes — Show kind |
| Skip to a page | Yes | No |
| Calculated fields | No | Yes — Calculate kind |
| Conditional requirement | No | Yes — Require kind |
| Cross-field validation | No | Yes — Validate kind |
| Date arithmetic | No | Yes |
| Option list lookups | No | Yes |
| Cross-form references | No | Yes |
| Nesting depth | One condition | Up to 24 levels |
Use Conditional Logic for simple “if the answer is X, show Y” cases and for page-jump navigation. Use Rules when you need calculations, nested conditions, date arithmetic, lookups, or cross-form data.