SurveyLoopr
Guides
11 min read

ODK Entity Case Studies: Close Cases, Track Visits

Mehrab Ali
Mehrab AliPublished November 9, 2026

An ODK entity list is a shared register on your ODK Central server: one form adds records to it, later forms pick a record from it, and their answers update that record. Register a household or a case once, follow it up as often as you need, and close it when the work is done.

This guide works through five field situations, each with a form you can copy. Every example compiles with pyxform, the converter ODK Central uses. If you are new to entities, read ODK Entities and Longitudinal Workflow first for the concepts.

How does an ODK entity list work?

An entity list is a table that lives on the server, next to your forms. Each row is one entity (a household, a case, a beneficiary) and each column is a property.

  • Forms that create entities have an entities sheet with a list_name and a label. Answers marked with save_to become the new entity's properties.
  • Forms that read entities use select_one_from_file <list>.csv. ODK Central sends the list to every phone that has the form, so it works offline.
  • Forms that update entities read the list, store the chosen entity's ID, and name it in the entity_id column of their entities sheet. Only answers with save_to change the entity.
  • Every entity has a system ID (name in lookups) and a label (what the enumerator sees in the list).

The submission stays the full record of each visit. The entity holds only the latest values that later forms need: a status, a count, a date, a size.

Tip:

In SurveyLoopr you can also make an entity list without a form: open your server, go to Entity lists, and choose New list. Start it empty or from a spreadsheet of households or schools, add columns, and edit rows. A test drive can do all of this in its own project.

Case study 1: How do I close a case when the survey is done?

A protection team opens a case at the first meeting and follows it up until it is resolved. Closed cases must disappear from the enumerators' phones.

The pattern is a status property. The registration form saves open. The follow-up form hides cases whose status is closed, and saves closed when the case is resolved.

Registration form. Each submission opens one case:

typenamelabelrequiredsave_tocalculation
textcase_codeCase ID from the referral cardyescase_code
textclient_nameClient's nameyesclient_name
select_one districtdistrictDistrictyesdistrict
calculatecase_statusstatus'open'
calculateopened_onopened_ontoday()
surveychoicesentitiessettings
list_namelabel
casesconcat(${case_code}, ' - ', ${client_name})
surveychoicesentitiessettings

The second block is the form's entities sheet. Its label shows "C-104 - Rahima Begum" in the follow-up list, so two clients with the same name stay apart.

Follow-up form. The choice filter hides closed cases, and update_if changes the entity only on the visit that closes it:

typenamelabelrequiredrelevantchoice_filtersave_tocalculation
select_one_from_file cases.csvchosen_caseWhich case are you following up?yesstatus != 'closed'
calculateclientinstance('cases')/root/item[name=${chosen_case}]/client_name
noteclient_noteFollowing up with ${client}.
select_one yes_noresolvedIs the case resolved?yes
textclosure_reasonWhy can the case close?yes${resolved} = '1'closure_reason
calculatenew_statusstatus'closed'
calculateclosed_onclosed_ontoday()
surveychoicesentitiessettings
list_nameentity_idupdate_if
cases${chosen_case}${resolved} = '1'
surveychoicesentitiessettings
list_namenamelabel
yes_no1Yes
yes_no0No
district1North
district2South
district3East
surveychoicessettings

What happens in the field:

  1. The registration form creates "C-104 - Rahima Begum" with status = open.
  2. Each follow-up visit records what happened. While the case is not resolved, the entity does not change.
  3. On the visit that resolves it, update_if is true: the entity gets status = closed, closed_on and closure_reason.
  4. The case leaves the follow-up list at once on the phone that closed it (ODK Collect v2024.3 and newer apply their own entity updates offline), and on other phones when they next sync. It stays in Central, with every visit, for reporting.
Note:

Updating only when the status changes also means fewer offline conflicts: two enumerators visiting the same open case don't both overwrite it.

Case study 2: How do I count visits to each household?

A nutrition programme visits each household every month and needs the number of visits and the date of the last one, without anyone counting by hand.

The follow-up form reads the household's current visits property, adds one, and saves it back:

typenamelabelrequiredsave_tocalculation
select_one_from_file households.csvhouseholdWhich household are you visiting?yes
calculateprevious_visitsinstance('households')/root/item[name=${household}]/visits
calculatevisitsvisitsif(${previous_visits} = '', 1, ${previous_visits} + 1)
calculatelast_visitlast_visittoday()
notevisit_noteThis is visit number ${visits} to this household.
select_one yes_noanyone_homeWas anyone at home?yes
surveychoicesentitiessettings
list_nameentity_id
households${household}
surveychoicesentitiessettings

The if() treats a household with no visits yet as visit 1. Start the households list from a registration form, or upload it as a spreadsheet in Entity lists.

Warning:

Two phones that visit the same household while offline both read the old count, so one visit can be lost from the counter. ODK Central flags that entity as a conflict to review. Assign each household to one enumerator per round, and count visits from submissions in your analysis.

Case study 3: How do I show last round's answer in a panel survey?

A panel survey asks the same households about household size every round. Big changes are often typing mistakes, so the enumerator should see last round's answer and explain a big jump.

typenamelabelrequiredrelevantsave_tocalculation
select_one_from_file households.csvhouseholdWhich household is this?yes
calculatelast_sizeinstance('households')/root/item[name=${household}]/hh_size
notelast_size_noteLast round this household had ${last_size} members.${last_size} != ''
integerhh_sizeHow many people live in this household now?yeshh_size
textsize_change_reasonThat is a change of more than 3 people since last round. What happened?yes${last_size} != '' and abs(${hh_size} - ${last_size}) > 3
calculatesize_roundsize_round'2'
surveychoicesentitiessettings
list_nameentity_id
households${household}
surveychoicesentitiessettings

