Production Ready ✅ | Version 1.0 | Complete Reference & Quick Guide
- Quick Start
- Overview
- Basic Usage
- Real-World Examples
- Query Reference
- Troubleshooting
- Production Checklist
Transform static dropdowns into dynamic, data-driven selections:
// ❌ Static (gets outdated)
{
"village": {
"type": "string",
"enum": ["kopria", "lorenkacho", "chare"]
}
}
// ✅ Dynamic (always current)
{
"village": {
"type": "string",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "household",
"params": {},
"valueField": "data.hh_village_name",
"labelField": "data.hh_village_name",
"distinct": true
}
}
}{
"fieldName": {
"type": "string",
"title": "Display Title",
"x-dynamicEnum": {
"function": "getDynamicChoiceList", // Always this value
"query": "formType", // Form to query (e.g., "household", "hh_person")
"params": {}, // Filters (see below)
"valueField": "data.fieldName", // Path to value in observations
"labelField": "data.fieldName", // Path to display label
"distinct": true // true = unique values only
}
}
}| Parameter | Type | Required | Description | Example |
|---|---|---|---|---|
function |
string | ✅ Yes | Must be "getDynamicChoiceList" |
"getDynamicChoiceList" |
query |
string | ✅ Yes | Form type to query | "household", "hh_person" |
valueField |
string | ✅ Yes | Path to value (use data. prefix) |
"data.hh_village_name" |
labelField |
string | No | Path to label (defaults to valueField) | "data.names" |
params |
object | No | Filter parameters | {"sex": "male"} |
distinct |
boolean | No | Return only unique values | true or false |
Dynamic Choice Lists enable:
✅ Data-driven dropdowns - Load from existing observations
✅ Filtered queries - Filter by static parameters
✅ ODK-X parity - Replaces linked tables and "select person"
Note: Cascading dropdowns (template parameters like {{data.field}}) are not supported. Use static filters only.
┌─────────────────────────────────────────────────────────┐
│ Form Schema (JSON) │
│ - x-dynamicEnum configuration │
└───────────────────────┬─────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────┐
│ DynamicEnumControl (React Component) │
│ - Resolves templates: {{data.field}} │
│ - Calls getDynamicChoiceList │
└───────────────────────┬─────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────┐
│ builtinExtensions.ts (formulus-formplayer) │
│ - Builds WHERE clause from params + whereClause │
│ - Handles age_from_dob() via JS filtering │
│ - Calls window.formulus.getObservationsByQuery │
└───────────────────────┬─────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────┐
│ WebView Bridge (formulus-load.js / FormulusInjection) │
│ - getObservationsByQuery sends message to native host │
└───────────────────────┬─────────────────────────────────┘
│
┌───────────────────────▼─────────────────────────────────┐
│ FormulusMessageHandlers → FormService │
│ - filterObservationsByWhereClause supports: │
│ - data.field = 'value' (builtinExtensions) │
│ - json_extract(data, '$.field') = 'value' (extensions) │
│ - Queries WatermelonDB, returns filtered observations │
└─────────────────────────────────────────────────────────┘
{
"village": {
"type": "string",
"title": "Select Village",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "household",
"params": {},
"valueField": "data.hh_village_name",
"labelField": "data.hh_village_name",
"distinct": true
}
}
}Result: Shows all unique villages from household observations.
{
"male_person": {
"type": "string",
"title": "Select Male Participant",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "hh_person",
"params": {
"sex": "male"
},
"valueField": "observationId",
"labelField": "data.names",
"distinct": false
}
}
}Result: Shows only males from hh_person observations.
{
"village": {
"type": "string",
"title": "Select Village",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "household",
"params": {},
"valueField": "data.hh_village_name",
"labelField": "data.hh_village_name",
"distinct": true
}
},
"subvillage": {
"type": "string",
"title": "Select Subvillage",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "household",
"params": {
"hh_village_name": "kopria"
},
"valueField": "data.hh_subvillage",
"labelField": "data.hh_subvillage",
"distinct": true
}
}
}Note: Template parameters ({{data.field}}) are not supported. Use static filter values.
Basic - All Persons:
{
"person_id": {
"type": "string",
"title": "Select Person",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "hh_person",
"params": {},
"valueField": "observationId",
"labelField": "data.names",
"distinct": false
}
}
}Filtered by Village (Static):
{
"person_id": {
"type": "string",
"title": "Select Person from Village",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "hh_person",
"params": {
"p_hh_res_validation": "kopria"
},
"valueField": "observationId",
"labelField": "data.names",
"distinct": false
}
}
}Note: Template parameters are not supported. Use static values.
Filtered by Sex:
{
"female_participant": {
"type": "string",
"title": "Select Female Participant",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "hh_person",
"params": {
"sex": "female"
},
"valueField": "observationId",
"labelField": "data.names",
"distinct": false
}
}
}Multiple Filters:
{
"participant": {
"type": "string",
"title": "Select Female from Kopria",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "hh_person",
"params": {
"sex": "female",
"p_hh_res_validation": "kopria"
},
"valueField": "observationId",
"labelField": "data.names",
"distinct": false
}
}
}{
"rank_1": {
"type": "string",
"title": "Most Influential Person (Rank #1)",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "hh_person",
"params": {
"sex": "male"
},
"valueField": "observationId",
"labelField": "data.names",
"distinct": false
}
},
"rank_2": {
"type": "string",
"title": "Rank #2",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "hh_person",
"params": {
"sex": "male"
},
"valueField": "observationId",
"labelField": "data.names",
"distinct": false
}
}
}Note: Template parameters are not supported. Use static filters only.
{
"p_kin_sibling_id": {
"type": "string",
"title": "Select Sibling",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "hh_person",
"params": {},
"valueField": "observationId",
"labelField": "data.names",
"distinct": false
}
}
}With Sex Filter (for mother/father):
{
"p_kin_mother_id": {
"type": "string",
"title": "Select Mother",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "hh_person",
"params": {
"sex": "female"
},
"valueField": "observationId",
"labelField": "data.names",
"distinct": false
}
}
}Adults Only (18+) - Using age_from_dob():
{
"adult_participant": {
"type": "string",
"title": "Select Adult (18+)",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "hh_person",
"params": {
"whereClause": "age_from_dob(data.dob) >= 18"
},
"valueField": "observationId",
"labelField": "data.names",
"distinct": false
}
}
}Age Range - Using age_from_dob():
{
"working_age": {
"type": "string",
"title": "Select Working Age Adult (18-65)",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "hh_person",
"params": {
"whereClause": "age_from_dob(data.dob) >= 18 AND age_from_dob(data.dob) <= 65"
},
"valueField": "observationId",
"labelField": "data.names",
"distinct": false
}
}
}Using p_age_participant_estimate (if available):
{
"adult_participant": {
"type": "string",
"title": "Select Adult (18+)",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "hh_person",
"params": {
"whereClause": "data.p_age_participant_estimate >= 18"
},
"valueField": "observationId",
"labelField": "data.names",
"distinct": false
}
}
}Combined Filters (Static + WHERE clause):
{
"adult_male": {
"type": "string",
"title": "Select Adult Male (18+)",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "hh_person",
"params": {
"sex": "male",
"whereClause": "age_from_dob(data.dob) >= 18"
},
"valueField": "observationId",
"labelField": "data.names",
"distinct": false
}
}
}Note: age_from_dob(data.dob) calculates age from date of birth in JavaScript. Use this when you need accurate age calculations. Static age fields like p_age_participant_estimate can also be used directly in WHERE clauses.
Form Fields: data.fieldName
data.hh_village_name // Village name from household
data.names // Person name from hh_person
data.sex // Sex from hh_person
data.p_age_participant_estimate // Age
Metadata Fields:
observationId // Unique ID
formType // Form type (e.g., "household")
createdAt // Creation timestamp
updatedAt // Update timestamp
isDraft // Is draft?
isDeleted // Is deleted?
| Operator | Description | Example |
|---|---|---|
= |
Equals | data.sex = 'male' |
!= or <> |
Not equals | data.sex != 'male' |
< |
Less than | age_from_dob(data.dob) < 18 |
> |
Greater than | age_from_dob(data.dob) > 65 |
<= |
Less than or equal | age_from_dob(data.dob) <= 18 |
>= |
Greater than or equal | age_from_dob(data.dob) >= 18 |
AND |
Logical AND | age_from_dob(data.dob) >= 18 AND age_from_dob(data.dob) <= 65 |
OR |
Logical OR | data.hh_village_name = 'kopria' OR data.hh_village_name = 'chare' |
NOT |
Logical NOT | NOT (age_from_dob(data.dob) >= 18 AND age_from_dob(data.dob) <= 65) |
() |
Grouping | (age_from_dob(data.dob) >= 18 AND age_from_dob(data.dob) <= 30) OR age_from_dob(data.dob) >= 50 |
Special Functions:
age_from_dob(data.dob)- Calculates age from date of birth field. Use for accurate age filtering.
Parameter Filtering (Simple Equality):
{
"params": {
"sex": "male",
"p_hh_res_validation": "kopria"
}
}Equivalent to: WHERE data.sex = 'male' AND data.p_hh_res_validation = 'kopria'
WHERE Clause (Complex Logic):
{
"params": {
"whereClause": "data.p_age_participant_estimate >= 18 AND data.p_age_participant_estimate <= 65"
}
}Combined (Static Filters + WHERE Clause):
{
"params": {
"sex": "female",
"whereClause": "age_from_dob(data.dob) >= 18"
}
}Equivalent to: WHERE data.sex = 'female' AND age_from_dob(data.dob) >= 18
WHERE Clause Format:
- WHERE clauses using
data.field = 'value'format are automatically converted tojson_extract(data, '$.field') = 'value'format internally. age_from_dob()conditions are filtered in JavaScript after fetching observations.- Static filter parameters (like
sex: "male") are automatically converted to the correct SQL format.
Note: Template syntax ({{data.fieldName}}) is not supported. Use static filter values only.
✅ Use distinct: true for unique values:
{"distinct": true} // Returns ["kopria", "chare"] instead of 100 duplicates✅ Filter at query level:
// Good - filters at query level
{"params": {"p_hh_res_validation": "kopria"}}
// Bad - loads all then filters in UI
{"params": {}}✅ Combine filters for precision:
{
"params": {
"sex": "female",
"p_hh_res_validation": "kopria",
"whereClause": "data.p_age_participant_estimate >= 18"
}
}Possible Causes:
-
No observations exist
- Check: Do saved observations exist for the queried form type?
- Solution: Create at least one observation
-
Field path doesn't exist
- Check: Is
valueFieldspelled correctly? Does it match the form schema? - Solution: Verify field name in form schema, use correct
data.prefix
- Check: Is
-
All values are null/empty
- Check: Are observations actually filled in for this field?
- Solution: Edit observations to populate the field
-
Filter too restrictive
- Check: Remove filters temporarily - do results appear?
- Solution: Relax filters or add observations that match
Debug:
- Open Chrome DevTools (
chrome://inspect) and inspect the Formulus WebView - Verify observations exist for the queried form type in the database
- If dropdown is empty: check valueField/labelField paths match observation structure
Possible Causes:
-
Filter field name mismatch
- Check: Does param name match database field?
- Example: Using
village_namebut database hashh_village_name - Solution: Use correct field name from form schema
-
Value mismatch (case-sensitive)
- Check: Do values match exactly?
- Example: "Kopria" vs "kopria"
- Solution: Ensure consistent casing
-
Filter value not matching any records
- Check: Do any observations have this exact value?
- Solution: Verify filter value exists in database
Debug:
Verify param names match database field names (e.g. hh_village_name not village_name).
Possible Causes:
-
JSON syntax error
- Check: Missing comma, extra comma, wrong quotes?
- Solution: Validate JSON (use online validator)
-
Function name wrong
- Check: Is
functionexactly"getDynamicChoiceList"? - Solution: Use correct function name (case-sensitive)
- Check: Is
-
Missing required fields
- Check: Are
function,query,valueFieldpresent? - Solution: Add all required fields
- Check: Are
Solution: Set labelField to a human-readable field:
{
"valueField": "observationId",
"labelField": "data.names"
}Solutions:
- Add filters to reduce dataset size
- Use
distinct: truefor categorical fields - Simplify WHERE clause
Chrome DevTools:
1. Connect device via USB
2. Chrome → chrome://inspect
3. Click "Inspect" on Formulus WebView
4. Check Console tab
Metro Bundler:
cd formulus
npx react-native start-
functionis"getDynamicChoiceList" -
querymatches existing form type -
valueFieldpath exists in observations -
labelFieldis human-readable -
distinctset appropriately -
paramsfilter fields exist - Tested with real data
- Dropdown populates with expected choices
- Performance < 1 second load time
✅ Do:
- Use
distinct: truefor unique values - Use parameter filtering for simple cases
- Use meaningful
labelField - Test with actual data
- Use
valueField: "observationId"for record references - Use static filter values
❌ Don't:
- Query all observations without filtering
- Use
distinct: truefor record IDs - Forget
data.prefix in field paths - Use typos in field names
- Query non-existent form types
- Use template parameters (
{{data.field}}) - not supported
Before:
{
"village": {
"type": "string",
"enum": ["kopria", "lorenkacho", "chare"]
}
}After:
{
"village": {
"type": "string",
"x-dynamicEnum": {
"function": "getDynamicChoiceList",
"query": "household",
"params": {},
"valueField": "data.hh_village_name",
"labelField": "data.hh_village_name",
"distinct": true
}
}
}Benefits:
- Automatically updated as data changes
- No schema redeployment needed
- Always reflects current data
| ODK-X Feature | Formulus Equivalent |
|---|---|
| Linked tables with SQL | x-dynamicEnum with query |
| Select person prompt | query: "hh_person" with filters |
| Choice filters | params or whereClause |
query() function |
getDynamicChoiceList |
_id column |
observationId field |
Note: Cascading selects (template parameters) are not supported.
Dynamic Choice Lists provide:
✅ Data-driven dropdowns from local observations
✅ Filtered queries with static parameters
✅ ODK-X feature parity (select person, ranking)
✅ Production-ready with error handling
Note: Cascading dropdowns (template parameters) are not supported.
Implementation:
formulus-formplayer/src/DynamicEnumControl.tsx- Rendererformulus-formplayer/src/builtinExtensions.ts- Query logic, WHERE clause building, age_from_dob filteringformulus-formplayer/public/formulus-load.js- getObservationsByQuery polyfill (ensures correct message routing)formulus/src/webview/FormulusMessageHandlers.ts- onGetObservationsByQuery handlerformulus/src/services/FormService.ts- getObservationsByQuery, filterObservationsByWhereClause
Documentation:
- This file - Complete reference
- Check examples above for similar use case
- Review troubleshooting section
- Check console logs for errors
- Test with simplified schema
Version 1.0 - Production Ready
Last Updated: 2026-02-06