With ODK Web Forms, you can define forms with powerful logic using the spreadsheet-based XLSForm standard. Use our Vue-based frontend or build your own user experience around the engine!
The packages are available on npm.
Warning
This repository is archived
Development of ODK Web Forms has moved to the ODK Central Frontend repository. This allows us to write end-to-end tests that cover the full form-filling experience, which speeds up development and helps us catch regressions early. We timed this change to coincide with Web Forms becoming the default for new forms in ODK Central.
We continue to publish the following packages to npm:
@getodk/web-forms— Vue component for form filling@getodk/xforms-engine— XForms engine@getodk/xpath— XPath evaluator with ODK XForms extensions@getodk/tree-sitter-xpath— XPath grammar for tree-sitter
For fork maintainers: if you maintain a fork of this repository or of Central Frontend with local changes, you should be able to migrate those changes without manually recreating moved files. We recommend planning your update after the Central v2026.2 release in June 2026. For more details and guidance, see the announcement on the ODK forum.
This section is auto generated. Please update feature-matrix.json and then run npm run feature-matrix from the repository's root to update it.
| Feature |
Progress |
|---|---|
| text | ✅ |
| integer | ✅ |
| decimal | ✅ |
| note | ✅ |
| select_one | ✅ |
| select_multiple | ✅ |
| select_*_from_file | ✅ |
| repeat | ✅ |
| group | ✅ |
| geopoint | ✅ |
| geotrace | ✅ |
| geoshape | ✅ |
| start-geopoint | ✅ |
| range | ✅ |
| image | ✅ |
| barcode | |
| audio | ✅ |
| background-audio | |
| video | ✅ |
| file | ✅ |
| date | ✅ |
| time | ✅ |
| datetime | ✅ |
| rank | ✅ |
| csv-external | ✅ |
| acknowledge | ✅ |
| start | ✅ |
| end | ✅ |
| today | ✅ |
| deviceid | ✅ |
| username | ✅ |
| phonenumber | ✅ |
| ✅ | |
| audit |
| Feature |
Progress |
|---|---|
| numbers | ✅ |
| multiline | ✅ |
| url | |
| ex: | |
| thousands-sep | ✅ |
| bearing | |
| vertical | |
| no-ticks | |
| picker | |
| rating | ✅ |
| new | |
| new-front | |
| draw | |
| annotate | |
| signature | |
| no-calendar | |
| month-year | ✅ |
| year | ✅ |
| ethiopian | |
| coptic | |
| islamic | |
| bikram-sambat | |
| myanmar | |
| persian | |
| placement-map | ✅ |
| maps | ✅ |
| hide-input | |
| minimal | ✅ |
| search / autocomplete | ✅ |
| quick | |
| columns-pack | ✅ |
| columns | ✅ |
| columns-n | ✅ |
| no-buttons | ✅ |
| image-map | |
| likert | ✅ |
| map | ✅ |
| field-list | ✅ |
| label | ✅ |
| list-nolabel | ✅ |
| list | ✅ |
| table-list | ✅ |
| counter | |
| hidden-answer | |
| printer | |
| masked | ✅ |
| Feature |
Progress |
|---|---|
| randomize | ✅ |
| seed | ✅ |
| value | ✅ |
| label | ✅ |
| rows | ✅ |
| geopoint capture-accuracy, warning-accuracy, allow-mock-accuracy |
✅ |
| range start, end, step | ✅ |
| image max-pixels | ✅ |
| audio quality | |
| Audit: location-priority, location-min-interval, location-max-age, track-changes, track-changes-reasons, identify-user |
|
| geotrace/shape incremental=true | |
| range labels, placeholder |
| Feature |
Progress |
|---|---|
| label | ✅ |
| hint | ✅ |
| guidance hint | |
| form translations | ✅ |
| form translations with ref to other field |
✅ |
| Markdown | ✅ |
| Inline HTML | ✅ |
| image | ✅ |
| big-image | |
| audio | ✅ |
| video | ✅ |
| autoplay |
| Feature |
Progress |
|---|---|
| grid | |
| pages | |
| logo | |
| application translations | |
| theme color | |
| preview form | ✅ |
| send instance | ✅ |
| view instance | |
| edit instance | ✅ |
| table of contents |
| Feature |
Progress |
|---|---|
| operators | ✅ |
| predicates | ✅ |
| axes | ✅ |
| string(* arg) | ✅ |
| concat(string arg*|node-set arg*) | ✅ |
| join(string separator, node-set nodes*) | ✅ |
| substr(string value, number start, number end?) |
✅ |
| substring-before(string, string) | ✅ |
| substring-after(string, string) | ✅ |
| translate(string, string, string) | ✅ |
| string-length(string arg) | ✅ |
| normalize-space(string arg?) | ✅ |
| contains(string haystack, string needle) | ✅ |
| starts-with(string haystack, string needle) |
✅ |
| ends-with(string haystack, string needle) |
✅ |
| uuid(number?) | ✅ |
| digest(string src, string algorithm, string encoding?) |
✅ |
| pulldata(string instance_id, string desired_element, string query_element, string query) |
✅ |
| if(boolean condition, _ then, _ else) | ✅ |
| coalesce(string arg1, string arg2) | ✅ |
| once(string calc) | ✅ |
| true() | ✅ |
| false() | ✅ |
| boolean(* arg) | ✅ |
| boolean-from-string(string arg) | ✅ |
| not(boolean arg) | ✅ |
| regex(string value, string expression) | ✅ |
| checklist(number min, number max, string v*) |
✅ |
| weighted-checklist(number min, number max, [string v, string w]*) |
✅ |
| number(* arg) | ✅ |
| random() | ✅ |
| int(number arg) | ✅ |
| sum(node-set arg) | ✅ |
| max(node-set arg*) | ✅ |
| min(node-set arg*) | ✅ |
| round(number arg, number decimals?) | ✅ |
| pow(number value, number power) | ✅ |
| log(number arg) | ✅ |
| log10(number arg) | ✅ |
| abs(number arg) | ✅ |
| sin(number arg) | ✅ |
| cos(number arg) | ✅ |
| tan(number arg) | ✅ |
| asin(number arg) | ✅ |
| acos(number arg) | ✅ |
| atan(number arg) | ✅ |
| atan2(number arg, number arg) | ✅ |
| sqrt(number arg) | ✅ |
| exp(number arg) | ✅ |
| exp10(number arg) | ✅ |
| pi() | ✅ |
| count(node-set arg) | ✅ |
| count-non-empty(node-set arg) | ✅ |
| position(node arg?) | ✅ |
| instance(string id) | ✅ |
| current() | ✅ |
| randomize(node-set arg, number seed) | ✅ |
| today() | ✅ |
| now() | ✅ |
| format-date(date value, string format) | ✅ |
| format-date-time(dateTime value, string format) |
✅ |
| date(* value) | ✅ |
| decimal-date-time(dateTime value) | ✅ |
| decimal-time(time value) | ✅ |
| selected(string list, string value) | ✅ |
| selected-at(string list, number index) | ✅ |
| count-selected(node node) | ✅ |
| jr:choice-name(node node, string value) | ✅ |
| jr:itext(string id) | ✅ |
| indexed-repeat(node-set arg, node-set repeat1, number index1, [node-set repeatN, number indexN]{0,2}) |
✅ |
| area(node-set ns|geoshape gs) | ✅ |
| distance(node-set ns|geoshape gs|geotrace gt|(geopoint|string) arg*) |
✅ |
| geofence(geopoint p, geoshape gs) | ✅ |
| base64-decode(base64Binary input) | ✅ |
| intersects(geoshape gs|geotrace gt) |
| Feature |
Progress |
|---|---|
| last saved instance | |
| defaults from query parameters | |
| multi-form app-like experience | |
| prevent multiple submissions | |
| configure end of form experience | |
| save as draft | |
| offline entities | |
| MBtiles / offline map layers | |
| submission encryption |
Why not evolve Enketo?
Enketo is critical infrastructure for a number of organizations and used in many different ways. As its maintainer, we found deeper changes to be challenging because they often led to regressions, many times in functionality that we don't use ourselves. We hope that the narrower scope of ODK Web Forms (in particular, no transformation step and no standalone service) will allow us to iterate quickly and align more closely with Collect while allowing organizations that have built infrastructure around Enketo to continue using it as they prefer.
Why not build a web frontend around JavaRosa?
After many years of maintaining JavaRosa and a few maintaining Enketo, we have learned a lot about how we'd like to structure an ODK XForms engine to isolate concerns and reduce the risk of regressions. We believe a fresh start will give us an opportunity to build strong patterns that will allow for a faster development pace with fewer bugs and performance issues.
There exist more and more ways to run code written with web technologies in different environments and web technologies continue to increase in popularity. We believe this choice will give us a lot of flexibility in how these packages can be used.
We aspire to use the engine to drive other kinds of frontends such as test runners and eventually mobile applications. Additionally, our experience maintaining JavaRosa and Enketo suggests that blurring the engine/frontend line can be the cause of many surprising bugs that are hard to troubleshoot.
Vue powers Central frontend where it has served us well. For Web Forms, we've selected to use a component library to help us build a consistent, accessible, and user-friendly experience in minimal time. We chose PrimeVue for its development pace, approach to extensibility, and dedication to backwards compatibility.
Why not use browsers' XPath parser and evaluator (e.g. Enketo's wrapper around them)?
We want to be able to use this code in browsers but also in backends and eventually wrapped by mobile applications. Taking control of XPath evaluation gives us more portability and also has the advantage of giving us the opportunity to make targeted performance improvements.
While XLSForm is a powerful form authoring format, it doesn't have clearly defined engine semantics or a formal specification. An XLSForm engine would have to refer to the underlying ODK XForms specification for much of its behavior and represent the form in a way that's appropriate for XPath querying.
Developing Web Forms inside Central Frontend allows us to write end-to-end tests covering the full form-filling experience in its primary distribution environment. This speeds up development and helps us catch regressions early. See the announcement on the ODK forum for more details.