SurveyLoopr
Tips & Tricks
11 min read

XLSForm pulldata(): Look Up Values from a CSV

Mehrab Ali
Mehrab AliPublished October 28, 2026

pulldata() in XLSForm is pulldata('file', 'column_to_return', 'column_to_search', ${value}), written in the calculation column of a calculate row, with the CSV uploaded as a form attachment. It returns the value from the first row of file.csv whose search column matches. ODK, KoboToolbox and SurveyCTO use the same four arguments, and 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. Attach your own households.csv to fill it.Download

What is the syntax of pulldata() in XLSForm?

A follow-up survey needs the household head's name from the baseline. The baseline export becomes households.csv:

hh_id,head_name,village,hh_size
HH-0101,Rahima Begum,Char Kukri,5
HH-0102,Abdul Karim,Char Kukri,7
HH-0103,Shefali Rani,Dakshin Char,4

The form asks for the ID and pulls the name:

typenamelabelcalculation
texthh_idEnter the household ID from the visit list (e.g. HH-0101)
calculatehead_namepulldata('households', 'head_name', 'hh_id', ${hh_id})
noteshow_headHousehold head: ${head_name}
surveychoicessettings

The four arguments, in order:

  1. The file name without .csv. 'households' reads households.csv.
  2. The column to return, exactly as spelled in the header row.
  3. The column to search. Usually the ID column.
  4. The value to search for, usually ${question}.

The first three are text, so they go in single quotes. The match is exact text: hh-0101 does not find HH-0101, and a trailing space in the CSV is part of the value. When no row matches, pulldata() returns an empty string, not an error.

How do I attach the CSV file to the form?

The form only names the file; the file itself is uploaded next to the form:

PlatformWhere the CSV goes
ODK CentralUpload the form, then upload households.csv under the draft's form attachments before publishing
KoboToolboxProject Settings, then Media, upload the CSV, then redeploy the form
SurveyCTOAttach the CSV to the form definition when you upload it

Four file rules prevent most "pulldata returns nothing" threads on the ODK and Kobo forums:

  • Same name, no spaces. household list.csv fails silently in Enketo on KoboToolbox. Use household_list.csv and 'household_list'.
  • UTF-8. Save from Excel as "CSV UTF-8", or names in Bangla, Arabic or with accents come out garbled or never match.
  • A dot for decimals. Excel on a computer set to comma decimals (2,5) saves CSV files with semicolons between columns, so each row reads as one column. Switch the decimal separator to . before exporting.
  • Header names without spaces. Short names like hh_size, not Household size.
Warning:

After you replace a CSV, devices keep the old copy until they download the form again. On ODK Central, upload the new file to a new draft and publish it; then have enumerators run Get Blank Form. Kobo needs a redeploy. Test with an ID that only exists in the new file.

How do I pull several columns for one household?

Write one calculate per column. Each one searches the same ID column and returns a different field. Then show them together so the enumerator can confirm the right household before starting:

typenamelabelcalculationrelevant
texthh_idEnter the household ID from the visit list (e.g. HH-0101)
calculatehead_namepulldata('households', 'head_name', 'hh_id', ${hh_id})
calculatevillagepulldata('households', 'village', 'hh_id', ${hh_id})
noteconfirm_hhHousehold head: ${head_name}, village: ${village}
select_one yes_nohh_correctIs this the household you are visiting?
notewrong_hhGo back and check the household ID on the visit list.${hh_correct} = '0'
surveychoicessettings
list_namenamelabel
yes_no1Yes
yes_no0No
surveychoicessettings

Pulled values work like any other answer. ${village} can drive skip logic or a choice filter in later questions.

How do I use a pulled number in a calculation?

pulldata() always returns text, because a CSV has no data types. xlsform.org advises converting it with number() (or int() for whole numbers) before you use it as a number. This round-two check flags a household that changed size by more than two people since baseline:

typenamelabelcalculationconstraintconstraint_message
texthh_idEnter the household ID from the visit list (e.g. HH-0101)
calculatebaseline_sizepulldata('households', 'hh_size', 'hh_id', ${hh_id})
integercurrent_sizeHow many people live in this household now?${baseline_size} = '' or abs(. - number(${baseline_size})) <= 2Baseline recorded ${baseline_size} people. Check the roster before entering a bigger change.
surveychoicessettings

The ${baseline_size} = '' part lets a household that is missing from the baseline file through: number('') is not a number, and any comparison with it is false, so without that part the question could never be answered. More validation patterns are in the XLSForm constraint examples.

How do I check that the ID exists in the CSV?

Put pulldata() in the ID question's own constraint. If the lookup comes back empty, the ID is not in the file and the enumerator cannot move on. Inside a constraint, . means "this question's answer":

typenamelabelcalculationconstraintconstraint_message
texthh_idEnter the household ID from the visit list (e.g. HH-0101)pulldata('households', 'hh_id', 'hh_id', .) != ''This ID is not on the visit list. Check for a typo or a missing dash.
calculatehead_namepulldata('households', 'head_name', 'hh_id', ${hh_id})
noteshow_headHousehold head: ${head_name}
surveychoicessettings

