Form Logic¶
ODK Collect supports a wide range of dynamic form behavior. This document covers how to specify this behavior in your XLSForm definition.
See also
Warning
Relevance, constraint and calculation evaluation within the same screen is supported in Collect v1.22 and later. In earlier versions of Collect, questions tied by logic must be displayed on different screens.
Form logic building blocks¶
Variables¶
Variables reference the value of previously answered questions. To use a variable, put the question's name in curly brackets preceded by a dollar sign:
${question-name}
Variables can be used in label, hint, and repeat_count columns, as well as any column that accepts an expression.


XLSForm
type | name | label |
---|---|---|
text | your_name | What is your name? |
note | hello_name | Hello, ${your_name}. |
Expressions¶
An expression, sometimes called a formula, is evaluated dynamically as a form is filled out. It can include XPath functions, operators, values from previous responses, and (in some cases) the value of the current response.
Example expressions
- ${bill_amount} * 0.18
- Multiplies the previous value bill_amount by 18%, to calculate a suitable tip.
- concat(${first_name}, ' ', ${last_name})
- Concatenates two previous responses with a space between them into a single string.
- ${age} >= 18
- Evaluates to
True
orFalse
, depending on the value of age. - round(${bill_amount} * ${tip_percent} * 0.01, 2)
- Calculates a tip amount based on two previously entered values, and then rounds the result to two decimal places.
Expressions are used in:
Calculations¶
To evaluate complex expressions, use a calculate row. Put the expression to be evaluated in the calculation column. Then, you can refer to the calculated value using the calculate row's name.
Expressions cannot be used in label and hint columns, so if you want to display calculated values to the user, you must first use a calculate row and then a variable.


XLSForm
type | name | label | calculation |
---|---|---|---|
decimal | bill_amount | Bill amount: | |
calculate | tip_18 | round((${bill_amount} * 0.18),2) | |
calculate | tip_18_total | ${bill_amount} + ${tip_18} | |
note | tip_18_note | Bill: $${bill_amount}
Tip (18%): $${tip_18}
Total: $${tip_18_total}
|
Form logic gotchas¶
When expressions are evaluated¶
Every expression is constantly re-evaluated as an enumerator progresses through a form. This is an important mental model to have and can explain sometimes unexpected behavior. More specifically, expressions are re-evaluated when:
- a form is opened
- the value of any question in the form changes
- a repeat group is added or deleted
- a form is saved or finalized
This is particularly important to remember when using functions that access state outside of the form such as random()
or now()
. The value they represent will change over and over again as an enumerator fills out a form.
The once()
function prevents multiple evaluation by only evaluating the expression passed into it if the node has no value. That means the expression will be evaluated once either on form open or when any values the expression depends on are set.
Every call on now()
in the form will have the same value unless the once()
function is used. For example, the following calculate will keep track of the first time the form was opened:
type | name | label | calculation |
---|---|---|---|
calculate | datetime_first_opened | once(now()) |
The following calculate will keep track of the first time the enumerator set a value for the age question:
type | name | label | calculation |
---|---|---|---|
integer | age | What is your age? | |
calculate | age_timestamp | if(${age} = '', '', once(now())) |
Empty values in math¶
Unanswered number questions are nil.
That is, they have no value.
When a variable referencing an empty value is used
in a math operator
or function,
it is treated as Not a Number (NaN
).
The empty value will not be converted to zero.
The result of a calculation including NaN
will also be NaN
,
which may not be the behavior you want or expect.
To convert empty values to zero,
use either the coalesce()
function
or the if()
function.
coalesce(${potentially_empty_value}, 0)
if(${potentially_empty_value}="", 0, ${potentially_empty_value})
Requiring responses¶
By default, users are able to skip questions in a form. To make a question required, put yes in the required column.
Required questions are marked with a small asterisk to the left of the question label. You can optionally include a required_message which will be displayed to the user who tries to advance the form without answering the question.


