guia completo · pt-BR
Construindo um questionário
do zero, célula por célula.
XLSForm é o padrão que transforma uma planilha comum em um formulário digital completo —
com lógica de pulo, validação, GPS, fotos e vários idiomas — pronto para rodar em
ODK Collect, KoboToolbox ou Enketo. Este guia constrói uma pesquisa domiciliar
passo a passo e documenta todos os tipos de pergunta e configurações,
com exemplos que você pode copiar e converter com rxform.
1 · Anatomia de um XLSForm
Um XLSForm é um arquivo .xlsx (ou .xls/.ods) com até três abas principais:
survey— as perguntas, na ordem em que aparecem. Colunas mínimas:type,nameelabel.choices— as listas de opções das perguntas de múltipla escolha. Colunas mínimas:list_name,nameelabel.settings— título, identificador, versão, idioma padrão e outras configurações do formulário (uma única linha de dados).
Regras de ouro para a coluna name (vale para perguntas, grupos e opções):
Nomes começam com letra ou _, contêm apenas letras, números, -, _ e . — sem espaços nem acentos. Eles viram os nomes das colunas no seu banco de dados, então prefira renda_mensal a Renda Mensal (R$). O label, por outro lado, é texto livre: acentos, emoji, o que precisar.
2 · O primeiro formulário
O menor formulário útil tem três linhas na aba survey:
| type | name | label | |
|---|---|---|---|
| 2 | text | entrevistador | Nome de quem aplica a pesquisa |
| 3 | integer | moradores | Quantas pessoas moram no domicílio? |
| 4 | select_one sim_nao | tem_agua | O domicílio tem água encanada? |
A pergunta da linha 4 usa a lista sim_nao, definida na aba choices:
| list_name | name | label | |
|---|---|---|---|
| 2 | sim_nao | sim | Sim |
| 3 | sim_nao | nao | Não |
E a aba settings dá identidade ao formulário:
| form_title | form_id | version | |
|---|---|---|---|
| 2 | Pesquisa Domiciliar 2026 | pesq_dom_2026 | 2026081101 |
Converta e pronto:
$ rxform pesquisa_domiciliar.xlsx
pesquisa_domiciliar.xml
3 · Todos os tipos de pergunta
Texto e números
| type | name | label | hint | |
|---|---|---|---|---|
| 2 | text | observacoes | Observações gerais | Campo livre |
| 3 | integer | idade | Idade (anos completos) | Somente números inteiros |
| 4 | decimal | renda | Renda mensal (R$) | Use ponto para centavos |
| 5 | range | satisfacao | Satisfação com o transporte | 0 = péssimo · 10 = ótimo |
text— texto livre. Para caixas maiores, useparameters=rows=5.integer/decimal— números com teclado numérico no aparelho.range— controle deslizante; limites viaparameters=start=0 end=10 step=1. Comappearance=ratingvira estrelas.
Datas e horas
| type | name | label | appearance | |
|---|---|---|---|---|
| 2 | date | data_visita | Data da visita | |
| 3 | date | mes_mudanca | Quando se mudou? | month-year |
| 4 | time | hora_inicio | Hora de início | |
| 5 | dateTime | agendamento | Agendar retorno para |
Aparências úteis para date: month-year, year e no-calendar.
Escolhas e listas
| type | name | label | appearance | |
|---|---|---|---|---|
| 2 | select_one escolaridade | escolaridade | Escolaridade do responsável | minimal |
| 3 | select_multiple servicos | servicos | Quais serviços chegam à rua? | |
| 4 | rank prioridades | prioridades | Ordene as prioridades do bairro |
select_one LISTA— uma resposta.appearance:minimal(menu suspenso),likert,quick(avança sozinho),columns…select_multiple LISTA— várias respostas; gravadas separadas por espaço, por isso osnamedas opções não podem conter espaço.rank LISTA— o respondente ordena as opções.select_one LISTA or_other— acrescenta a opção “Other” e uma pergunta automática “Specify other.”.
Colunas extras na aba choices são permitidas e viram dados da opção — a base dos filtros em cascata:
Seleções em cascata (choice_filter)
| list_name | name | label | uf | |
|---|---|---|---|---|
| 2 | ufs | pe | Pernambuco | |
| 3 | ufs | rs | Rio Grande do Sul | |
| 4 | municipios | recife | Recife | pe |
| 5 | municipios | olinda | Olinda | pe |
| 6 | municipios | poa | Porto Alegre | rs |
| type | name | label | choice_filter | |
|---|---|---|---|---|
| 2 | select_one ufs | uf | Estado | |
| 3 | select_one municipios | municipio | Município | uf = ${uf} |
No filtro, colunas da lista aparecem pelo nome (uf) e respostas anteriores como ${uf}. Para embaralhar opções: parameters = randomize=true (com seed=42 para ordem reproduzível).
Localização
| type | name | label | parameters | |
|---|---|---|---|---|
| 2 | geopoint | ponto | Localização do domicílio | capture-accuracy=5 warning-accuracy=10 |
| 3 | geotrace | trajeto | Trajeto até o ponto de ônibus | |
| 4 | geoshape | terreno | Contorno do terreno |
geopoint captura um ponto (com precisão-alvo em metros via parameters); geotrace, uma linha; geoshape, um polígono fechado.
Mídia e arquivos
| type | name | label | appearance | parameters | |
|---|---|---|---|---|---|
| 2 | image | foto_fachada | Foto da fachada | max-pixels=1024 | |
| 3 | image | assinatura | Assinatura do respondente | signature | |
| 4 | audio | relato | Grave o relato | quality=voice-only | |
| 5 | video | video_via | Vídeo da via | ||
| 6 | file | comprovante | Anexe o comprovante (PDF) | ||
| 7 | barcode | codigo | Código de barras do hidrômetro |
image com appearance = signature vira campo de assinatura; draw, desenho livre. max-pixels reduz o tamanho das fotos no aparelho.
Notas e confirmações
| type | name | label | |
|---|---|---|---|
| 2 | note | abertura | Bom dia! Esta pesquisa leva ~15 minutos. As respostas são confidenciais. |
| 3 | acknowledge | consentimento | O respondente concorda em participar |
note— texto exibido, sem resposta. Aceita${referências}: “Obrigado, ${entrevistador}!”. É o único tipo que dispensaname(o rxform gera um automaticamente).acknowledge— exige um “ok” explícito de quem responde.
Campos invisíveis
| type | name | calculation | |
|---|---|---|---|
| 2 | calculate | renda_per_capita | ${renda} div ${moradores} |
| 3 | hidden | versao_amostra |
calculate computa um valor com XPath (funções como if(), concat(), selected(), count(), round()…); hidden guarda um valor preenchível por default ou por integrações.
Metadados — coletados sozinhos
| type | name | label / trigger | |
|---|---|---|---|
| 2 | start | inicio | |
| 3 | end | fim | |
| 4 | today | hoje | |
| 5 | deviceid | aparelho | |
| 6 | username | usuario | |
| 7 | audit | audit | |
| 8 | start-geopoint | local_inicio | |
| 9 | background-audio | gravacao | |
| 10 | background-geopoint | local_resposta | trigger: ${tem_agua} |
start/end— carimbo de início/fim do preenchimento;today— a data;deviceid,username,phonenumber,email— identificação do aparelho/conta.audit— trilha de auditoria do preenchimento; comparameters=location-priority=balanced location-min-interval=60 location-max-age=120registra também a localização ao longo da entrevista.start-geopoint— captura silenciosa da localização ao abrir;background-audiograva o áudio da entrevista;background-geopointcaptura a localização quando a pergunta dotriggerfor respondida.
4 · Lógica do formulário
Toda a lógica usa ${nome} para referenciar respostas anteriores.
relevant — pular perguntas
| type | name | label | relevant | |
|---|---|---|---|---|
| 2 | select_one sim_nao | trabalha | Você trabalha? | |
| 3 | text | ocupacao | Qual a sua ocupação? | ${trabalha} = 'sim' |
| 4 | note | obs_idade | Módulo aplicável a maiores de idade. | ${idade} >= 18 |
A pergunta só aparece quando a expressão é verdadeira. Combine condições com and/or; para múltipla escolha use selected(${servicos}, 'agua').
constraint — validar respostas
| type | name | label | constraint | constraint_message | |
|---|---|---|---|---|---|
| 2 | integer | idade | Idade | . >= 0 and . <= 120 | Idade deve estar entre 0 e 120. |
| 3 | date | nascimento | Data de nascimento | . <= today() | A data não pode ser futura. |
O ponto . é a própria resposta. A mensagem aparece quando a regra falha.
required, default, read_only
| type | name | label | required | required_message | default | |
|---|---|---|---|---|---|---|
| 2 | select_one sim_nao | tem_agua | Tem água encanada? | yes | Esta resposta é obrigatória. | |
| 3 | date | data_visita | Data da visita | today() | ||
| 4 | integer | setor | Setor censitário | 42 |
required=yes(ou uma expressão) impede avançar sem resposta.defaultaceita valor fixo (42) ou expressão dinâmica (today(),${uf}) avaliada ao abrir o formulário — inclusive${last-saved#setor}para herdar o valor do último envio.read_only=yesmostra sem permitir edição.
trigger — recalcular quando algo muda
| type | name | label | trigger | calculation | |
|---|---|---|---|---|---|
| 2 | integer | moradores | Quantos moradores? | ||
| 3 | integer | criancas | Quantas crianças? | ${moradores} |
Com trigger, o campo é (re)definido sempre que a pergunta referenciada mudar — aqui, limpando criancas quando moradores é alterado. Se houver calculation, ela roda nesse momento (em vez de continuamente).
5 · Grupos, repetições e loops
Grupos
| type | name | label | appearance | relevant | |
|---|---|---|---|---|---|
| 2 | begin_group | modulo_agua | Módulo: Saneamento | field-list | ${tem_agua} = 'sim' |
| 3 | select_one origem_agua | origem | Origem da água | ||
| 4 | integer | dias_falta | Dias sem água no mês | ||
| 5 | end_group |
appearance=field-listmostra todas as perguntas do grupo numa tela só.table-list— para uma sequência deselect_onecom a mesma lista, exibe formato de matriz (linhas × colunas).- O
relevantdo grupo vale para tudo dentro dele.
Repetições
| type | name | label | repeat_count | |
|---|---|---|---|---|
| 2 | begin_repeat | morador | Dados do morador | ${moradores} |
| 3 | text | nome | Nome | |
| 4 | integer | idade_morador | Idade | |
| 5 | end_repeat |
O bloco se repete uma vez por morador. Sem repeat_count, o entrevistador adiciona repetições manualmente; com uma expressão (ou número fixo), a quantidade é automática. Dentro da repetição, use position(..) para saber o índice atual e indexed-repeat() para ler valores de outra repetição.
Loops sobre uma lista
| type | name | label | |
|---|---|---|---|
| 2 | begin loop over servicos | aval_servicos | |
| 3 | select_one notas | nota | Como avalia o serviço de %(label)s? |
| 4 | end loop |
Gera um bloco por opção da lista servicos, substituindo %(label)s e %(name)s pelo rótulo/nome de cada opção — uma pergunta “Como avalia o serviço de Água?”, outra “…de Esgoto?”, e assim por diante.
6 · Múltiplos idiomas
| type | name | label::Português (pt) | label::English (en) | hint::Português (pt) | |
|---|---|---|---|---|---|
| 2 | integer | moradores | Quantas pessoas moram aqui? | How many people live here? | Conte todos os residentes |
Basta sufixar as colunas traduzíveis com ::Idioma (código): funciona para label, hint, guidance_hint, constraint_message, required_message, image, audio e video — nas abas survey e choices. Defina o idioma inicial em settings → default_language = Português (pt). O aplicativo ganha um menu para alternar idiomas.
Mídia por pergunta: as colunas image/audio/video associam arquivos (ex.: cartao_resposta.jpg) enviados junto com o formulário.
7 · Aparências (appearance)
| appearance | vale para | efeito |
|---|---|---|
| minimal | select_one/multiple | menu suspenso compacto |
| quick | select_one | avança ao selecionar |
| likert | select_one | escala horizontal estilo Likert |
| columns / columns-n | selects | opções em colunas |
| autocomplete | select_one | busca conforme digita |
| field-list | grupos | grupo inteiro numa tela |
| table-list | grupos | matriz de selects com a mesma lista |
| multiline | text | caixa de várias linhas |
| numbers / thousands-sep | text | teclado numérico · separador de milhar |
| month-year · year · no-calendar | date | precisão reduzida · sem calendário |
| signature · draw · annotate | image | assinatura · desenho · anotar sobre foto |
| map · quick map | select_one_from_file (geojson) | escolha no mapa |
| rating | range | estrelas |
| label · list-nolabel | selects | peças para montar matrizes manuais |
8 · Parâmetros (parameters)
A coluna parameters recebe pares chave=valor separados por espaço:
| 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 · A aba settings
| coluna | para quê |
|---|---|
| form_title | título exibido no aplicativo |
| form_id | identificador único do formulário no servidor |
| version | versão (use data: 2026081101); o servidor gerencia atualizações por ela |
| instance_name | nome de cada envio na listagem — ex.: concat(${municipio}, '-', ${data_visita}) |
| default_language | idioma inicial, ex.: Português (pt) |
| style | pages (uma tela por grupo) · theme-grid |
| public_key | chave RSA para criptografar envios de ponta a ponta |
| submission_url · auto_send · auto_delete | destino e política de envio |
| allow_choice_duplicates | permite name repetido numa lista |
| clean_text_values | no preserva espaços múltiplos nas células (padrão colapsa) |
| name · namespaces · attribute::x · prefix · delimiter · flat · omit_instanceID | ajustes avançados do XML gerado |
10 · Dados externos e entities
Listas grandes em arquivo
| type | name | label | choice_filter | |
|---|---|---|---|---|
| 2 | select_one_from_file municipios.csv | municipio | Município | uf = ${uf} |
| 3 | select_one_from_file setores.geojson | setor | Setor (no mapa) |
O CSV precisa de colunas name e label (ou indique outras com parameters = value=codigo label=descricao); GeoJSON usa id/title e habilita appearance = map. O arquivo é enviado ao servidor junto com o formulário.
Consultas com pulldata()
| type | name | calculation | |
|---|---|---|---|
| 2 | calculate | meta_setor | pulldata('metas', 'meta', 'setor', ${setor}) |
Busca em metas.csv a coluna meta da linha em que setor = resposta. Há ainda ${last-saved#campo} (valor do último envio salvo) e a aba entities, que permite ao formulário criar e atualizar cadastros compartilhados entre formulários (ex.: cadastrar domicílios numa visita e reencontrá-los na seguinte) — com save_to nas perguntas e create_if/update_if/label na aba.
11 · Erros comuns — e como o rxform avisa
- Tipo digitado errado →
[sheet 'survey', row 3, column 'type'] unknown question type 'integr' — did you mean 'integer'? - Referência quebrada →
'${idadde}' does not match… — did you mean 'idade'? - Lista inexistente → aponta a linha e sugere a lista mais parecida.
- Grupo sem fechar → aponta a linha do
begin_groupe pede oend_group. - Nomes duplicados entre irmãos, nome com espaço/acento, pergunta visível sem label, opções repetidas na lista — todos com aba, linha, coluna e causa provável.
12 · Converter e publicar
# converter
$ rxform pesquisa_domiciliar.xlsx
pesquisa_domiciliar.xml
# conferir erros de autoria é só rodar — a mensagem aponta a célula
$ rxform pesquisa_domiciliar.xlsx --stdout > /dev/null
- KoboToolbox: você pode subir o próprio
.xlsx(o Kobo converte no servidor com pyxform — o rxform gera exatamente o mesmo XML, então serve como validação local instantânea) ou implantar o XML via API. - ODK Central: publique o
.xlsxou o.xmlgerado; anexe os CSVs/GeoJSON de listas externas. - Enketo: os formulários publicados ganham link web automaticamente.
Fluxo recomendado: mantenha o .xlsx no controle de versão, rode rxform no CI para validar cada mudança (a conversão falha com mensagem precisa se algo quebrar) e publique a partir da versão validada.
guia · rxform — voltar ao início · referência oficial xlsform.org