A1  ·  fx  =convert(survey)

Spreadsheets in.
XForms out.

rxform converts XLSForm workbooks into ODK XForm XML — the format ODK Collect, Enketo and KoboToolbox speak. A Rust implementation of pyxform: one fast static binary, output checked form-by-form against the original, and error messages that point at the exact cell.

source ↗
survey.xlsx — sheet survey
type name label
2 select_one yes_no good_day Good day today?
3 begin repeat visit Each visit
4 geopoint spot Where are you?
form.xml — ODK XForm
<select1 ref="/data/good_day">
  <itemset nodeset="instance('yes_no')/root/item">
    <value ref="name"/>
  </itemset>
</select1>
<repeat nodeset="/data/visit">…</repeat>
<bind type="geopoint" nodeset="/data/visit/spot"/>

34/34

reference forms identical to pyxform 4.5.0

100%

of the xlsform.org specification

65

tests pinning output & diagnostics

1

static binary — no Python required

B7 · when forms break

Errors that name
the cell, not the stack.

A typo in row 3 should not read like a crash report. Every rxform error carries the sheet, row and column where it happened — plus a probable cause: the closest question type, the choice list you probably meant, the begin row a stray end group forgot about.

  • misspelled types & ${references} → “did you mean …?”
  • unclosed groups point at their begin row
  • ambiguous names caught at the place of use
  • entities, triggers & choice lists cross-checked
terminal
$ rxform household_survey.xlsx
error: [sheet 'survey', row 12, column 'type'] unknown question type 'selct_one districts'
  probable cause: did you mean 'select_one'?

$ rxform household_survey.xlsx
error: [sheet 'survey', row 48, column 'relevant'] '${distrct}' does not match
the name of any question or group
  probable cause: did you mean 'district'?

$ rxform household_survey.xlsx
household_survey.xml

C2 · sheet “features”

The whole spec. In one sheet.

Everything on xlsform.org converts — each row below was verified against pyxform’s own output.

type name label
2 select_one spec question_types Every type: text → geo traces, ranges, media, barcodes, rank, calculate, audit, metadata preloads
3 begin repeat structure Groups, repeats (+ counts), legacy loops, table-list, flat forms — nested arbitrarily
4 select_multiple langs multilanguage label::Idioma (código), translated hints, media & messages via <itext>, guidance hints
5 select_one_from_file external_data CSV/XML/GeoJSON selects, pulldata(), search(), last-saved, select_one_external + itemsets.csv
6 text entities Create & update entity lists: save_to, create_if/update_if, offline version tracking
7 calculate logic ${refs} → xpaths, cascading choice_filter, dynamic defaults, trigger actions, randomize
8 note settings Titles, ids, versions, styles, encryption keys, namespaces, custom attributes, SMS compact tags

D4 · under the hood

A pipeline you can point at.

Workbook → normalized cells → question tree → validation with locations → XForm XML, printed with pyxform’s conventions so existing tooling sees familiar output.

rxform pipeline: xlsx workbook through parser, validation and generator to XForm XML
Mapping from XLSForm sheets to the parts of the generated XForm document

E9 · quick start

CLI or crate. Your call.

Prebuilt binaries for every platform ship with each GitHub release — Linux (static musl + .deb), macOS and Windows.

macOS · Homebrew

Debian · Ubuntu

$ sudo dpkg -i rxform_*.deb

get the .deb ↗

Windows · Scoop

Anywhere · Cargo

command line
# install once
$ cargo install rxform

# convert — writes survey.xml next to the input
$ rxform survey.xlsx

# or choose the destination / print to stdout
$ rxform survey.xlsx -o out/form.xml
$ rxform survey.xlsx --stdout
library — docs.rs/rxform
// cargo add rxform
use std::path::Path;

let conversion = rxform::convert(Path::new("survey.xlsx"))?;
std::fs::write("form.xml", &conversion.xml)?;

if let Some(csv) = conversion.itemsets_csv {
    std::fs::write("itemsets.csv", csv)?; // external selects
}