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



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 reference the value of previously answered questions. To use a variable, put the question's name in curly brackets preceded by a dollar sign:


Variables can be used in label, hint, and repeat_count columns, as well as any column that accepts an expression.

A text widget in Collect. The question is "What is your name?" The entry field has the value "Adam". A note widget in Collect. The text is "Hello, Adam."


type name label
text your_name What is your name?
note hello_name Hello, ${your_name}.


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 or False, 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:


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.

The decimal widget in Collect. The question label is "Bill amount". The entered value is "55.88". A note widget in Collect the text is: "Bill: 55.88; Tip: 10.06; Total: 65.95"


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}

Values from the last saved record


Support for last-saved was added in Collect v1.21.0. Form conversion requires XLSForm Online ≥ v2.0.0 or pyxform ≥ v1.0.0. Using older versions will have unpredictable results.

You can refer to values from the last saved record of this form definition:


This can be very useful when an enumerator has to enter the same value for multiple consecutive records. An example of this would be entering in the same district for a series of households.

XLSForm that shows using a last-saved value as a dynamic default

type name label default
text street Street ${last-saved#street}

The value is pulled from the last saved record. This is often the most recently created record but it could also be a previously-existing record that was edited and saved. For the first record ever saved for a form definition, the last saved value for any field will be blank.

Questions of any type can have their defaults set based on the last saved record. References to the last saved record can be used as part of any expression wherever expressions are allowed.

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.


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 a value in the default column. Defaults are set when a record is first created from a form definition. Defaults can either be fixed values (static defaults) or the result of some expression (dynamic defaults).

Static defaults

The text in the default column for a question is taken literally as the default value. Quotes should not be used to wrap values, unless you actually want those quote marks to appear in the default response value.

In the example below, the "Phone call" option with underlying value phone_call will be selected when the question is first displayed. The enumerator can either keep that selection or change it.

XLSForm to select "Phone call" as the default contact method

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 email Email

Dynamic defaults


Support for dynamic defaults was added in Collect v1.24.0. Form conversion requires XLSForm Online ≥ v2.0.0 or pyxform ≥ v1.0.0. Using older versions will have unpredictable results.

If you put an expression in the default column for a question, that expression will be evaluated once when a record is first created from a form definition. This allows you to use values from outside the form like the current date or the server username. Dynamic defaults can't be used to set the default value of one field to the value of another field in the form. Learn about alternatives in the tip below.

XLSForm to set the current date as default

type name label default
date fever_onset When did the fever start? now()

In the example below, if a username is set either in the server configuration or the metadata settings, that username will be used as the default for the question asked to the enumerator.

XLSForm to set the default username as the server username

type name label default
username username    
text confirmed_username What is your username? ${username}

If enumerators will need to enter the same value for multiple consecutive records, dynamic defaults can be combined with last saved.

Dynamic defaults in repeats are evaluated when a new repeat instance is added.


You may want to use a value filled out by the enumerator as a default for another question that the enumerator will later fill in. Dynamic defaults can't be used for this because they are evaluated once when a record is first created which is before an enumerator fills in any data.

One option is to use the calculation column and wrap your default value expression in a once() function.

XLSForm that uses a child's current age as the default for diagnosis age

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:

  • 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 to 10, when you get to malaria_age, the default value will be 8.

  • 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 value 8 when you get to malaria_age (intending to leave it blank) the 8 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.
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.


Constraints are not evaluated if the response is left blank. To restrict empty responses, make the question required.

A text widget in Collect. The question text is "What is your middle initial?" The entered value is "Michael". Over the widget is an alert message: "Just the first letter."


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.


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


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


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
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)


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

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:

The jump menu with a few grouped questions.


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 questions

You can ask the same question or questions multiple times by wrapping them in begin_repeat…end_repeat. By default, enumerators are asked before each repetition whether they would like to add another repeat. It is also possible to determine the number of repetitions ahead of time which can make the user interaction more intuitive. You can also add repeats as long as a condition is met.

XLSForm — Repeating one or more questions

type name label
begin_repeat my_repeat repeat group label
note repeated_note All of these questions will be repeated.
text name What is your name?
text quest What is your quest?
text fave_color What is your favorite color?


Displaying repeating questions on the same screen (inside a field-list group) is not supported.

See also

Repeat Recipes and Tips describes strategies to address common repetition scenarios.


Using repetition in a form is very powerful but can also make training and data analysis more time-consuming. Repeats exported from Central or Briefcase be in their own files 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.

Controlling the number of repetitions

User-controlled repeats

By default, the enumerator controls how many times the questions are repeated.

Before each repetition, the user is asked if they want to add another.


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.

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 a dynamically defined repeat count.

The Collect app. A modal dialog labeled "Add new group?" with the question: "Add a new 'repeat group label' group?" and options "Do not add" and "Add Group".

The user is given the option to add each repetition.


The jump menu also provides shortcuts to add or remove repeat instances.

Fixed repeat count

Use the repeat_count column to define the number of times that questions will repeat.


type name label repeat_count
begin_repeat my_repeat Repeat group label 3
note repeated_note These questions will be repeated exactly three times.  
text name What is your name?  
text quest What is your quest?  
text fave_color What is your favorite color?  

Dynamically defined repeat count

The repeat_count column can reference previous responses and calculations.


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  

Repeating as long as a condition is met

If the enumerator won't know how many repetitions are needed ahead of time, you can still avoid the "Add new group?" dialog by using the answer to a question to decide whether another repeat instance should be added. In the example below, repeated questions about plants will be asked as long as the user answers "yes" to the last question.

type name label calculation repeat_count    
calculate count   count(${plant})      
begin_repeat plant Plant   if(${count} = 0 or ${plant}[position()=${count}]/more_plants = 'yes' ${count} + 1 ${count})
text species Species        
integer estimated_size Estimated size        
select_one yes_no more_plants Are there more plants in this area?        
list_name name label
yes_no yes Yes
yes_no no No

This works by maintaining a count() of the existing repetitions and either making repeat_count one more than that if the continuing condition is met or keeping the repeat_count the same if the ending condition is met.

In the repeat_count expression, ${count} = 0 ensures that there is always at least one repeat instance created. The continuing condition is ${plant}[position()=${count}]/more_plants = 'yes' which means "the answer to more_plants was yes the last time it was asked." The expression position()=${count} uses the position() function to select the last plant that was added. Adding /more_plants to the end of that selects the more_plants question.

Repeating zero or more times

Sometimes it only makes sense to collect information represented by the questions in a repeat under certain conditions. If the number of total repetitions is known ahead of time, use Dynamically defined repeat count and allow a count of 0. If the count is not known ahead of time, Conditionally showing questions can be used to represent 0 or more repetitions. In the example below, questions about trees will only be asked if the user indicates that there are trees to survey.

type name label relevant  
select_one yes_no trees_present Are there any trees in this area?    
begin_repeat tree Tree ${trees_present} = 'yes'  
text species Species    
integer estimated_age Estimated age    
list_name name label
yes_no yes Yes
yes_no no No

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.


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