guía completa · ES
Construyendo un cuestionario
desde cero, celda por celda.
XLSForm es el estándar que convierte una planilla común en un formulario digital completo —
con lógica de salto, validación, GPS, fotos y varios idiomas — listo para correr en
ODK Collect, KoboToolbox o Enketo. Esta guía construye una encuesta de hogares
paso a paso y documenta todos los tipos de pregunta y configuraciones,
con ejemplos que puede copiar y convertir con rxform.
1 · Anatomía de un XLSForm
Un XLSForm es un archivo .xlsx (o .xls/.ods) con hasta tres hojas principales:
survey— las preguntas, en su orden de aparición. Columnas mínimas:type,nameylabel.choices— las listas de opciones de las preguntas de selección. Columnas mínimas:list_name,nameylabel.settings— título, identificador, versión, idioma por defecto y demás configuración del formulario (una sola fila de datos).
Reglas de oro para la columna name (preguntas, grupos y opciones):
Los nombres empiezan con letra o _ y contienen solo letras, números, -, _ y . — sin espacios ni acentos. Se convierten en los nombres de columna de su base de datos: prefiera ingreso_mensual a Ingreso Mensual ($). El label, en cambio, es texto libre: acentos, emoji, lo que haga falta.
2 · El primer formulario
El formulario útil más pequeño tiene tres filas en la hoja survey:
| type | name | label | |
|---|---|---|---|
| 2 | text | encuestador | Nombre de quien aplica la encuesta |
| 3 | integer | tamano_hogar | ¿Cuántas personas viven en el hogar? |
| 4 | select_one sim_nao | tiene_agua | ¿El hogar tiene agua potable? |
La pregunta de la fila 4 usa la lista si_no, definida en la hoja choices:
| list_name | name | label | |
|---|---|---|---|
| 2 | si_no | si | Sí |
| 3 | si_no | no | No |
Y la hoja settings da identidad al formulario:
| form_title | form_id | version | |
|---|---|---|---|
| 2 | Encuesta de Hogares 2026 | enc_hogares_2026 | 2026081101 |
Convierta y listo:
$ rxform encuesta_hogares.xlsx
encuesta_hogares.xml
3 · Todos los tipos de pregunta
Texto y números
| type | name | label | hint | |
|---|---|---|---|---|
| 2 | text | observaciones | Observaciones generales | Texto libre |
| 3 | integer | edad | Edad (años cumplidos) | Solo números enteros |
| 4 | decimal | ingreso | Ingreso mensual | Use punto para centavos |
| 5 | range | satisfaccion | Satisfacción con el transporte | 0 = pésimo · 10 = excelente |
text— texto libre. Para cajas más altas,parameters=rows=5.integer/decimal— números, con teclado numérico en el dispositivo.range— control deslizante; límites víaparameters=start=0 end=10 step=1. Conappearance=ratingse vuelve estrellas.
Fechas y horas
| type | name | label | appearance | |
|---|---|---|---|---|
| 2 | date | fecha_visita | Fecha de la visita | |
| 3 | date | mes_mudanza | ¿Cuándo se mudó? | month-year |
| 4 | time | hora_inicio | Hora de inicio | |
| 5 | dateTime | cita | Agendar retorno para |
Apariencias útiles para date: month-year, year y no-calendar.
Opciones y listas
| type | name | label | appearance | |
|---|---|---|---|---|
| 2 | select_one escolaridad | escolaridad | Escolaridad del jefe de hogar | minimal |
| 3 | select_multiple servicios | servicios | ¿Qué servicios llegan a la calle? | |
| 4 | rank prioridades | prioridades | Ordene las prioridades del barrio |
select_one LISTA— una respuesta.appearance:minimal(menú desplegable),likert,quick(avanza solo),columns…select_multiple LISTA— varias respuestas; se guardan separadas por espacios, por eso losnamede las opciones no pueden contener espacios.rank LISTA— la persona ordena las opciones.select_one LISTA or_other— agrega la opción “Other” y una pregunta automática “Specify other.”.
Se permiten columnas extra en la hoja choices, que se vuelven datos de la opción — la base de los filtros en cascada:
Selecciones en cascada (choice_filter)
| list_name | name | label | uf | |
|---|---|---|---|---|
| 2 | departamentos | lm | Lima | |
| 3 | departamentos | cs | Cusco | |
| 4 | distritos | miraflores | Miraflores | lm |
| 5 | distritos | barranco | Barranco | lm |
| 6 | distritos | pisac | Písac | cs |
| type | name | label | choice_filter | |
|---|---|---|---|---|
| 2 | select_one departamentos | depto | Departamento | |
| 3 | select_one distritos | distrito | Distrito | depto = ${depto} |
En el filtro, las columnas de la lista aparecen por su nombre (depto) y las respuestas previas como ${depto}. Para barajar opciones: parameters = randomize=true (con seed=42 para un orden reproducible).
Ubicación
| type | name | label | parameters | |
|---|---|---|---|---|
| 2 | geopoint | punto | Ubicación del hogar | capture-accuracy=5 warning-accuracy=10 |
| 3 | geotrace | trayecto | Trayecto hasta la parada de bus | |
| 4 | geoshape | terreno | Contorno del terreno |
geopoint captura un punto (precisión objetivo en metros vía parameters); geotrace, una línea; geoshape, un polígono cerrado.
Medios y archivos
| type | name | label | appearance | parameters | |
|---|---|---|---|---|---|
| 2 | image | foto_fachada | Foto de la fachada | max-pixels=1024 | |
| 3 | image | firma | Firma del encuestado | signature | |
| 4 | audio | relato | Grabe el relato | quality=voice-only | |
| 5 | video | video_calle | Video de la calle | ||
| 6 | file | comprobante | Adjunte el comprobante (PDF) | ||
| 7 | barcode | codigo | Código de barras del medidor |
image con appearance = signature se vuelve campo de firma; draw, dibujo libre. max-pixels reduce el tamaño de las fotos en el dispositivo.
Notas y confirmaciones
| type | name | label | |
|---|---|---|---|
| 2 | note | apertura | ¡Buenos días! Esta encuesta toma ~15 minutos. Las respuestas son confidenciales. |
| 3 | acknowledge | consentimiento | La persona acepta participar |
note— texto mostrado, sin respuesta. Acepta${referencias}: “¡Gracias, ${encuestador}!”. Es el único tipo que puede omitirname(rxform genera uno).acknowledge— exige un “ok” explícito de quien responde.
Campos invisibles
| type | name | calculation | |
|---|---|---|---|
| 2 | calculate | ingreso_per_capita | ${ingreso} div ${tamano_hogar} |
| 3 | hidden | version_muestra |
calculate computa un valor con XPath (funciones como if(), concat(), selected(), count(), round()…); hidden guarda un valor rellenable por default o integraciones.
Metadatos — recogidos solos
| type | name | label / trigger | |
|---|---|---|---|
| 2 | start | inicio | |
| 3 | end | fin | |
| 4 | today | hoy | |
| 5 | deviceid | dispositivo | |
| 6 | username | usuario | |
| 7 | audit | audit | |
| 8 | start-geopoint | ubicacion_inicio | |
| 9 | background-audio | grabacion | |
| 10 | background-geopoint | ubicacion_respuesta | trigger: ${tiene_agua} |
start/end— marcas de tiempo de apertura/cierre;today— la fecha;deviceid,username,phonenumber,email— identificación del dispositivo/cuenta.audit— bitácora de auditoría del llenado; conparameters=location-priority=balanced location-min-interval=60 location-max-age=120registra además la ubicación durante la entrevista.start-geopoint— captura silenciosa de la ubicación al abrir;background-audiograba el audio de la entrevista;background-geopointcaptura la ubicación cuando se responde la pregunta deltrigger.
4 · Lógica del formulario
Toda la lógica usa ${nombre} para referenciar respuestas anteriores.
relevant — saltar preguntas
| type | name | label | relevant | |
|---|---|---|---|---|
| 2 | select_one sim_nao | trabaja | ¿Usted trabaja? | |
| 3 | text | ocupacion | ¿Cuál es su ocupación? | ${trabaja} = 'si' |
| 4 | note | nota_edad | Módulo aplicable a mayores de edad. | ${edad} >= 18 |
La pregunta solo aparece cuando la expresión es verdadera. Combine condiciones con and/or; para selección múltiple use selected(${servicios}, 'agua').
constraint — validar respuestas
| type | name | label | constraint | constraint_message | |
|---|---|---|---|---|---|
| 2 | integer | edad | Edad | . >= 0 and . <= 120 | La edad debe estar entre 0 y 120. |
| 3 | date | nacimiento | Fecha de nacimiento | . <= today() | La fecha no puede ser futura. |
El punto . es la propia respuesta. El mensaje aparece cuando la regla falla.
required, default, read_only
| type | name | label | required | required_message | default | |
|---|---|---|---|---|---|---|
| 2 | select_one si_no | tiene_agua | ¿Agua potable? | yes | Esta respuesta es obligatoria. | |
| 3 | date | fecha_visita | Fecha de la visita | today() | ||
| 4 | integer | sector | Sector censal | 42 |
required=yes(o una expresión) impide avanzar sin respuesta.defaultacepta un valor fijo (42) o una expresión dinámica (today(),${depto}) evaluada al abrir el formulario — incluso${last-saved#sector}para heredar el último envío.read_only=yesmuestra sin permitir edición.
trigger — recalcular cuando algo cambia
| type | name | label | trigger | calculation | |
|---|---|---|---|---|---|
| 2 | integer | tamano_hogar | ¿Cuántos residentes? | ||
| 3 | integer | ninos | ¿Cuántos niños? | ${tamano_hogar} |
Con trigger, el campo se (re)define cada vez que cambia la pregunta referenciada — aquí, limpiando ninos cuando se edita tamano_hogar. Si hay calculation, corre en ese momento (en vez de continuamente).
5 · Grupos, repeticiones y loops
Grupos
| type | name | label | appearance | relevant | |
|---|---|---|---|---|---|
| 2 | begin_group | modulo_agua | Módulo: Saneamiento | field-list | ${tiene_agua} = 'si' |
| 3 | select_one fuente_agua | fuente | Fuente del agua | ||
| 4 | integer | dias_sin | Días sin agua en el mes | ||
| 5 | end_group |
appearance=field-listmuestra todo el grupo en una sola pantalla.table-list— para una secuencia deselect_onecon la misma lista, muestra una matriz (filas × columnas).- El
relevantdel grupo aplica a todo su contenido.
Repeticiones
| type | name | label | repeat_count | |
|---|---|---|---|---|
| 2 | begin_repeat | residente | Datos del residente | ${tamano_hogar} |
| 3 | text | nombre | Nombre | |
| 4 | integer | edad_residente | Edad | |
| 5 | end_repeat |
El bloque se repite una vez por residente. Sin repeat_count, el encuestador agrega repeticiones a mano; con una expresión (o un número), la cantidad es automática. Dentro de la repetición, use position(..) para el índice actual e indexed-repeat() para leer valores de otra repetición.
Loops sobre una lista
| type | name | label | |
|---|---|---|---|
| 2 | begin loop over servicios | eval_servicios | |
| 3 | select_one notas | nota | ¿Cómo evalúa el servicio de %(label)s? |
| 4 | end loop |
Genera un bloque por opción de la lista servicios, sustituyendo %(label)s y %(name)s — una pregunta “¿Cómo evalúa el servicio de Agua?”, otra “…de Alcantarillado?”, etcétera.
6 · Varios idiomas
| type | name | label::Español (es) | label::English (en) | hint::Español (es) | |
|---|---|---|---|---|---|
| 2 | integer | moradores | ¿Cuántas personas viven aquí? | How many people live here? | Cuente a todos los residentes |
Basta sufijar las columnas traducibles con ::Idioma (código): funciona para label, hint, guidance_hint, constraint_message, required_message, image, audio y video — en las hojas survey y choices. Defina el idioma inicial en settings → default_language = Español (es). La aplicación gana un menú de idiomas.
Medios por pregunta: las columnas image/audio/video asocian archivos (p. ej. tarjeta_respuesta.jpg) enviados junto con el formulario.
7 · Apariencias (appearance)
| appearance | aplica a | efecto |
|---|---|---|
| minimal | select_one/multiple | menú desplegable compacto |
| quick | select_one | avanza al seleccionar |
| likert | select_one | escala horizontal estilo Likert |
| columns / columns-n | selects | opciones en columnas |
| autocomplete | select_one | busca mientras escribe |
| field-list | grupos | grupo entero en una pantalla |
| table-list | grupos | matriz de selects con la misma lista |
| multiline | text | caja de varias líneas |
| numbers / thousands-sep | text | teclado numérico · separador de miles |
| month-year · year · no-calendar | date | precisión reducida · sin calendario |
| signature · draw · annotate | image | firma · dibujo · anotar sobre foto |
| map · quick map | select_one_from_file (geojson) | elegir en el mapa |
| rating | range | estrellas |
| label · list-nolabel | selects | piezas para armar matrices manuales |
8 · Parámetros (parameters)
La columna parameters recibe pares clave=valor separados por espacios:
| tipo | parámetros |
|---|---|
| range | start=0 end=10 step=1 |
| text | rows=5 |
| image | max-pixels=1024 |
| audio · background-audio | quality=voice-only | low | normal |
| geopoint | capture-accuracy=5 warning-accuracy=10 |
| selects | randomize=true seed=42 |
| select_*_from_file | value=coluna label=coluna |
| audit | location-priority=balanced location-min-interval=60 location-max-age=120 |
9 · La hoja settings
| columna | para qué |
|---|---|
| form_title | título mostrado en la aplicación |
| form_id | identificador único del formulario en el servidor |
| version | versión (use fecha: 2026081101); el servidor gestiona actualizaciones con ella |
| instance_name | nombre de cada envío en los listados — ej. concat(${distrito}, '-', ${fecha_visita}) |
| default_language | idioma inicial, ej. Español (es) |
| style | pages (una pantalla por grupo) · theme-grid |
| public_key | clave RSA para cifrar envíos de extremo a extremo |
| submission_url · auto_send · auto_delete | destino y política de envío |
| allow_choice_duplicates | permite name repetido en una lista |
| clean_text_values | no preserva espacios múltiples en las celdas (por defecto se colapsan) |
| name · namespaces · attribute::x · prefix · delimiter · flat · omit_instanceID | ajustes avanzados del XML generado |
10 · Datos externos y entities
Listas grandes en archivo
| type | name | label | choice_filter | |
|---|---|---|---|---|
| 2 | select_one_from_file distritos.csv | distrito | Distrito | depto = ${depto} |
| 3 | select_one_from_file sectores.geojson | sector | Sector (en el mapa) |
El CSV necesita columnas name y label (o indique otras con parameters = value=codigo label=descripcion); GeoJSON usa id/title y habilita appearance = map. El archivo se sube al servidor junto con el formulario.
Consultas con pulldata()
| type | name | calculation | |
|---|---|---|---|
| 2 | calculate | meta_sector | pulldata('metas', 'meta', 'sector', ${sector}) |
Busca en metas.csv la columna meta de la fila donde sector = la respuesta. También existen ${last-saved#campo} (valor del último envío guardado) y la hoja entities, que permite al formulario crear y actualizar registros compartidos entre formularios (p. ej. registrar hogares en una visita y reencontrarlos en la siguiente) — con save_to en las preguntas y create_if/update_if/label en la hoja.
11 · Errores comunes — y cómo avisa rxform
- Tipo mal escrito →
[sheet 'survey', row 3, column 'type'] unknown question type 'integr' — did you mean 'integer'? - Referencia rota →
'${edda}' does not match… — did you mean 'edad'? - Lista inexistente → señala la fila y sugiere la lista más parecida.
- Grupo sin cerrar → señala la fila del
begin_groupy pide elend_group. - Nombres duplicados entre hermanos, nombre con espacio/acento, pregunta visible sin label, opciones repetidas en la lista — todo con hoja, fila, columna y causa probable.
12 · Convertir y publicar
# convertir
$ rxform encuesta_hogares.xlsx
encuesta_hogares.xml
# revisar errores de autoría es solo ejecutarlo — el mensaje señala la celda
$ rxform encuesta_hogares.xlsx --stdout > /dev/null
- KoboToolbox: suba el propio
.xlsx(Kobo convierte en el servidor con pyxform — rxform genera exactamente el mismo XML, así que sirve como validación local instantánea) o despliegue el XML vía API. - ODK Central: publique el
.xlsxo el.xmlgenerado; adjunte los CSV/GeoJSON de listas externas. - Enketo: los formularios publicados reciben enlace web automáticamente.
Flujo recomendado: mantenga el .xlsx bajo control de versiones, corra rxform en CI para validar cada cambio (la conversión falla con un mensaje preciso si algo se rompe) y publique desde la versión validada.
guía · rxform — volver al inicio · referencia oficial xlsform.org