Flow Screen
WhatsApp
Flow Screen
WhatsApp Flow screen UI
Paleta de componentes da tela
Embed da paleta real de componentes do WhatsApp Flow Screen.
Paleta WAFlowComponentPalette
Painel de configuracao
Esta visualizacao usa o frontend real do ConfigPanel em modo embed, carregando um no de exemplo com o JSON base desta documentacao.
Preview real do painel
JSON base do node
{
"_nodeType": "wa.flowScreen",
"screenDefinition": {},
"response": {}
}Portas
Entradas
Input
input · control
Saidas
Output
default · control
Notas de uso
Each wa.flowScreen node represents one screen in a WhatsApp Interactive Flow.
### screenDefinition structure (MANDATORY fields)
```json
{
"id": "SCREEN_ID",
"title": "Screen Title",
"terminal": false,
"data": {
"field_name": { "type": "string", "__example__": "Example value" }
},
"layout": {
"type": "SingleColumnLayout",
"children": [ ...components... ]
}
}
```
- `__example__` is REQUIRED on every field in `data`. Meta rejects the flow without it.
- `terminal: true` = last screen. Terminal screens MUST NOT have outgoing edges.
- `layout.type` is always `"SingleColumnLayout"`.
### Available layout components (children array)
**Text**
- `{ "type": "TextHeading", "text": "Title" }`
- `{ "type": "TextSubheading", "text": "Sub" }`
- `{ "type": "TextBody", "text": "Body text" }`
- `{ "type": "TextCaption", "text": "Small text" }`
**Input (must be inside a Form)**
- `{ "type": "TextInput", "name": "field_name", "label": "Label", "input-type": "text", "required": true }`
- `{ "type": "TextArea", "name": "field_name", "label": "Label", "required": false }`
- `{ "type": "DatePicker", "name": "date_field", "label": "Date", "required": true }`
- `{ "type": "RadioButtonsGroup", "name": "field", "label": "Choose", "data-source": [{"id":"v1","title":"Option 1"}] }`
- `{ "type": "Dropdown", "name": "field", "label": "Select", "data-source": "${data.list_field}", "required": true }`
- `{ "type": "CheckboxGroup", "name": "field", "label": "Select all that apply", "data-source": [{"id":"v1","title":"Yes"}] }`
- `{ "type": "OptIn", "name": "opt_in", "label": "I agree to the terms" }`
**Form container (wraps inputs + Footer)**
```json
{
"type": "Form",
"name": "form",
"children": [
{ "type": "TextInput", "name": "customer_name", "label": "Nome", "required": true },
{ "type": "Footer", "label": "Continuar", "on-click-action": {
"name": "data_exchange",
"payload": { "screen": "SCREEN_ID", "customer_name": "${form.customer_name}" }
}}
]
}
```
**⚠️ CRITICAL — WA Flow variable syntax: `${FORM_NAME.field_name}`**
The `${...}` syntax references a field from a **named Form component**.
- `FORM_NAME` = the `name` attribute of the `Form` component (e.g. `"name": "form"` → `${form.field}`).
- If you name the Form `"form_cliente"`, you MUST write `${form_cliente.field}` — NOT `${form.field}`.
- **Best practice:** name all Form components `"form"` to keep payloads simple: `${form.field_name}`.
- If you use a custom Form name for any reason, every `${}` reference in that screen's Footer payload MUST use that same name prefix.
**Footer (navigation/submit — REQUIRED on every screen)**
```json
{ "type": "Footer", "label": "Button label", "on-click-action": {
"name": "data_exchange",
"payload": { "screen": "NEXT_SCREEN_ID", "field": "${form.field}" }
}}
```
- `"name": "data_exchange"` → submits the screen to the SERVER. Use on ALL screens, including the last one, whenever downstream nodes (wa.sendText, etc.) must run after the flow.
- `"name": "navigate"` → navigate without server call (data is NOT sent to the server).
- `"name": "complete"` → closes the flow CLIENT-SIDE without any HTTP call to the server. **NEVER use `complete` if there are downstream nodes that need to execute (e.g. wa.sendText to confirm the data).** Only use `complete` when the flow truly ends with no server-side follow-up.
**Image**
`{ "type": "Image", "src": "${data.image_url}", "width": 100, "height": 100, "scale-type": "contain" }`
### Dynamic data from `data` section
Reference data fields with `${data.field_name}` in component properties.
Reference form values with `${FORM_NAME.field_name}` in Footer payload, where FORM_NAME is the `name` attribute of the Form component.
**Default pattern (recommended):** name every Form `"form"` → use `${form.field_name}` everywhere.
### ABSOLUTE RULE — every screen MUST have at least one visible component inside layout.children
A wa.flowScreen with an empty layout.children array is INVALID. Always include at minimum:
- One text component (TextHeading, TextSubheading, or TextBody) describing what the screen is for, AND
- One Form component with at least one input (TextInput, DatePicker, RadioButtonsGroup, etc.) and a Footer.
**If you are configuring a wa.flowScreen node, you MUST populate screenDefinition.layout.children. Never leave it as [] or omit it.**
### UX / modeling recommendation: prefer multiple short screens over one long screen
For flows that collect many fields, prefer a progressive multi-screen journey instead of a single overloaded screen.
- Split large forms into 2+ screens with clear steps (e.g. identification -> preferences -> confirmation).
- Keep each screen focused on a small number of fields to reduce abandonment and validation errors.
- Use `data_exchange` to carry accumulated data between screens (see accumulation rules below).
- Reserve one-screen forms for very short payloads only.
### CRITICAL: accumulate data across screens
Each `data_exchange` is an independent HTTP call. Data collected in previous screens is NOT
automatically available in subsequent screens. You MUST propagate all previously collected
fields in every `data_exchange` payload:
```json
// Screen 1 footer — Form named "form": use ${form.field}
{ "name": "data_exchange", "payload": { "screen": "TELA_2", "nome": "${form.nome}" } }
// Screen 2 footer — re-sends all previous fields + its own
{ "name": "data_exchange", "payload": { "screen": "TELA_3", "nome": "${data.nome}", "email": "${form.email}" } }
// Screen 3 footer (last) — re-sends all fields + its own
{ "name": "data_exchange", "payload": { "nome": "${data.nome}", "email": "${data.email}", "telefone": "${form.telefone}" } }
```
The server merges `${data.X}` (fields echoed from previous responses) with `${form.X}` (current form input — requires Form named `"form"`).
### CRITICAL: include ALL fields submitted in the payload — they feed the condition
When the next step is a `condition` node that evaluates a field from this screen,
that field MUST appear in the Footer's `data_exchange` payload. The engine stores
each screen's payload as `local.[nodeId].response.data.FIELD`, which the condition reads.
```json
// ✅ CORRECT — eh_cliente is included so the downstream condition can evaluate it
{ "name": "data_exchange", "payload": { "screen": "PROXIMO", "eh_cliente": "${form_cliente.eh_cliente}" } }
// ❌ WRONG — missing eh_cliente; the condition will always receive null → wrong branch
{ "name": "data_exchange", "payload": { "screen": "PROXIMO" } }
```
### Condition / branching between screens
You CAN insert `condition` or `switch` nodes between wa.flowScreen nodes to branch
the flow based on user input. The execution engine automatically traverses these
logic nodes and sends the correct NEXT_SCREEN_ID back to WhatsApp:
```
wa.flowScreen (PERGUNTA_CLIENTE)
│ default
condition ←── evaluates "local.node_screen_cliente.response.data.eh_cliente" == "sim"
true │ │ false
wa.flowScreen wa.flowScreen
(PERGUNTA_CPF) (PERGUNTA_SERVICO)
```
Rules:
- The condition variable path MUST be `local.NODE_ID.response.data.FIELD` where
NODE_ID is the ID of the wa.flowScreen node whose footer submitted that field.
- The field MUST also appear in that screen's `data_exchange` payload (see above).
- Condition nodes between flowScreens do NOT produce a WA Flow API call — they are
evaluated server-side to decide which next screen ID to return.
- Do NOT mark intermediate screens as `terminal:true` when a condition follows them.
Only the final screen(s) that lead to wa.sendText (or another non-flowScreen node)
should be `terminal:true`.
### Executable nodes BETWEEN screens (http, postgres, setVariable, ...)
You CAN place executable nodes between two screens — they run on the server
DURING the navigation (after screen A is submitted, before screen B is rendered):
```
wa.flowScreen (BUSCA_CEP) ──► httpRequest (consulta API) ──► wa.flowScreen (CONFIRMA_ENDERECO)
```
- The intermediate node's results are available to screen B via the SOURCE screen's
response data templates, rendered AFTER the intermediate nodes run:
set screen A's response data to e.g. `"endereco": "{{local.http_cep.response.logradouro}}"`.
- Use this for: fetching data to show on the next screen, saving partial progress
to a database, validating against an API.
- Terminal screens: nodes AFTER the terminal screen also run in the same
data_exchange execution (e.g. save to DB + wa.sendText confirmation).
### Server-side validation with per-field error messages (CPF, CEP, availability...)
Route the FALSE branch of a condition BACK to the same screen node to re-render it
with an error message under the field:
```
wa.flowScreen (CADASTRO) ──► condition (cpf válido?) ──true──► wa.flowScreen (PROXIMA)
▲ │false
└────────────────────────────────────────────┘
```
1. Declare the error field in the screen's `data`: `"erro_cpf": { "type": "string", "__example__": "" }`.
2. Bind it on the input component: `"error-message": "${data.erro_cpf}"` (TextInput supports it).
3. Set the screen's response data with a conditional template:
`"erro_cpf": "{{#if local.valida_cpf.response.valido}}{{else}}CPF inválido — confira os dígitos{{/if}}"`.
4. Self-routes are allowed by the compiler (data_exchange refresh) and the response is
filtered to the screen's declared `data` keys — error fields never leak to other screens.
### Accessing flow data in downstream nodes (wa.sendText, etc.)
After the last `data_exchange`, all accumulated payload fields are available via `{{input.data.FIELD}}`:
- `{{input.data.nome}}` — value of "nome" from any screen's payload
- `{{input.data.email}}` — value of "email"
- `{{input.data.cidade}}` — value of "cidade"
**DO NOT use `{{local.SCREEN_ID.response.data.FIELD}}` in wa.sendText or post-flow nodes —
use `{{input.data.FIELD}}` instead. `local.X.response.data.FIELD` only works inside
the condition node that immediately evaluates the submitted screen.**
### response object (tells the executor what to send back to WhatsApp)
```json
{
"screenId": "THIS_SCREEN_ID",
"data": {
"field_name": "{{local.someNode.result}}"
}
}
```
Use Handlebars `{{local.NODE_ID.key}}` / `{{global.key}}` / `{{input.key}}` for dynamic data.Objetos filhos e estruturas aninhadas
WAFlowScreenResponse
Screen response returned to WhatsApp
| Campo | Tipo | Obrigatorio | Descricao |
|---|---|---|---|
| nextScreenId | string | Nao | Next screen ID to navigate to |
| action | string | Nao | Response action type |
| data | object | Nao | Key-value data sent back to WhatsApp |
Exemplos de configuracao
Tela intermediária com Form + TextInput + Footer (data_exchange)
{
"screenDefinition": {
"id": "TELA_DADOS",
"title": "Seus dados",
"terminal": false,
"data": {
"nome": { "type": "string", "__example__": "João Silva" },
"email": { "type": "string", "__example__": "joao@email.com" }
},
"layout": {
"type": "SingleColumnLayout",
"children": [
{ "type": "TextHeading", "text": "Preencha seus dados" },
{
"type": "Form",
"name": "form",
"children": [
{ "type": "TextInput", "name": "nome", "label": "Nome completo", "input-type": "text", "required": true },
{ "type": "TextInput", "name": "email", "label": "E-mail", "input-type": "email", "required": true },
{ "type": "Footer", "label": "Continuar", "on-click-action": {
"name": "data_exchange",
"payload": { "screen": "TELA_CONFIRMACAO", "nome": "${form.nome}", "email": "${form.email}" }
}}
]
}
]
}
}
}Tela terminal (última tela) com TextBody + Footer de conclusão
{
"screenDefinition": {
"id": "TELA_CONFIRMACAO",
"title": "Confirmação",
"terminal": true,
"data": {},
"layout": {
"type": "SingleColumnLayout",
"children": [
{ "type": "TextBody", "text": "Obrigado! Seus dados foram recebidos com sucesso." },
{
"type": "Form",
"name": "form",
"children": [
{ "type": "Footer", "label": "Concluir", "on-click-action": {
"name": "data_exchange",
"payload": {}
}}
]
}
]
}
}
}