XLSForm
type | name | label | required | required_message |
---|---|---|---|---|
text | name | What is your name? | yes | Please answer the question. |
Setting default responses¶
To provide a default response to a question, put the response value in the default column.
Default values must be static values, not expressions or variables.
Note
The content of the default row in a question is taken literally as the default value. Quotes should not be used to wrap string values, unless you actually want those quote marks to appear in the default response value.
XLSForm
type | name | label | default |
---|---|---|---|
select_one contacts | contact_method | How should we contact you? | phone_call |
list_name | name | label |
---|---|---|
contacts | phone_call | Phone call |
contacts | text_message | Text message |
contacts |
Tip
You may want to use a previously entered value as a default, but the default column does not accept dynamic values.
To work around this, use the calculation column instead,
and wrap your default value expression in a once()
function.
XLSForm
type | name | label | calculation |
---|---|---|---|
text | name | Child's name | |
integer | current_age | Child's age | |
select_one gndr | gender | Gender | |
integer | malaria_age | Age at malaria diagnosis | once(${current_age}) |
This solution has some limitations, though.
The value of the calculated default will get set to the first value that the earlier question receives, even if it is changed before viewing the later question.
Example: In the above form, if you enter
8
on current_age, then advance to gender, then back up and change current_age to10
, when you get to malaria_age, the default value will be8
.If the first earlier question has a value, the dependent question will also have a value —
once()
will evaluate anytime the question's value is blank.Example: In the above form, if you enter
8
on current_age and then delete the value8
when you get to malaria_age (intending to leave it blank) the8
value will come back as the answer when you advance. (In this case, using a blank value to indicate "child does not have malaria" would fail.)
Validating and restricting responses¶
To validate or restrict response values,
use the constraint column.
The constraint expression will be evaluated
when the user advances to the next screen.
If the expression evaluates to True
,
the form advances as usual.
If False
,
the form does not advance
and the constraint_message is displayed.
The entered value of the response is represented in the expression
with a single dot (.
).
Constraint expressions often use comparison operators and regular expressions. For example:
- . >= 18
- True if response is greater than or equal to 18.
- . < 20 and . > 200
- True if the response is between 20 and 200.
- regex(.,'p{L}+')
- True if the response only contains letters, without spaces, separators, or numbers.
- not(contains(., 'prohibited'))
- True if the substring
prohibited
does not appear in the response.
Note
Constraints are not evaluated if the response is left blank. To restrict empty responses, make the question required.
See also

XLSForm
type | name | label | constraint | constraint_message |
---|---|---|---|---|
text | middle_initial | What is your middle initial? | regex(., 'p{L}') | Just the first letter. |
Read-only questions¶
To completely restrict user-entry, use the read_only column with a value of yes. This is usually combined with a default response, which is often calculated based on previous responses.
XLSForm
type | name | label | read_only | default | calculation |
---|---|---|---|---|---|
decimal | salary_income | Income from salary | |||
decimal | self_income | Income from self-employment | |||
decimal | other_income | Other income | |||
calculate | income_sum | ${salary_income} + ${self_income} + ${other_income} | |||
decimal | total_income | Total income | yes | ${income_sum} |
Conditionally showing questions¶
The relevant column can be used to show or hide questions and groups of questions based on previous responses.
If the expression in the relevant column
evaluates to True
,
the question or group is shown.
If False
,
the question is skipped.
Often, comparison operators are used in relevance expressions. For example:
- ${age} <= 5
- True if age is five or less.
- ${has_children} = 'yes'
- True if the answer to has_children was
yes
.
Relevance expressions can also use functions. For example:
- selected(${allergies}, 'peanut')
- True if
peanut
was selected in the Multi select widget named allergies. - contains(${haystack}, 'needle')
- True if the exact string
needle
is contained anywhere inside the response to haystack. - count-selected(${toppings}) > 5
- True if more than five options were selected in the Multi select widget named toppings.
Simple example¶
XLSForm
type | name | label | relevant |
---|---|---|---|
select_one yes_no | watch_sports | Do you watch sports? | |
text | favorite_team | What is your favorite team? | ${watch_sports} = 'yes' |
list_name | name | label |
---|---|---|
yes_no | yes | Yes |
yes_no | no | No |
Complex example¶
XLSForm
type | name | label | hint | relevant | constraint |
---|---|---|---|---|---|
select_multiple medical_issues | what_issues | Have you experienced any of the following? | Select all that apply. | ||
select_multiple cancer_types | what_cancer | What type of cancer have you experienced? | Select all that apply. | selected(${what_issues}, 'cancer') | |
select_multiple diabetes_types | what_diabetes | What type of diabetes do you have? | Select all that apply. | selected(${what_issues}, 'diabetes') | |
begin_group | blood_pressure | Blood pressure reading | selected(${what_issues}, 'hypertension') | ||
integer | systolic_bp | Systolic | . > 40 and . < 400 | ||
integer | diastolic_bp | Diastolic | . >= 20 and . <= 200 | ||
end_group | |||||
text | other_health | List other issues. | selected(${what_issues}, 'other') | ||
note | after_health_note | This note is after all health questions. |
list_name | name | label |
---|---|---|
medical_issues | cancer | Cancer |
medical_issues | diabetes | Diabetes |
medical_issues | hypertension | Hypertension |
medical_issues | other | Other |
cancer_types | lung | Lung cancer |
cancer_types | skin | Skin cancer |
cancer_types | prostate | Prostate cancer |
cancer_types | breast | Breast cancer |
cancer_types | other | Other |
diabetes_types | type_1 | Type 1 (Insulin dependent) |
diabetes_types | type_2 | Type 2 (Insulin resistant) |
Warning
Calculations are evaluated regardless of their relevance.
For example, if you have a calculate widget that adds together two previous responses, you cannot use relevant to skip in the case of missing values. (Missing values will cause an error.)
Instead,
use the if() function to check for the existence of a value,
and put your calculation inside the then
argument.
For example,
when adding together fields a
and b
:
if(${a} != '' and ${b} != '', ${a} + ${b}, '')
In context:
type | name | label | calculation |
---|---|---|---|
integer | a | a = | |
integer | b | b = | |
calculate | a_plus_b | if(${a} != '' and ${b} != '', ${a} + ${b}, '') | |
note | display_sum | a + b = ${a_plus_b} |
Groups of questions¶
To group questions, use the begin_group…end_group syntax.
XLSForm — Question group
type | name | label |
---|---|---|
begin_group | my_group | My text widgets |
text | question_1 | Text widget 1 |
text | question_2 | These questions will both be grouped together |
end_group |
If given a label, groups will be visible in the form path to help orient the user (e.g. My text widgets > Text widget 1). Labeled groups will also be visible as clickable items in the jump menu:

