XLSForm: Show the Choice Label, Not the Value
To show a choice's label instead of its value in XLSForm, add a calculate with jr:choice-name(${water}, '${water}') and put ${water_label} in your note or question label. The first argument is the code to look up, the second is the select question in single quotes. ODK and KoboToolbox use this syntax; SurveyCTO's version is choice-label(${water}, ${water}). Every example below compiles with pyxform and ODK Validate, the check ODK Central runs when you upload a form.
Why does my note show the code instead of the label?
A select question stores the choice's name, not its label. When you write ${water} in a note, the form inserts that stored value, so the enumerator reads "Main source: 2" instead of "Main source: Tube well". That is by design: codes keep the data stable when labels are reworded or translated.
To show the words, look the label up with jr:choice-name():
| type | name | label | calculation |
|---|---|---|---|
| select_one water_source | water | What is the household's main source of drinking water? | |
| calculate | water_label | jr:choice-name(${water}, '${water}') | |
| note | confirm_water | You said the main source is: ${water_label}. Is that right? If not, go back and change it. |
| list_name | name | label |
|---|---|---|
| water_source | 1 | Piped water into the dwelling |
| water_source | 2 | Tube well or borehole |
| water_source | 3 | Protected dug well |
| water_source | 4 | Unprotected dug well |
| water_source | 5 | Surface water (river or pond) |
Three rules cover most mistakes:
- Quote the second argument. It is
'${water}'in single quotes, not${water}. The quotes tell the form which question's choice list to read. - Name the question, not the list.
'${water}'is right;'${water_source}'(the list name) fails because no question has that name. - Keep the
jr:prefix. The function isjr:choice-name, lowercase.choice-namealone orJR:choice-nameare rejected.
How do I show the labels of a select_multiple answer?
jr:choice-name() looks up one code. A select_multiple answer holds several codes separated by spaces (1 4), which matches no single choice, so passing it straight in only works when exactly one option is ticked. Take each code with selected-at(), which counts from 0, and look up its label:
| type | name | label | calculation | constraint | constraint_message |
|---|---|---|---|---|---|
| select_multiple income_source | income_sources | What were the household's sources of income in the last 30 days? Select up to three. | count-selected(.) <= 3 | Select at most three sources. | |
| calculate | source_1 | jr:choice-name(selected-at(${income_sources}, 0), '${income_sources}') | |||
| calculate | source_2 | if(count-selected(${income_sources}) >= 2, concat(', ', jr:choice-name(selected-at(${income_sources}, 1), '${income_sources}')), '') | |||
| calculate | source_3 | if(count-selected(${income_sources}) >= 3, concat(', ', jr:choice-name(selected-at(${income_sources}, 2), '${income_sources}')), '') | |||
| calculate | sources_text | concat(${source_1}, ${source_2}, ${source_3}) | |||
| note | show_sources | Income sources: ${sources_text} |
| list_name | name | label |
|---|---|---|
| income_source | 1 | Crop sales |
| income_source | 2 | Livestock sales |
| income_source | 3 | Daily wage labour |
| income_source | 4 | Salaried work |
| income_source | 5 | Remittances |
| income_source | 6 | Petty trade |
The if(count-selected(...) >= 2, ...) guard adds the comma only when there is a second label, so one ticked source reads "Crop sales", not "Crop sales, , ". The constraint keeps the answer within the three slots the calculates cover; add a row per extra slot if you allow more. To skip questions based on a ticked option, use selected() as in select_multiple skip logic.
How do I list every ticked label, however many there are?
For long lists, use a repeat that runs once per ticked option, look up one label in each instance, then join them with join() outside the repeat. The repeat holds only calculates, so the enumerator never sees it:
| type | name | label | calculation | repeat_count |
|---|---|---|---|---|
| select_multiple illness | illnesses | Which illnesses has the child had in the last two weeks? | ||
| begin_repeat | illness_rpt | Illnesses | count-selected(${illnesses}) | |
| calculate | illness_code | selected-at(${illnesses}, position(..) - 1) | ||
| calculate | illness_name | jr:choice-name(${illness_code}, '${illnesses}') | ||
| end_repeat | ||||
| calculate | illness_text | join(', ', ${illness_name}) | ||
| note | show_illness | The child had: ${illness_text} |
| list_name | name | label |
|---|---|---|
| illness | 1 | Fever |
| illness | 2 | Diarrhoea |
| illness | 3 | Cough or fast breathing |
| illness | 4 | Skin rash |
| illness | 5 | Eye infection |
| illness | 6 | Measles |
The same repeat is the base for asking follow-up questions about each ticked option by name, covered step by step in repeat a group for each selected option.
The calculate stores the label as text in the submission, next to the code. Analyse the code column and treat the label column as display text only: if an enumerator works in another language, or you reword a choice between form versions, the stored labels change while the codes stay the same.
Can I show the label of a choice that was not selected?
Yes. The first argument can be any code, not only the answer. Write it as a quoted literal to get one fixed label from a question's list, which keeps a reminder in sync with the choices sheet when you reword a choice:
| type | name | label | calculation | relevant |
|---|---|---|---|---|
| select_one water_source | water_main | What is the household's main source of drinking water? | ||
| calculate | safe_label | jr:choice-name('1', '${water_main}') | ||
| note | safe_note | Remind the household that ${safe_label} is the safest option in this area. | ${water_main} != '1' |
| list_name | name | label |
|---|---|---|
| water_source | 1 | Piped water into the dwelling |
| water_source | 2 | Tube well or borehole |
| water_source | 3 | Protected dug well |
| water_source | 4 | Unprotected dug well |
| water_source | 5 | Surface water (river or pond) |
Which language does jr:choice-name() return?
It returns the label in the language that is active when the calculate runs. In a form with label::English (en) and label::Bangla (bn) columns, an enumerator working in Bangla sees and saves the Bangla label. Setting up those columns is covered in adding languages to an ODK form.
Two consequences come up on the ODK forum again and again:
- Switching language mid-interview may not update it. ODK Collect does not recalculate on a language change, so a label already worked out can stay in the old language until its question is answered again.
- There is no argument for "always English". If you need the English label in the data whatever language the enumerator uses, add your own column to the choices sheet and read it with
instance().
| type | name | label::English (en) | label::Bangla (bn) | calculation |
|---|---|---|---|---|
| select_one crop | main_crop | What is the main crop this season? | এই মৌসুমে প্রধান ফসল কী? | |
| calculate | crop_label | jr:choice-name(${main_crop}, '${main_crop}') | ||
| calculate | crop_en | instance('crop')/root/item[name = ${main_crop}]/label_en | ||
| note | show_crop | Main crop: ${crop_label} | প্রধান ফসল: ${crop_label} |
| list_name | name | label::English (en) | label::Bangla (bn) | label_en |
|---|---|---|---|---|
| crop | 1 | Rice | ধান | Rice |
| crop | 2 | Jute | পাট | Jute |
| crop | 3 | Wheat | গম | Wheat |
| crop | 4 | Potato | আলু | Potato |
crop_label follows the enumerator's language for the note; crop_en is always English, for the dataset. The column name label_en is your choice; it only needs to be a plain name without ::.
Many people add jr:choice-name() calculates only to get labels into the export. KoboToolbox can export labels directly: pick a labels format instead of XML values when you create the export. ODK Central exports the codes only, so there you either add the calculate or join the labels in during analysis.
Common mistakes and how to fix them
We ran each of these through pyxform and ODK Validate. The messages are what you will see in ODK Central, KoboToolbox or the XLSForm validator:
| You wrote | Write instead | Error you will see |
|---|---|---|
jr:choice-name(${water}, ${water}) | Quote the question: '${water}' | error in evaluation of xpath function [choice-name] => ... Parse error in XPath path: []. |
jr:choice-name(${water}, '${water_source}') (the list name) | The question name: '${water}' | Could not find the name 'water_source'. |
jr:choice-name(${water}) | Add the question: jr:choice-name(${water}, '${water}') | type mismatch for function 'jr:choice-name' |
choice-name(...) or JR:choice-name(...) | jr:choice-name(...) | cannot handle function 'choice-name' |
choice-label(${water}, ${water}) in an ODK or Kobo form | jr:choice-name(${water}, '${water}') | cannot handle function 'choice-label' |
jr:choice-name(${income_sources}, '${income_sources}') on select_multiple | jr:choice-name(selected-at(${income_sources}, 0), '${income_sources}') | No error, but it only works when one option is ticked |
The last row passes every check and fails in the field, the first time someone ticks two options. For other compile messages, see common XLSForm errors.
Does this work the same in ODK, KoboToolbox and SurveyCTO?
ODK and KoboToolbox share the same function. SurveyCTO has its own, with the arguments the other way round:
| Platform | Function | Notes |
|---|---|---|
| ODK Collect and Central | jr:choice-name(${water}, '${water}') | Code first, quoted question second. Central exports codes only |
| KoboToolbox (KoboCollect and Enketo) | jr:choice-name(${water}, '${water}') | Same as ODK. In the Formbuilder, type it in a Calculate question. Exports can use labels |
| SurveyCTO | choice-label(${water}, ${water}) | Field first, value second, no quotes. For select_multiple: choice-label(${sources}, selected-at(${sources}, 0)) |
selected-at(), count-selected(), concat() and if() work the same on all three, so only the label function changes when you move a form between ODK and SurveyCTO. For more calculate patterns, see the XLSForm calculations guide.
Why read the label back to the respondent
A confirmation note that says "You said the main source is Surface water" catches a mis-tap while the household is still in front of you. A note that says "You said 5" catches nothing, because nobody remembers which code is surface water. The same lookup makes later questions read naturally ("How much rice did you harvest?"), which cuts the guesswork that turns into wrong answers.
You can describe it in plain language instead: "after the water source question, read the chosen source back to the respondent and ask them to confirm". LooprAI writes the jr:choice-name() calculate and the note, then compiles the form with pyxform before anything is saved, so a missing quote never reaches your form.
Next: XLSForm calculation examples →
Frequently Asked Questions
How do I show the label of a choice instead of its value in XLSForm?
Add a calculate with jr:choice-name(${question}, '${question}') and reference that calculate in a note or label. The first argument is the stored code, the second is the select question's reference in single quotes. It works in ODK Collect, Central and KoboToolbox.
Why does my note show 1 instead of the answer's text?
A select question stores the choice's name, not its label, and ${question} inserts that stored name. If your choice names are numeric codes, the note shows the number. Look the label up with jr:choice-name() in a calculate and show the calculate instead.
How do I use jr:choice-name() with a select_multiple question?
Look up one code at a time: jr:choice-name(selected-at(${q}, 0), '${q}') gives the first ticked option's label, index 1 the second, and so on. Join them with concat(), or use a repeat with count-selected() and join() for any number of ticked options.
Why does jr:choice-name() give an error about a parse error in XPath path?
The second argument is missing its quotes. Write jr:choice-name(${water}, '${water}') with single quotes around the question reference. Without them the form passes the answer itself instead of the question's path, and ODK Validate rejects the expression.
Can jr:choice-name() return the English label in a multi-language form?
No, it always returns the label in the language that is active. To store English whatever language the enumerator uses, add a plain column such as label_en to the choices sheet and read it with instance('list')/root/item[name = ${question}]/label_en.
Is jr:choice-name() the same in KoboToolbox and SurveyCTO?
KoboToolbox uses the same jr:choice-name(${q}, '${q}') as ODK. SurveyCTO uses choice-label(${q}, ${q}) instead, with the field first, the value second and no quotes. selected-at() and count-selected() work the same on all three platforms.
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.