SurveyLoopr
Tips & Tricks
11 min read

Randomize Choices and Question Order in XLSForm

Mehrab Ali
Mehrab AliPublished November 10, 2026

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.

Download example form (.xlsx)All five examples in one XLSForm, one group each. Tested with pyxform and ODK Validate; free to reuse under CC BY 4.0.Download

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:

typenamelabelparameters
select_one sourcestaple_sourceWhere does the household buy most of its rice?randomize=true
select_multiple sourceother_sourcesWhere else did the household buy food in the last 7 days?randomize=true
surveychoicessettings
list_namenamelabel
source1Local market
source2Village shop
source3Supermarket
source4Mobile vendor
source5Cooperative or fair price shop
surveychoicessettings

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:

typenamelabelcalculationparameters
calculateorder_seedonce(substr(decimal-date-time(now()), 8))
select_one brandsoap_brandWhich brand of soap does the household use most?randomize=true seed=${order_seed}
calculatesoap_orderonce(join(' ', randomize(instance('brand')/root/item/name, ${order_seed})))
surveychoicessettings
list_namenamelabel
brand1Lifebuoy
brand2Lux
brand3Keya
brand4Meril
surveychoicessettings
  • 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 how once() behaves.
  • seed needs randomize=true in the same cell. The two are separated by a space.
  • soap_order saves the shuffled codes, for example 3 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.

Warning:

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:

typenamelabelcalculationrelevantrepeat_count
calculateblock_orderonce(join(' ', randomize(instance('wash_block')/root/item/name)))
begin_repeatsectionWASH section3
calculateblock_idselected-at(${block_order}, position(..) - 1)
begin_groupwaterDrinking water${block_id} = '1'
select_one yes_nowater_treatedDid the household treat its drinking water in the last 7 days?
end_groupwater
begin_groupsanitationSanitation${block_id} = '2'
select_one yes_nolatrine_sharedDoes the household share its latrine with other households?
end_groupsanitation
begin_grouphygieneHandwashing${block_id} = '3'
select_one yes_nosoap_seenIs soap present at the handwashing place?
end_grouphygiene
end_repeatsection
surveychoicessettings
list_namenamelabel
wash_block1Drinking water
wash_block2Sanitation
wash_block3Handwashing
surveychoicessettings
list_namenamelabel
yes_no1Yes
yes_no0No
surveychoicessettings

How it works:

  • block_order reads the codes of the wash_block list, shuffles them with randomize() and joins them into text such as 2 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 from block_order (it counts from 0, hence position(..) - 1), and only the group with that code is relevant.
  • block_order is 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:

typenamelabelcalculationrelevant
calculatearmonce(int(random() * 2) + 1)
begin_groupversion_aVersion A${arm} = 1
integerdays_ill_aIn the last 30 days, how many days was the child ill?
end_groupversion_a
begin_groupversion_bVersion B${arm} = 2
integerdays_ill_bIn the last 30 days, how many days did the child have fever, diarrhoea or cough?
end_groupversion_b
surveychoicessettings

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.

Note:

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:

typenamelabelcalculationrelevant
calculatepathonce(int(random() * 2) + 1)
begin_grouppath_1Income then expenses${path} = 1
integerincome_1What was the household income last month?
integerspend_1What did the household spend on food last month?
end_grouppath_1
begin_grouppath_2Expenses then income${path} = 2
integerspend_2What did the household spend on food last month?
integerincome_2What was the household income last month?
end_grouppath_2
calculateincomecoalesce(${income_1}, ${income_2})
calculatefood_spendcoalesce(${spend_1}, ${spend_2})
surveychoicessettings

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 wroteWrite insteadError you will see
randomize=yesrandomize=truerandomize must be set to true or false: 'yes' is an invalid value
randomise=truerandomize=true (with a z)Accepted parameters are 'randomize, seed'. The following are invalid parameter(s): 'randomise'.
seed=${order_seed} alonerandomize=true seed=${order_seed}Parameters must include randomize=true to use a seed.
randomize=true on a text questiononly on select_one or select_multipleAccepted parameters are 'rows'. The following are invalid parameter(s): 'randomize'.
seed=${orderseed} (misspelt name)the exact name of the seed calculateCould not find the name 'orderseed'.
randomize(instance('brand')/root/item/name) in a calculatewrap it: once(join(' ', randomize(...)))XPath evaluation: type mismatch
randomize=true in the appearance columnmove it to parametersNo 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:

PlatformShuffle choicesKeep one order per interviewKeep some choices fixed
ODK (XLSForm)randomize=true in parametersadd seed=${field}Not possible
KoboToolbox (XLSForm upload)Same as ODKSame as ODKNot possible
SurveyCTOrandomized in the appearance column, or "Automatically randomize choice order" in the form designerSee SurveyCTO's documentationrandomized(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).

Tip:

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.

Build my first form free

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.