Warning
If you use ODK Build v0.3.4 or earlier, your groups will not be visible in the jump menu. The items inside the groups will display as if they weren't grouped at all.
Groups without labels can be helpful for organizing questions in a way that's invisible to the user. This technique can be helpful for internal organization of the form. These groups can also be a convenient way to conditionally show certain questions.
Repeating groups of questions¶
Note
Using repetition in a form is very powerful but can also make training and data analysis more time-consuming. Aggregate does not export repeats so Briefcase or one of the data publishers will be needed to transfer data from Aggregate. Repeats will be in their own documents and will need to be joined with their parent records for analysis.
Before adding repeats to your form, consider other options:
- if the number of repetitions is small and known ahead of time, consider "unrolling" the repeat by copying the same questions several times.
- if the number of repetitions is large and includes many questions, consider building a separate form that enumerators fill out multiple times and link the forms with some parent key (e.g., a household ID).
If repeats are needed, consider adding some summary calculations at the end so that analysis will not require joining the repeats with their parent records. For example, if you are gathering household information and would like to compute the total number of households visited across all enumerators, add a calculation after the repeats that counts the repetitions in each submission.
To repeat questions or groups of questions use the begin_repeat…end_repeat syntax.
XLSForm — Single question repeat group
type | name | label |
---|---|---|
begin_repeat | my_repeat_group | Repeat group label |
text | repeated_question | This question will be repeated. |
end_repeat |
XLSForm — Multi-question repeat group
type | name | label |
---|---|---|
begin_repeat | my_repeat | Repeat group label |
note | repeated_note | These questions will be repeated as an entire group. |
text | name | What is your name? |
text | quest | What is your quest? |
text | fave_color | What is your favorite color? |
end_repeat |
Controlling the number of repetitions¶
User-controlled repeats¶
By default, the user controls how many times the questions are repeated.
Before each repetition, the user is asked if they want to add another repeat group.
Note
The label in the begin_repeat row is shown in the Add New Group? message.
A meaningful label will help enumerators and participants navigate the form as intended.

The user is given the option to add each iteration.
XLSForm
type | name | label |
---|---|---|
begin_repeat | repeat_example | repeat group label |
text | repeat_test | Question label |
end_repeat |
Note
This interaction may be confusing to users the first time they see it. If enumerators know the number of repetitions ahead of time, consider using dynamically defined repeats.
Statically defined repeats¶
Use the repeat_count column to define the number of times a group will repeat.
XLSForm
type | name | label | repeat_count |
---|---|---|---|
begin_repeat | my_repeat | Repeat group label | 3 |
note | repeated_note | These questions will be repeated as an entire group. | |
text | name | What is your name? | |
text | quest | What is your quest? | |
text | fave_color | What is your favorite color? | |
end_repeat |
Dynamically defined repeats¶
The repeat_count column can reference previous responses and calculations.
XLSForm
type | name | label | repeat_count |
---|---|---|---|
integer | number_of_children | How many children do you have? | |
begin_repeat | child_questions | Questions about child | ${number_of_children} |
text | child_name | Child's name | |
integer | child_age | Child's age | |
end_repeat |
See also
Filtering options in select questions¶
To limit the options in a select question based on the answer to a previous question, use a choice_filter row in the survey sheet, and filter key columns in the choices sheet.
For example, you might ask the user to select a state first, and then only display cities within that state. This is called a cascading select, and can be extended to any depth. This example form shows a three-tiered cascade: state, county, city.
XLSForm
type | name | label | choice_filter |
---|---|---|---|
select_one job_categories | job_category | Job category | |
select_one job_titles | job_title | Job title | job_category=${job_category} |
list_name | name | label | job_category |
---|---|---|---|
job_categories | finance | Finance | |
job_categories | hr | Human Resources | |
job_categories | admin | Administration/Office | |
job_categories | marketing | Marketing | |
job_titles | ar | Accounts Receivable | finance |
job_titles | ap | Account Payable | finance |
job_titles | bk | Bookkeeping | finance |
job_titles | pay | Payroll | finance |
job_titles | recruiting | Recruiting | hr |
job_titles | training | Training | hr |
job_titles | retention | Retention | hr |
job_titles | asst | Office Assistant | admin |
job_titles | mngr | Office Manager | admin |
job_titles | scheduler | Scheduler | admin |
job_titles | reception | Receptionist | admin |
job_titles | creative_dir | Creative Director | marketing |
job_titles | print_design | Print Designer | marketing |
job_titles | ad_buyer | Ad Buyer | marketing |
job_titles | copywriter | Copywriter | marketing |