SurveyLoopr
Tips & Tricks
10 min read

XLSForm Choice Filter from a Select Multiple

Mehrab Ali
Mehrab AliPublished November 4, 2026

To show only the options ticked in an earlier select_multiple, write selected(${crops_grown}, name) in the later question's choice_filter column, and give both questions the same choice list. name is the column on the choices sheet, so each option stays only if its code is in the earlier answer. To hide the ticked options instead, wrap it: not(selected(${crops_grown}, name)). Every example below compiles with pyxform and ODK Validate, the same check ODK Central runs when you upload a form.

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 show only the options ticked in an earlier question?

An agriculture survey asks which crops the household grew, then which one brought in the most income. The second list should hold only the crops already ticked, not all six:

typenamelabelrelevantchoice_filtercalculation
select_multiple cropcrops_grownWhich crops did the household grow this season?
select_one cropmain_cropWhich of these crops brought in the most income?count-selected(${crops_grown}) > 1selected(${crops_grown}, name)
calculatemain_crop_finalif(count-selected(${crops_grown}) = 1, ${crops_grown}, ${main_crop})
surveychoicessettings
list_namenamelabel
crop1Rice
crop2Wheat
crop3Maize
crop4Lentils
crop5Jute
crop-96Other
surveychoicessettings

Read the filter as: keep each choice row whose name is one of the codes in ${crops_grown}. A household that ticked Rice and Jute sees two options. Three details make it work:

  1. Same list on both questions. The earlier answer stores codes like 1 5, and the filter compares them with the name column of the later list. Two lists with different codes match nothing.
  2. name has no quotes and no ${}. It is the choices column, read once per option.
  3. The relevant stops an empty or pointless question. With one crop ticked there is nothing to choose, so the question is skipped and main_crop_final copies the single answer.

The select_multiple skip logic guide explains why selected() is needed here instead of =.

How do I hide the options already picked?

Turn the filter round with not(). A follow-up about crops the household wants to try next season should leave out the ones it already grows:

typenamelabelchoice_filter
select_multiple cropcrops_nowWhich crops did the household grow this season?
select_multiple cropcrops_nextWhich new crops would the household like to grow next season?not(selected(${crops_now}, name))
surveychoicessettings

When the earlier question is a select_one, compare with !=. Asking for the first, second and third income source this way stops the same source being chosen twice:

typenamelabelchoice_filter
select_one incomeincome_1What is the household's main source of income?
select_one incomeincome_2What is the second most important source?name != ${income_1}
select_one incomeincome_3What is the third most important source?name != ${income_1} and name != ${income_2}
surveychoicessettings
list_namenamelabel
income1Crop farming
income2Livestock
income3Daily wage labour
income4Small business
income5Remittances
income-96Other
surveychoicessettings
Warning:

Do not filter a question by its own answer. not(selected(${crops_now}, name)) on crops_now itself compiles, but each option disappears the moment it is ticked. Always filter by an earlier question.

How do I rank only the options someone picked?

The rank type takes a choice_filter too, so a respondent can rank just the reasons they ticked, however many that is:

typenamelabelrelevantchoice_filter
select_multiple reasonbuy_reasonsWhy does the household buy from this shop?
rank reasonreason_rankPut these reasons in order of importance.count-selected(${buy_reasons}) > 1selected(${buy_reasons}, name)
surveychoicessettings
list_namenamelabel
reason1Low prices
reason2Close to home
reason3Sells on credit
reason4Good quality goods
reason5Trusted shopkeeper
surveychoicessettings

The answer is saved as the codes in ranked order, such as 3 1 5. The rank widget is documented by ODK for Collect and Web Forms; check it in your own app before relying on it elsewhere.

How do I filter a child list by several parents?

In a normal cascading select the parent is a select_one, so the filter is district=${district}. When traders work in more than one district, the parent is a select_multiple and = no longer fits. Swap the arguments into selected(), with the choices column second:

typenamelabelchoice_filter
select_multiple districttrade_districtsIn which districts does the trader do business?
select_multiple wardtrade_wardsIn which wards does the trader do business?selected(${trade_districts}, district)
surveychoicessettings
list_namenamelabeldistrict
district1Kurigram
district2Rangpur
ward101Kurigram Ward 11
ward102Kurigram Ward 21
ward201Rangpur Ward 12
ward202Rangpur Ward 22
surveychoicessettings

