ODK Entity Case Studies: Close Cases, Track Visits
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
entitiessheet with alist_nameand alabel. Answers marked withsave_tobecome 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_idcolumn of theirentitiessheet. Only answers withsave_tochange the entity. - Every entity has a system ID (
namein 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.
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:
| type | name | label | required | save_to | calculation |
|---|---|---|---|---|---|
| text | case_code | Case ID from the referral card | yes | case_code | |
| text | client_name | Client's name | yes | client_name | |
| select_one district | district | District | yes | district | |
| calculate | case_status | status | 'open' | ||
| calculate | opened_on | opened_on | today() |
| list_name | label |
|---|---|
| cases | concat(${case_code}, ' - ', ${client_name}) |
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:
| type | name | label | required | relevant | choice_filter | save_to | calculation |
|---|---|---|---|---|---|---|---|
| select_one_from_file cases.csv | chosen_case | Which case are you following up? | yes | status != 'closed' | |||
| calculate | client | instance('cases')/root/item[name=${chosen_case}]/client_name | |||||
| note | client_note | Following up with ${client}. | |||||
| select_one yes_no | resolved | Is the case resolved? | yes | ||||
| text | closure_reason | Why can the case close? | yes | ${resolved} = '1' | closure_reason | ||
| calculate | new_status | status | 'closed' | ||||
| calculate | closed_on | closed_on | today() |
| list_name | entity_id | update_if |
|---|---|---|
| cases | ${chosen_case} | ${resolved} = '1' |
| list_name | name | label |
|---|---|---|
| yes_no | 1 | Yes |
| yes_no | 0 | No |
| district | 1 | North |
| district | 2 | South |
| district | 3 | East |
What happens in the field:
- The registration form creates "C-104 - Rahima Begum" with
status=open. - Each follow-up visit records what happened. While the case is not resolved, the entity does not change.
- On the visit that resolves it,
update_ifis true: the entity getsstatus=closed,closed_onandclosure_reason. - 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.
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:
| type | name | label | required | save_to | calculation |
|---|---|---|---|---|---|
| select_one_from_file households.csv | household | Which household are you visiting? | yes | ||
| calculate | previous_visits | instance('households')/root/item[name=${household}]/visits | |||
| calculate | visits | visits | if(${previous_visits} = '', 1, ${previous_visits} + 1) | ||
| calculate | last_visit | last_visit | today() | ||
| note | visit_note | This is visit number ${visits} to this household. | |||
| select_one yes_no | anyone_home | Was anyone at home? | yes |
| list_name | entity_id |
|---|---|
| households | ${household} |
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.
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.
| type | name | label | required | relevant | save_to | calculation |
|---|---|---|---|---|---|---|
| select_one_from_file households.csv | household | Which household is this? | yes | |||
| calculate | last_size | instance('households')/root/item[name=${household}]/hh_size | ||||
| note | last_size_note | Last round this household had ${last_size} members. | ${last_size} != '' | |||
| integer | hh_size | How many people live in this household now? | yes | hh_size | ||
| text | size_change_reason | That is a change of more than 3 people since last round. What happened? | yes | ${last_size} != '' and abs(${hh_size} - ${last_size}) > 3 | ||
| calculate | size_round | size_round | '2' |
| list_name | entity_id |
|---|---|
| households | ${household} |
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:
| type | name | label | required | choice_filter | save_to | calculation |
|---|---|---|---|---|---|---|
| select_one_from_file beneficiaries.csv | beneficiary | Who is collecting today? | yes | received != '1' | ||
| calculate | ration | instance('beneficiaries')/root/item[name=${beneficiary}]/ration_kg | ||||
| note | ration_note | Hand over ${ration} kg. | ||||
| select_one yes_no | handed_over | Was the ration handed over? | yes | |||
| calculate | mark_received | received | '1' | |||
| calculate | received_on | received_on | today() |
| list_name | entity_id | update_if |
|---|---|---|
| beneficiaries | ${beneficiary} | ${handed_over} = '1' |
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.
| type | name | label | required | relevant | save_to | calculation |
|---|---|---|---|---|---|---|
| select_one yes_no | on_list | Is this household already on the list? | yes | |||
| select_one_from_file households.csv | household | Pick the household | yes | ${on_list} = '1' | ||
| text | new_head | Name of the household head | yes | ${on_list} = '0' | ||
| calculate | listed_head | instance('households')/root/item[name=${household}]/hh_head | ||||
| calculate | head | hh_head | if(${on_list} = '1', ${listed_head}, ${new_head}) | |||
| integer | hh_size | How many people live here? | yes | hh_size | ||
| calculate | last_visit | last_visit | today() |
| list_name | label | entity_id | create_if | update_if |
|---|---|---|---|---|
| households | concat(${head}, ' (', ${hh_size}, ')') | coalesce(${household}, uuid()) | ${on_list} = '0' | ${on_list} = '1' |
Two details keep the data clean:
headis worked out on both paths. A question that isn't asked is blank, and a blank answer saved withsave_towould wipe the property. The calculate keeps the listed name on updates and takes the new name on creates.entity_idiscoalesce(${household}, uuid()): the chosen household when updating, a new ID when creating.
What mistakes break entity forms?
| Mistake | What happens | Fix |
|---|---|---|
The .csv name doesn't match list_name | The follow-up list is empty | Use exactly select_one_from_file cases.csv for the list cases |
No entity_id in a follow-up form | Every visit creates a new entity | Put the chosen entity's field in entity_id |
save_to on a question that can be skipped | A skipped visit blanks the property | Save a calculate that keeps the old value, or use update_if |
| Testing in a form draft | No entity is created or updated | Test in a separate project, or a test drive, with published forms |
| Label from one answer only | Two "Fatema Khatun" entries look the same | Build the label from an ID plus a name |
| Phones on an old ODK Collect | Entity forms don't download | Use 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 driveRead 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.