XLSForm Choice Filter from a Select Multiple
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.
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:
| type | name | label | relevant | choice_filter | calculation |
|---|---|---|---|---|---|
| select_multiple crop | crops_grown | Which crops did the household grow this season? | |||
| select_one crop | main_crop | Which of these crops brought in the most income? | count-selected(${crops_grown}) > 1 | selected(${crops_grown}, name) | |
| calculate | main_crop_final | if(count-selected(${crops_grown}) = 1, ${crops_grown}, ${main_crop}) |
| list_name | name | label |
|---|---|---|
| crop | 1 | Rice |
| crop | 2 | Wheat |
| crop | 3 | Maize |
| crop | 4 | Lentils |
| crop | 5 | Jute |
| crop | -96 | Other |
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:
- Same list on both questions. The earlier answer stores codes like
1 5, and the filter compares them with thenamecolumn of the later list. Two lists with different codes match nothing. namehas no quotes and no${}. It is the choices column, read once per option.- 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_finalcopies 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:
| type | name | label | choice_filter |
|---|---|---|---|
| select_multiple crop | crops_now | Which crops did the household grow this season? | |
| select_multiple crop | crops_next | Which new crops would the household like to grow next season? | not(selected(${crops_now}, name)) |
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:
| type | name | label | choice_filter |
|---|---|---|---|
| select_one income | income_1 | What is the household's main source of income? | |
| select_one income | income_2 | What is the second most important source? | name != ${income_1} |
| select_one income | income_3 | What is the third most important source? | name != ${income_1} and name != ${income_2} |
| list_name | name | label |
|---|---|---|
| income | 1 | Crop farming |
| income | 2 | Livestock |
| income | 3 | Daily wage labour |
| income | 4 | Small business |
| income | 5 | Remittances |
| income | -96 | Other |
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:
| type | name | label | relevant | choice_filter |
|---|---|---|---|---|
| select_multiple reason | buy_reasons | Why does the household buy from this shop? | ||
| rank reason | reason_rank | Put these reasons in order of importance. | count-selected(${buy_reasons}) > 1 | selected(${buy_reasons}, name) |
| list_name | name | label |
|---|---|---|
| reason | 1 | Low prices |
| reason | 2 | Close to home |
| reason | 3 | Sells on credit |
| reason | 4 | Good quality goods |
| reason | 5 | Trusted shopkeeper |
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:
| type | name | label | choice_filter |
|---|---|---|---|
| select_multiple district | trade_districts | In which districts does the trader do business? | |
| select_multiple ward | trade_wards | In which wards does the trader do business? | selected(${trade_districts}, district) |
| list_name | name | label | district |
|---|---|---|---|
| district | 1 | Kurigram | |
| district | 2 | Rangpur | |
| ward | 101 | Kurigram Ward 1 | 1 |
| ward | 102 | Kurigram Ward 2 | 1 |
| ward | 201 | Rangpur Ward 1 | 2 |
| ward | 202 | Rangpur Ward 2 | 2 |
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 wrote | Write instead | Error you will see |
|---|---|---|
select_one crop or_other with a choice_filter | select_one crop with a -96 Other option | Choice filter not supported with or_other. |
selected(${crop_grown}, name) (typo in the name) | The exact name from the survey sheet | Reference variables must contain a name from the 'survey' sheet. Could not find the name 'crop_grown'. |
selected(${crops_grown}, name | Close the bracket | Something broke the parser. followed by Unsupported nodeset expression |
select(${crops_grown}, name) | selected(${crops_grown}, name) | XPath evaluation: cannot handle function 'select' |
${crops_grown} = name | selected(${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 sheet | Add the column, such as district | No error; the list is always empty |
| Different lists on the two questions | One list for both | No 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:
| Platform | Where to type it | Notes |
|---|---|---|
| ODK Central, Collect and Web Forms | choice_filter column of the XLSForm | ODK documents that any true or false expression works as a choice filter, and that rank accepts one |
| KoboToolbox | choice_filter column, then upload the XLSForm | Works in KoboCollect and Enketo web forms with the same selected() syntax |
| SurveyCTO | choice_filter column of the form definition | Also 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.
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.
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.