Randomize Choices and Question Order in XLSForm
To shuffle the choices of a question in XLSForm, write randomize=true in the parameters column of a select_one or select_multiple row; add seed=${my_seed} to keep one order per interview. There is no column that shuffles the order of questions, but a repeat with randomize() and selected-at() does it, and once(random()) assigns a random form version. Every example below compiles with pyxform and ODK Validate.
How do I randomize the order of choices?
Respondents pick options near the top of a list more often, especially when the enumerator reads the list aloud. Shuffling the list spreads that bias across all options. A food security survey asking where the household buys staples:
| type | name | label | parameters |
|---|---|---|---|
| select_one source | staple_source | Where does the household buy most of its rice? | randomize=true |
| select_multiple source | other_sources | Where else did the household buy food in the last 7 days? | randomize=true |
| list_name | name | label |
|---|---|---|
| source | 1 | Local market |
| source | 2 | Village shop |
| source | 3 | Supermarket |
| source | 4 | Mobile vendor |
| source | 5 | Cooperative or fair price shop |
Only the display order changes. The data still stores 1 for Local market, whatever position it appeared in, so analysis code needs no change.
Without a seed, the order changes every time the question is shown, including when the enumerator goes back to it or reopens a saved draft. That is confusing in the field and makes it impossible to know which order a respondent saw.
How do I keep the same order for the whole interview?
Give the shuffle a seed. A seed that is fixed for the submission gives one order per interview, and the same order every time that interview is reopened. ODK's own documentation uses the fractional part of the current time:
| type | name | label | calculation | parameters |
|---|---|---|---|---|
| calculate | order_seed | once(substr(decimal-date-time(now()), 8)) | ||
| select_one brand | soap_brand | Which brand of soap does the household use most? | randomize=true seed=${order_seed} | |
| calculate | soap_order | once(join(' ', randomize(instance('brand')/root/item/name, ${order_seed}))) |
| list_name | name | label |
|---|---|---|
| brand | 1 | Lifebuoy |
| brand | 2 | Lux |
| brand | 3 | Keya |
| brand | 4 | Meril |
once()is what holds the seed. Without it, the time is read again on later evaluations and the order can change. The unique ID guide explains howonce()behaves.seedneedsrandomize=truein the same cell. The two are separated by a space.soap_ordersaves the shuffled codes, for example3 1 4 2, using the same function and seed as the question. Check a few test submissions against what the phone showed before relying on it for an order-effects analysis.
Any number unique to the interview works as a seed: a respondent ID, or the last digits of a phone number.
randomize=true shuffles every choice, including "Other", "Don't know" (-99) and "Refused" (-98). ODK and KoboToolbox cannot keep one option fixed at the bottom. Do not randomize ordinal scales (Never, Sometimes, Often) or lists where these codes must stay last.
If the list needs a "Don't know", ask about it first ("Does the household have a preferred brand?") and show the randomized list only to those who say yes. For an "Other" option with a text box, see Other, please specify in XLSForm.
How do I randomize the order of questions or sections?
XLSForm has no setting for question order. The method that scales, described on the ODK forum, is to shuffle the block numbers once, then put the blocks in a repeat that runs once per block, and show only one block in each round. Here a WASH survey asks its water, sanitation and hygiene sections in a random order:
| type | name | label | calculation | relevant | repeat_count |
|---|---|---|---|---|---|
| calculate | block_order | once(join(' ', randomize(instance('wash_block')/root/item/name))) | |||
| begin_repeat | section | WASH section | 3 | ||
| calculate | block_id | selected-at(${block_order}, position(..) - 1) | |||
| begin_group | water | Drinking water | ${block_id} = '1' | ||
| select_one yes_no | water_treated | Did the household treat its drinking water in the last 7 days? | |||
| end_group | water | ||||
| begin_group | sanitation | Sanitation | ${block_id} = '2' | ||
| select_one yes_no | latrine_shared | Does the household share its latrine with other households? | |||
| end_group | sanitation | ||||
| begin_group | hygiene | Handwashing | ${block_id} = '3' | ||
| select_one yes_no | soap_seen | Is soap present at the handwashing place? | |||
| end_group | hygiene | ||||
| end_repeat | section |
| list_name | name | label |
|---|---|---|
| wash_block | 1 | Drinking water |
| wash_block | 2 | Sanitation |
| wash_block | 3 | Handwashing |
| list_name | name | label |
|---|---|---|
| yes_no | 1 | Yes |
| yes_no | 0 | No |
How it works:
block_orderreads the codes of thewash_blocklist, shuffles them withrandomize()and joins them into text such as2 3 1. No question uses the list; it only feeds the shuffle.- The repeat runs 3 times. In each round,
selected-at()takes the next code fromblock_order(it counts from 0, henceposition(..) - 1), and only the group with that code is relevant. block_orderis saved with the submission, so you know which order each respondent got.
The cost is in the data: the answers land on the repeat sheet, one row per section, with blanks for the groups that were not shown in that round. Merge them back to one row per household at analysis time. For more on how repeats are exported, see XLSForm repeat groups.
To shuffle questions inside one section, use the same pattern with one question per round instead of one group.
How do I show a random version of a question (split-ballot)?
Survey experiments often show half the respondents one wording and half another. Draw a random number once and use it in relevant:
| type | name | label | calculation | relevant |
|---|---|---|---|---|
| calculate | arm | once(int(random() * 2) + 1) | ||
| begin_group | version_a | Version A | ${arm} = 1 | |
| integer | days_ill_a | In the last 30 days, how many days was the child ill? | ||
| end_group | version_a | |||
| begin_group | version_b | Version B | ${arm} = 2 | |
| integer | days_ill_b | In the last 30 days, how many days did the child have fever, diarrhoea or cough? | ||
| end_group | version_b |
int(random() * 2) + 1 gives 1 or 2 with equal chance; change the 2 for more arms. once() matters again: random() can be evaluated more than once during an interview, and without once() a respondent could switch arm partway through. The arm value is in the data, so you can compare the two versions. For the logic in relevant, see the XLSForm skip logic guide.
Assigning a version inside the form is fine for question wording experiments. It is not a substitute for treatment allocation in a trial, which needs a list generated and controlled by the research team.
How do I randomize the order of a few sections without a repeat?
With 2 or 3 sections, you can avoid the repeat: list each order as its own group and show one at random. Two sections have two orders:
| type | name | label | calculation | relevant |
|---|---|---|---|---|
| calculate | path | once(int(random() * 2) + 1) | ||
| begin_group | path_1 | Income then expenses | ${path} = 1 | |
| integer | income_1 | What was the household income last month? | ||
| integer | spend_1 | What did the household spend on food last month? | ||
| end_group | path_1 | |||
| begin_group | path_2 | Expenses then income | ${path} = 2 | |
| integer | spend_2 | What did the household spend on food last month? | ||
| integer | income_2 | What was the household income last month? | ||
| end_group | path_2 | |||
| calculate | income | coalesce(${income_1}, ${income_2}) | ||
| calculate | food_spend | coalesce(${spend_1}, ${spend_2}) |
Each question needs a different name in each path, so coalesce() joins the copies back into one income and one food_spend column. Three sections have 6 orders and four have 24, so past three sections the repeat method above is less work.
Common randomization mistakes and how to fix them
These are the messages pyxform and ODK Validate return, which is what ODK Central, KoboToolbox or the XLSForm converter shows when it rejects the form:
| You wrote | Write instead | Error you will see |
|---|---|---|
randomize=yes | randomize=true | randomize must be set to true or false: 'yes' is an invalid value |
randomise=true | randomize=true (with a z) | Accepted parameters are 'randomize, seed'. The following are invalid parameter(s): 'randomise'. |
seed=${order_seed} alone | randomize=true seed=${order_seed} | Parameters must include randomize=true to use a seed. |
randomize=true on a text question | only on select_one or select_multiple | Accepted parameters are 'rows'. The following are invalid parameter(s): 'randomize'. |
seed=${orderseed} (misspelt name) | the exact name of the seed calculate | Could not find the name 'orderseed'. |
randomize(instance('brand')/root/item/name) in a calculate | wrap it: once(join(' ', randomize(...))) | XPath evaluation: type mismatch |
randomize=true in the appearance column | move it to parameters | No error, but the choices are not shuffled |
int(random() * 2) + 1 without once() | once(int(random() * 2) + 1) | No error, but the version can change during the interview |
For other messages, see common XLSForm errors or paste the form into the free XLSForm validator.
Does this work the same in ODK, KoboToolbox and SurveyCTO?
ODK and KoboToolbox use the same syntax. SurveyCTO randomizes choices differently:
| Platform | Shuffle choices | Keep one order per interview | Keep some choices fixed |
|---|---|---|---|
| ODK (XLSForm) | randomize=true in parameters | add seed=${field} | Not possible |
| KoboToolbox (XLSForm upload) | Same as ODK | Same as ODK | Not possible |
| SurveyCTO | randomized in the appearance column, or "Automatically randomize choice order" in the form designer | See SurveyCTO's documentation | randomized(0, 1) keeps the top 0 and bottom 1 choices in place |
The question order and split-ballot methods use random(), once(), randomize() and selected-at(), which are ODK XPath functions. They compile for ODK Collect and KoboToolbox; preview the form in Enketo (Kobo's web form) before fieldwork. SurveyCTO has its own guidance on randomizing section order, so check it before reusing the repeat method there.
When randomizing is worth it
Randomize unordered lists that respondents hear read aloud or see on a tablet: brands, sources, reasons, preferences. Leave alone lists with a natural order (age bands, frequency scales, Likert items) and lists the enumerator needs in a fixed place to find quickly, such as districts. Shuffling a long list of districts only slows the interview down; for those, a searchable list works better (see the XLSForm appearance column).
You can describe the design in plain language instead: "show the water, sanitation and hygiene sections in a random order and save the order shown". LooprAI writes the seed, the shuffle and the repeat, then compiles the form with pyxform before anything is saved.
Next: XLSForm appearance column →
Frequently Asked Questions
How do I randomize choices in XLSForm?
Add a parameters column to the survey sheet and write randomize=true in the row of a select_one or select_multiple question. ODK Collect and KoboToolbox then show the choices in a shuffled order. The stored codes do not change, so analysis works as before.
Why does the choice order change when I go back to a question?
Without a seed, the order is shuffled every time the question is displayed, including after going back or reopening a draft. Add a calculate with once(substr(decimal-date-time(now()), 8)) and use randomize=true seed=${order_seed} to keep one order per interview.
Can I keep 'Other' or 'Don't know' at the bottom of a randomized list?
Not in ODK or KoboToolbox: randomize=true shuffles every choice in the list. Ask about the special answer in a question before the list, or leave the list unrandomized. SurveyCTO can do it with the randomized(0, 1) appearance, which keeps the last choice in place.
How do I randomize the order of questions in ODK or KoboToolbox?
There is no setting for it. Shuffle the block numbers once with once(join(' ', randomize(...))), put the blocks in a repeat that runs once per block, and make each block relevant only when selected-at() returns its number for that round.
How do I record which order a respondent saw?
Save the shuffle in a calculate. For question order, the block_order calculate already holds it. For choices, calculate once(join(' ', randomize(instance('list')/root/item/name, ${seed}))) with the same seed as the question, and check it against test submissions.
How do I randomly assign respondents to version A or B?
Add a calculate with once(int(random() * 2) + 1), which gives 1 or 2 with equal chance, and put ${arm} = 1 or ${arm} = 2 in the relevant column of each version's group. once() keeps the version from changing during the interview.
No Credit Card Required
Build the form first. Add hosting when you need it.
Start free with LooprAI, then deploy a managed ODK Central server or run DataSnap checks when your project is ready.
Automation handles the mechanical work.
Researchers decide what the evidence means.