XForm / ODK Reference
How SRP surveys map to XForm XML for ODK Collect, KoboToolbox, and compatible tools
01Supported Question Types
The following SRP question types have direct XForm equivalents and export without warnings or errors.
| SRP Type | Category | XForm Mapping |
|---|---|---|
OpenEnded |
Text | <input> with type="string" |
LongText |
Text | <input> with appearance="multiline" |
Number |
Numeric | <input> with type="decimal" |
Age |
Numeric | <input> with type="integer" + constraint |
PhoneNumber |
Text | <input> with type="string" + regex constraint |
Date |
Temporal | <input> with type="date" |
SingleSelect |
Choice | <select1> |
MultiSelect |
Choice | <select> |
Dropdown |
Choice | <select1> with appearance="minimal" |
Rating |
Scale | <input> with type="integer" + rating constraint |
Ranking |
Order | <select> with appearance="ranking" |
SingleSelectMatrix |
Matrix | Flattened to one <select1> per row. See Section 05. |
MultiSelectMatrix |
Matrix | Flattened to one <select> per row. See Section 05. |
NetPromoterScore |
Scale | <input> with type="integer" + 0–10 constraint |
YesNo |
Choice | <select1> with "Yes" / "No" items |
ImageCapture |
Media | <upload> with mediatype="image/*" |
Geolocation |
Location | <input> with type="geopoint" |
02Unsupported Question Types
These SRP question types have no XForm equivalent and will cause export validation to fail with an Error. Remove or replace them before exporting.
| SRP Type | Category | Reason |
|---|---|---|
CardSort |
Specialized | Drag-and-drop card categorization. No XForm equivalent. |
CardRating |
Specialized | Card-based rating interface. No XForm equivalent. |
MaxDiff |
Specialized | Maximum difference scaling (best-worst). No standard XForm mapping. |
ConstantSum |
Specialized | Allocate a fixed total across items. No XForm equivalent. |
Category |
Specialized | Drag items into named categories. No XForm equivalent. |
Autosuggest |
Specialized | Typeahead search with external data. No XForm equivalent. |
ConjointChoice |
Specialized | Conjoint analysis with attribute profiles. No XForm equivalent. |
EmotionSelector |
Specialized | Emotion/sentiment picker. No XForm equivalent. |
TextHighlighter |
Specialized | In-text highlighting/annotation. No XForm equivalent. |
USAddress |
Address | Multi-field US address form. Could theoretically flatten but not implemented. |
InternationalAddress |
Address | Multi-field international address form. Could theoretically flatten but not implemented. |
03Supported Language Features
required → <bind required="true()">
When a question has required true in the DSL, the XForm bind emits required="true()". ODK Collect and compatible tools will enforce this at submission time.
open_ended "name" do
text "Your name?"
required true
end
<bind nodeset="/data/name"
type="string"
required="true()"/>
show_only_if (segments) → <bind relevant="...XPath...">
Segment-based conditional display is translated to XPath relevant expressions. When a question references a segment, the renderer resolves which source question and value that segment corresponds to. Multiple top-level show_only_if entries use OR logic. See Section 06 for the full translation process.
show_only_if (group references / AND logic) → combined XPath with and
When show_only_if references a group, the group members (segments) are combined with and logic. Multiple groups at the top level are joined with or.
# If group "qualified" has members ["is_adult", "has_consent"]:
relevant="(${age_q} = 'yes' and ${consent_q} = 'agreed')"
action :skip_to → relevance conditions
Skip-to actions on options and rows are translated to XForm relevance expressions on the target questions/pages. The XForm renderer uses the segment registry to determine which questions should be hidden based on the respondent's selection path. This is a Supported mapping.
Special options → <item> with canonical values
The special option directives add_none_option, add_dont_know_option, and add_other_option are appended as <item> elements using their canonical values.
| DSL Directive | Label | Value |
|---|---|---|
add_none_option |
None | __none__ |
add_dont_know_option |
Don't Know | __dont_know__ |
add_other_option |
Other | __other__ |
Segments on special options are also collected into the segment registry and produce valid XPath references.
Pages → <group appearance="field-list">
Each SRP page is wrapped in an XForm group with appearance="field-list", which displays all questions on the page together (matching SRP's page-based layout). The group ref uses a sanitized version of the page name, and the group label comes from the page title.
Constraints
The renderer emits XForm constraint and jr:constraintMsg attributes for types that have built-in validation rules:
Age:constraint=". >= 0 and . <= 150"— message: "Age must be between 0 and 150"PhoneNumber:constraint="regex(., '^[0-9+\-\s()]+$')"— message: "Please enter a valid phone number"Numberwithmin_value/max_value:constraint=". >= N and . <= M"— message describes the allowed range
Submission URL → <submission action="...">
When you export via the ODK Export page, you can optionally enter a Submission URL. This injects a <submission> element into the XForm <model>, telling ODK Collect where to send responses.
Submission URL:
https://your-odk-server.example.com/submission
<model>
<instance>...</instance>
<submission
action="https://your-odk-server.example.com/submission"/>
<!-- binds ... -->
</model>
Leave the field blank to omit the <submission> element entirely.
04Unsupported / Degraded Features
The following DSL features are either ignored (with a warning) or blocked (with an error) during XForm export.
| Feature | Severity | Behavior |
|---|---|---|
randomized (options) |
Warning | Ignored. Options export in the order defined in the DSL. XForm has no client-side randomization mechanism. |
randomize_rows |
Warning | Ignored. Matrix rows export in defined order. |
randomize_columns |
Warning | Ignored. Matrix columns export in defined order. |
Text piping {{...}} |
Warning | Placeholders appear as literal text (e.g., the respondent sees {{name}}). XForm has no client-side text substitution. |
options_from |
Error | Blocked. Dynamic option references require client-side JavaScript which XForm cannot execute. Survey must use static options for XForm export. |
rows_from |
Error | Blocked. Dynamic row references cannot be resolved at export time. |
columns_from |
Error | Blocked. Dynamic column references cannot be resolved at export time. |
anchored options |
Warning | Ignored. The option order is preserved as defined, but the anchoring attribute has no effect in XForm. |
hidden questions |
Warning | Hidden questions are included in the instance and binds as calculate fields (calculate="", readonly="true()"), but are omitted from the XForm body so they collect no input. They are present in the submission data with an empty value. |
Page timing (min_seconds, max_seconds, show_timer) |
Warning | Ignored. XForm/ODK has no page-level timing mechanism. Pages export without timing constraints. |
Page randomized |
Warning | Ignored. Page randomization has no XForm equivalent. Pages export in defined order. |
action :terminate |
Warning | Converted to a skip-to-end-of-survey. XForm has no "terminate with disqualification" concept; the best approximation is skipping remaining questions. |
action :complete |
Warning | Converted to a skip-to-end-of-survey. Functionally identical to terminate in the XForm output. |
add_why_field |
Warning | Omitted. The "why" explanation text field has no XForm equivalent. |
Complex matrix conditions (if_multiple_cells, if_any_cells, if_column_count) |
Warning | May not translate precisely to XForm XPath. Simple conditions work; complex multi-cell conditions may produce approximate results. |
05Matrix Question Flattening
XForm has no native matrix question type. The SRP XForm renderer flattens each matrix question by creating a separate select question for every row. Each flattened question shares the same set of column options.
Naming Convention
The node name for each row is constructed as:
{matrix_name}_{sanitized_row_text}
Where sanitized_row_text is the row label converted to lowercase, stripped of non-alphanumeric characters, with spaces replaced by underscores.
Label Format
Each flattened question's label combines the matrix question text and the row text:
{question_text} - {row_text}
Full Example
single_select_matrix "satisfaction" do
text "Rate each area"
row "Speed"
row "Quality"
column "Poor"
column "Fair"
column "Good"
end
<satisfaction_speed/>
<satisfaction_quality/>
<bind nodeset="/data/satisfaction_speed"
type="string"/>
<bind nodeset="/data/satisfaction_quality"
type="string"/>
<select1 ref="/data/satisfaction_speed">
<label>Rate each area - Speed</label>
<item>
<label>Poor</label>
<value>poor</value>
</item>
<!-- Fair, Good items... -->
</select1>
<select1 ref="/data/satisfaction_quality">
<label>Rate each area - Quality</label>
<!-- same <item> children -->
</select1>
MultiSelectMatrix works identically, but uses <select> instead of <select1>.
06Segment → XPath Translation
How It Works
The XForm renderer builds a segment registry at render time by scanning all questions for segments defined on options, special options, matrix rows, and matrix columns. Each segment entry records:
- question_name — the XForm node name of the source question
- value — the sanitized option/row/column value that activates the segment (or
nilfor "any value" segments)
Translation Rules
| Segment Source | XPath Output |
|---|---|
| Option with value | ${question_name} = 'value' |
| Option without value (any selection) | ${question_name} != '' |
| Special option | ${question_name} = '__canonical_value__' |
Matrix row (with if_column) |
${matrix_row_node} = 'column_value' |
| Matrix row (no column filter) | ${matrix_row_node} != '' |
| Matrix column | ${question_name} = 'column_value' |
Combining Logic
- Multiple top-level
show_only_ifentries are joined with or. - Group references expand their member segments, joined with and, wrapped in parentheses.
Full Example
page "screening" do
single_select "owns_car" do
text "Do you own a car?"
option "Yes" do
segment "car_owner"
end
option "No"
end
end
page "car_details" do
open_ended "car_make" do
text "What make is your car?"
show_only_if "car_owner"
end
end
<!-- Instance -->
<owns_car/>
<car_make/>
<!-- Bindings -->
<bind nodeset="/data/owns_car"
type="string"/>
<bind nodeset="/data/car_make"
type="string"
relevant="${owns_car} = 'yes'"/>
<!-- Body -->
<select1 ref="/data/owns_car">
<label>Do you own a car?</label>
<item><label>Yes</label><value>yes</value></item>
<item><label>No</label><value>no</value></item>
</select1>
<input ref="/data/car_make">
<label>What make is your car?</label>
</input>
The segment "car_owner" is resolved to the option "Yes" on question owns_car. The sanitized value is 'yes'. The resulting relevant expression is ${owns_car} = 'yes', which ODK Collect evaluates to show or hide car_make based on the respondent's answer.
Group Reference Example
# Given a group "qualified" with members ["is_adult", "gave_consent"]
# where is_adult → age_check = "18_plus"
# and gave_consent → consent_q = "yes"
show_only_if :group, "qualified"
# Produces XPath:
# relevant="(${age_check} = '18_plus' and ${consent_q} = 'yes')"