Returning the search column itself ('hh_id' twice) is the simplest test: it is never empty for a row that exists. Scanning a barcode instead of typing (type barcode) removes most typos in the first place. For building IDs that are easy to type, see unique IDs in XLSForm.

Should I use pulldata() or instance() in ODK?

Both read the same attached CSV. ODK's documentation and xlsform.org now describe instance() as the preferred lookup, because the filter in square brackets can be any condition. Declare the file with a csv-external row, named after the file without .csv:

typenamelabelcalculation
csv-externalhouseholds
texthh_idEnter the household ID from the visit list (e.g. HH-0101)
calculatehead_nameinstance('households')/root/item[hh_id = ${hh_id}]/head_name
calculatebig_householdscount(instance('households')/root/item[hh_size > 6])
noteshow_headHousehold head: ${head_name}. Households with more than 6 members on the list: ${big_households}
surveychoicessettings
NeedUse
One value by ID, form used in KoboCollect or older appspulldata()
A lookup with two conditions, a count or a sum over rowsinstance() with csv-external
The respondent picks a row from the CSVselect_one_from_file, see cascading selects
Very large files (tens of thousands of rows) on slow phonespulldata(), which xlsform.org suggests when instance() is too slow

With instance(), one rule matters: if the filter matches two rows and you ask for one value, ODK Collect stops with a type mismatch. pulldata() quietly takes the first row instead, which hides duplicate IDs. Check the CSV for duplicates either way.

Common pulldata() mistakes and how to fix them

Most pulldata() mistakes compile without complaint and leave the field blank on the device, so test with a real ID before fieldwork. We ran each of these through pyxform and ODK Validate:

You wroteWrite insteadWhat you will see
PULLDATA(...)pulldata(...)cannot handle function 'PULLDATA'
pulldata('households.csv', ...)pulldata('households', ...)No error, field stays blank
pulldata(households, head_name, hh_id, ${hh_id})Quote the first three: 'households', 'head_name', 'hh_id'No error, field stays blank
pulldata('households', 'head_name', 'hh_id')Add the value: ..., 'hh_id', ${hh_id})No error, field stays blank
File named household list.csvhousehold_list.csv and 'household_list'No error, blank in Enketo
The same ID on two rows of the CSVOne row per IDNo error, the first row wins every time

For other compile messages, see common XLSForm errors, or paste your form into the free XLSForm validator.

Does pulldata() work the same in ODK, KoboToolbox and SurveyCTO?

The syntax is the same on all three: pulldata('file', 'column_to_return', 'column_to_search', value), with the file name given without .csv. What differs:

PlatformWhat to know
ODK Collect and CentralWorks offline once the form and its CSV are downloaded. instance() with csv-external is the documented alternative
KoboToolbox (KoboCollect and Enketo web forms)Upload the CSV under Settings, then Media. Enketo fails silently on file names with spaces
SurveyCTOSame function in the calculation column; skip logic goes in a column named relevance

A form that uses these examples moves between the three platforms as long as the CSV travels with it.

Why preload instead of asking again

Every value you pull from the baseline is a question the respondent does not answer twice, and a mismatch you can catch at the door instead of during cleaning. Showing "Household head: Rahima Begum" before the first question tells the enumerator they are in the right house. A constraint against the baseline household size catches a roster that lost three people while the household is still there to explain why.

Tip:

You can describe the lookup in plain language: "pull the head's name and village from households.csv by household ID, and stop the enumerator if the ID is not on the list". LooprAI writes the calculate and constraint rows, then compiles the form with pyxform before anything is saved.

Build my first form free

Next: XLSForm calculation examples →

Frequently Asked Questions

How do I use pulldata() in XLSForm?

Add a calculate row with pulldata('households', 'head_name', 'hh_id', ${hh_id}) in the calculation column, then upload households.csv as a form attachment. The arguments are the file name without .csv, the column to return, the column to search, and the value to find.

Why does pulldata() return a blank value?

Usually a file or column name does not match exactly, the ID differs in case or has a stray space, or the device still has an old CSV. pulldata() returns an empty string instead of an error, so test with an ID you know is in the file.

Do I include .csv in the pulldata() file name?

No. Write pulldata('households', ...) for a file called households.csv. Writing 'households.csv' compiles without an error but the lookup finds nothing, so the field stays blank on every device. The same rule applies to csv-external rows and instance() names.

How do I use a number pulled with pulldata()?

Wrap it in number() or int(), for example number(pulldata('households', 'hh_size', 'hh_id', ${hh_id})). A CSV has no data types, so pulldata() always returns text. When the ID is not in the file, the result is empty, so let that case through in constraints.

How do I update the CSV used by pulldata() during fieldwork?

Upload the new file to a new form draft and publish it in ODK Central, or replace it under Settings then Media and redeploy in KoboToolbox. Enumerators then download the blank form again. Test with an ID that exists only in the new file to confirm the update arrived.

Is pulldata() the same in KoboToolbox and SurveyCTO?

Yes. ODK, KoboToolbox and SurveyCTO all use pulldata('file', 'column_to_return', 'column_to_search', value) with the file name given without .csv. Only the upload step differs: draft attachments in ODK Central, Settings then Media in Kobo, and form attachments in SurveyCTO.

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.