Here district is the column on the choices sheet, tagged with each ward's district code. A trader in Kurigram and Rangpur sees all four wards; a trader in Rangpur only sees two.

What about None and Other options?

An exclusive "None" option (see adding a None of the above option) never needs to reach the later list: when it is ticked, count-selected() is 1 and the follow-up is skipped. If your filter could still show it, add and name != '0' to the expression.

"Other" carries over as the word Other, not as what was typed. Keep the Other, please specify text question next to the select_multiple, and do not use or_other on the filtered question: pyxform refuses it.

Common mistakes when filtering by a select_multiple

These are the messages pyxform and ODK Validate return when ODK Central, KoboToolbox or the XLSForm converter rejects the form:

You wroteWrite insteadError you will see
select_one crop or_other with a choice_filterselect_one crop with a -96 Other optionChoice filter not supported with or_other.
selected(${crop_grown}, name) (typo in the name)The exact name from the survey sheetReference variables must contain a name from the 'survey' sheet. Could not find the name 'crop_grown'.
selected(${crops_grown}, nameClose the bracketSomething broke the parser. followed by Unsupported nodeset expression
select(${crops_grown}, name)selected(${crops_grown}, name)XPath evaluation: cannot handle function 'select'
${crops_grown} = nameselected(${crops_grown}, name)No error; works only when one option is ticked
selected(${crops_grown}, 'name')selected(${crops_grown}, name)No error; the list is always empty
A filter column missing from the choices sheetAdd the column, such as districtNo error; the list is always empty
Different lists on the two questionsOne list for bothNo error; nothing matches

The last four compile and only show up when someone fills the form, so test each combination in preview before training. 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?

The expression is plain XPath, and choice_filter is an XLSForm column, so the syntax is the same wherever the form is built from XLSForm:

PlatformWhere to type itNotes
ODK Central, Collect and Web Formschoice_filter column of the XLSFormODK documents that any true or false expression works as a choice filter, and that rank accepts one
KoboToolboxchoice_filter column, then upload the XLSFormWorks in KoboCollect and Enketo web forms with the same selected() syntax
SurveyCTOchoice_filter column of the form definitionAlso an XLSForm column; run the form test with a few combinations, as for any filter

If one form has to run on more than one platform, keep the lists identical and test the filtered questions on each.

Why filter instead of showing the full list?

A main crop that the household never grew is a contradiction you cannot fix after the interview. Showing only the ticked crops makes that answer impossible, keeps the list short on a small screen, and saves the enumerator from reading six options when two apply. The same pattern covers main water source, the reason for choosing a clinic, and the most serious of several shocks.

Tip:

You can describe the logic in plain language instead: "ask which crops they grew, then which of those brought in the most income". LooprAI writes the rows and the filter, then compiles the form with pyxform before anything is saved.

Build my first form free

Next: XLSForm cascading selects →

Frequently Asked Questions

How do I show only the options selected in a previous question in XLSForm?

Use the same choice list for both questions and write selected(${previous_question}, name) in the later question's choice_filter column. name is the choices sheet column, so only options whose code is in the earlier answer stay visible.

How do I hide options that were already selected?

Wrap the filter in not(): not(selected(${previous_question}, name)) shows only the options that were not ticked. When the earlier question is a select_one, name != ${previous_question} does the same and can be chained with and for a third question.

Why is my filtered list empty?

Usually the two questions use different choice lists, name was written in quotes, or the filter column does not exist on the choices sheet. All three compile without an error. Check that the codes in the earlier answer match the name column of the later list.

Can a cascading select have a select_multiple parent?

Yes. Tag each child option with its parent's code in a column on the choices sheet, then write selected(${parent_question}, column) in the child's choice_filter. The child list then shows options for every parent that was ticked.

Can I use or_other with a choice filter?

No. pyxform rejects the form with the message that choice filter is not supported with or_other. Add your own Other option, such as -96, to the choice list and a separate text question that appears when it is ticked.

Does selected() in choice_filter work in KoboToolbox?

Yes. KoboToolbox reads the choice_filter column from the uploaded XLSForm, and the same selected(${question}, name) expression works in KoboCollect and Enketo web forms. Test a few combinations in preview, since a wrong filter compiles and simply shows an empty list.

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.