The entity always holds the latest size, so next round's check compares with this round. Change size_round when you publish the next round's form, so the list shows when each value was last updated.

Case study 4: How do I stop a family collecting a ration twice?

A distribution team works from a beneficiary list it already has in a spreadsheet. Each family may collect once.

You don't need a registration form for this. In SurveyLoopr, open Entity lists, choose New list, and upload the spreadsheet: one row per family, with columns such as ben_code, village and ration_kg. Then publish this distribution form:

typenamelabelrequiredchoice_filtersave_tocalculation
select_one_from_file beneficiaries.csvbeneficiaryWho is collecting today?yesreceived != '1'
calculaterationinstance('beneficiaries')/root/item[name=${beneficiary}]/ration_kg
noteration_noteHand over ${ration} kg.
select_one yes_nohanded_overWas the ration handed over?yes
calculatemark_receivedreceived'1'
calculatereceived_onreceived_ontoday()
surveychoicesentitiessettings
list_nameentity_idupdate_if
beneficiaries${beneficiary}${handed_over} = '1'
surveychoicesentitiessettings

A family that has collected is marked received = 1. It drops off the list at once on the phone that recorded it, and on the other phones when they sync. Phones that stay offline still show the family, so at a site with several phones, sync them between sessions or keep one phone per queue.

Case study 5: How do I register a household that isn't on the list yet?

Listing is never perfect: enumerators find households the list missed. One form can update a household that is on the list and create one that isn't, with create_if and update_if.

typenamelabelrequiredrelevantsave_tocalculation
select_one yes_noon_listIs this household already on the list?yes
select_one_from_file households.csvhouseholdPick the householdyes${on_list} = '1'
textnew_headName of the household headyes${on_list} = '0'
calculatelisted_headinstance('households')/root/item[name=${household}]/hh_head
calculateheadhh_headif(${on_list} = '1', ${listed_head}, ${new_head})
integerhh_sizeHow many people live here?yeshh_size
calculatelast_visitlast_visittoday()
surveychoicesentitiessettings
list_namelabelentity_idcreate_ifupdate_if
householdsconcat(${head}, ' (', ${hh_size}, ')')coalesce(${household}, uuid())${on_list} = '0'${on_list} = '1'
surveychoicesentitiessettings

Two details keep the data clean:

  • head is worked out on both paths. A question that isn't asked is blank, and a blank answer saved with save_to would wipe the property. The calculate keeps the listed name on updates and takes the new name on creates.
  • entity_id is coalesce(${household}, uuid()): the chosen household when updating, a new ID when creating.

What mistakes break entity forms?

MistakeWhat happensFix
The .csv name doesn't match list_nameThe follow-up list is emptyUse exactly select_one_from_file cases.csv for the list cases
No entity_id in a follow-up formEvery visit creates a new entityPut the chosen entity's field in entity_id
save_to on a question that can be skippedA skipped visit blanks the propertySave a calculate that keeps the old value, or use update_if
Testing in a form draftNo entity is created or updatedTest in a separate project, or a test drive, with published forms
Label from one answer onlyTwo "Fatema Khatun" entries look the sameBuild the label from an ID plus a name
Phones on an old ODK CollectEntity forms don't downloadUse Collect v2024.3 or newer

Can I try entity lists before buying a server?

Yes. A SurveyLoopr test drive is a free 14-day ODK project on a shared server, and it can make entity lists, add columns, upload rows and publish entity forms like any project. Your test drive shows only your own project. Other people's projects, forms and phones on the shared server are never shown to you.

Publish the registration form from case study 1, register two cases from your phone, then publish the follow-up form and close one. Watch it disappear from the list in Entity lists and on the phone.

Start a free test drive

Read how entity workflows are planned

Frequently Asked Questions

How do I close a case in ODK Entities?

Save a status property of 'open' when the case is registered. In the follow-up form, filter the case list with status != 'closed', save 'closed' to status, and set update_if to the answer that resolves the case. Other phones drop it at their next sync.

How do I hide closed entities from a follow-up form?

Add a choice filter to the select_one_from_file question that tests the entity's property, for example status != 'closed'. The phone checks the filter against its copy of the list: an entity closed on that phone disappears at once, one closed elsewhere after the next sync.

Can an ODK entity property count visits?

Yes. Read the current count with instance('households')/root/item[name=${household}]/visits, add one in a calculate, and save it back with save_to. Treat a blank count as zero, and count visits from submissions too, because offline visits can overlap.

Do I need a registration form to use entities?

No. On ODK Central v2024.1 or newer you can create an entity list and add entities from a CSV file. In SurveyLoopr, open Entity lists, choose New list, and upload a spreadsheet; follow-up forms can then read and update those entities.

Why does a skipped question blank my entity property?

A question that is not relevant has an empty value, and save_to writes that empty value to the entity. Save a calculate that keeps the old value from the list instead, or limit the update with update_if so the entity changes only when the answer exists.

Can a SurveyLoopr test drive use entity lists?

Yes. A test drive is a real ODK project on SurveyLoopr's shared server. It can create entity lists, add columns, upload rows and publish entity forms, and it shows only your own project, never anyone else's forms, phones or data.

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.