Catálogo de Variáveis e Formulários
Variáveis de processo são livres por padrão: qualquer etapa grava qualquer
nome, com qualquer valor. Isso é suficiente enquanto quem lê e quem escreve é o seu próprio código. Quando os
dados vêm de fora — um formulário de solicitação, um analista concluindo uma tarefa, um handler que chama um
serviço cujo retorno você não controla —, você quer duas garantias: que o dado chegou, e que chegou no
formato certo. O catálogo de variáveis dá as duas, declaradas no próprio .kikwi.
Um bom formulário em papel já diz tudo o que precisa: o nome de cada campo, o que é obrigatório (o asterisco), o formato esperado ("DD/MM/AAAA", "somente números") e, nas perguntas fechadas, as opções para marcar. Quem recebe o formulário confere antes de aceitar — e devolve apontando todos os campos com problema, não um de cada vez. O catálogo é esse formulário, e o motor é quem confere no balcão. A analogia para de funcionar na hora de preencher: o papel é o mesmo para todo mundo, mas o motor não desenha tela nenhuma — ele só descreve os campos, e quem desenha é o seu frontend.
Duas peças: o catálogo e o contrato de cada etapa
variableDeclarations(no nível do processo) declara cada dado uma vez: akey(o nome da variável de processo), umlabellegível, oformate, opcionalmente, opções, moeda e regex.variableBindings(em cada nó) diz quais variáveis do catálogo aquela etapa exige:inputs— o que precisa chegar. No início do processo (DEFAULT_START_EVENT), é o exigido para iniciar; numaEXTERNAL_TASK, o exigido para concluí-la.outputs— o que precisa existir depois da etapa, numaEXTERNAL_TASKou numaEXECUTABLE_TASK.
Cada vínculo é { "variable": "<key>", "required": true }. Sem required (ou com false), a variável é
opcional: não precisa vir, mas se vier é validada.
Um processo de abertura de conta com contrato de dados
{
"key": "abertura-de-conta",
"name": "Abertura de Conta Digital",
"variableDeclarations": [
{ "key": "cpf", "label": "CPF", "format": "SHORT_TEXT",
"validationRegex": "\\d{11}", "validationMessage": "Informe o CPF com 11 dígitos, sem pontuação." },
{ "key": "nome", "label": "Nome completo", "format": "SHORT_TEXT" },
{ "key": "rendaDeclarada", "label": "Renda mensal declarada", "format": "MONEY", "currency": "BRL" },
{ "key": "score", "label": "Score de crédito", "format": "INTEGER" },
{ "key": "decisao", "label": "Decisão", "format": "SINGLE_SELECT",
"options": [ { "value": "APROVADA", "label": "Aprovar" }, { "value": "RECUSADA", "label": "Recusar" } ] },
{ "key": "observacoes", "label": "Observações", "format": "LONG_TEXT" }
],
"defaultStartPoint": "INICIO",
"flowNodes": {
"INICIO": {
"id": "INICIO", "type": "DEFAULT_START_EVENT",
"variableBindings": {
"inputs": [
{ "variable": "cpf", "required": true },
{ "variable": "nome", "required": true },
{ "variable": "rendaDeclarada", "required": true }
]
},
"outgoing": [ { "targetNodeId": "CALCULAR_SCORE" } ]
},
"CALCULAR_SCORE": {
"id": "CALCULAR_SCORE", "name": "Calcular Score", "type": "EXECUTABLE_TASK",
"executor": "calcularScoreTaskHandler",
"variableBindings": { "outputs": [ { "variable": "score", "required": true } ] },
"outgoing": [ { "targetNodeId": "APROVAR_ABERTURA" } ]
},
"APROVAR_ABERTURA": {
"id": "APROVAR_ABERTURA", "name": "Aprovar Abertura", "type": "EXTERNAL_TASK",
"variableBindings": {
"inputs": [
{ "variable": "decisao", "required": true },
{ "variable": "observacoes" }
]
},
"outgoing": [ { "targetNodeId": "FIM" } ]
},
"FIM": { "id": "FIM", "type": "DEFAULT_END_EVENT", "outgoing": [] }
}
}
No Craft, o catálogo fica em Configurações do Processo → Variáveis, e os vínculos no painel de cada nó, em
Dados da etapa. A ordem dos inputs é a ordem em que os campos aparecem no formulário.
Formatos
O format é um conjunto fechado. Cada um define o que o motor aceita como valor:
format | Valor aceito | Observação |
|---|---|---|
SHORT_TEXT, LONG_TEXT | String | Únicos que aceitam validationRegex. |
INTEGER | Integer/Long/BigInteger, Double sem parte fracionária, ou string numérica inteira | "42.0" e "1e3" não são inteiros. |
DECIMAL | qualquer Number finito, ou string parseável por BigDecimal | "10,50" não é aceito — o separador é ponto. |
MONEY | como DECIMAL | A moeda é fixa na declaração (currency, ISO-4217); o valor é só o número. |
BOOLEAN | Boolean | A string "true" não é aceita. |
DATE | LocalDate, java.util.Date, ou string yyyy-MM-dd de uma data real | 2026-02-30 falha. |
DATETIME | OffsetDateTime/ZonedDateTime/Instant/LocalDateTime/Date, ou string ISO-8601 com offset | 2026-09-28T10:00:00 sem offset falha. |
SINGLE_SELECT | String igual a um options[].value | |
MULTI_SELECT | coleção de String, todas entre os options[].value |
O validationRegex precisa casar com o valor inteiro (semântica de Matcher.matches(), sem ^/$
necessários). Valores vazios — null, string só com espaços, coleção vazia — contam como ausentes.
extensionProperties de uma declaração é um mapa livre de dicas para o frontend (placeholder, máscara); o
motor não interpreta.
Quando o motor confere
| Momento | Contra o quê | Se falhar |
|---|---|---|
Início (startProcess().execute()) | As variáveis passadas em withVariables(...). | VariableValidationException na própria chamada; nada é persistido. |
Conclusão de EXTERNAL_TASK (completeExternalTask) | O estado mesclado da instância — um campo obrigatório pode ter vindo de uma etapa anterior. inputs e outputs são conferidos juntos. | VariableValidationException na chamada; a tarefa continua pendente. |
Fim de EXECUTABLE_TASK | O estado da instância logo depois do TaskHandler, antes de seguir para o próximo nó. | Tratada como falha do handler: retry pela retryPolicy do nó e, esgotadas as tentativas, incidente. |
Todos os erros são acumulados em uma única exceção — um por campo —, para o formulário marcar tudo de uma
vez. Cada erro tem a variable, um code e uma mensagem (em inglês, ou o validationMessage declarado):
code | Quando |
|---|---|
REQUIRED | Vínculo com required: true e valor ausente ou vazio. |
INVALID_FORMAT | Valor presente, mas fora do formato declarado. |
INVALID_OPTION | Select com valor que não está entre as options. |
PATTERN_MISMATCH | Texto que não casa com o validationRegex. |
UNDECLARED | Vínculo para uma key fora do catálogo. O deploy já rejeita esse caso, então uma definição implantada normalmente não chega a produzi-lo. |
Variáveis enviadas mas não vinculadas continuam aceitas, e um processo sem catálogo se comporta exatamente como antes.
Tratando a falha no seu código
package com.empresa.processo.abertura;
import io.kikwiflow.KikwiflowEngine;
import io.kikwiflow.exception.VariableValidationException;
import io.kikwiflow.model.execution.ProcessVariable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.Map;
@RestController
public class SolicitacaoController {
private final KikwiflowEngine kikwiflowEngine;
public SolicitacaoController(KikwiflowEngine kikwiflowEngine) {
this.kikwiflowEngine = kikwiflowEngine;
}
public record Solicitacao(String cpf, String nome, BigDecimal rendaDeclarada) {}
@PostMapping("/solicitacoes")
public ResponseEntity<?> abrir(@RequestBody Solicitacao solicitacao) {
try {
kikwiflowEngine.startProcess()
.byKey("abertura-de-conta")
.withBusinessKey(solicitacao.cpf())
.withVariables(Map.of(
"cpf", new ProcessVariable("cpf", solicitacao.cpf()),
"nome", new ProcessVariable("nome", solicitacao.nome()),
"rendaDeclarada", new ProcessVariable("rendaDeclarada", solicitacao.rendaDeclarada())))
.execute();
return ResponseEntity.accepted().build();
} catch (VariableValidationException e) {
return ResponseEntity.status(HttpStatus.UNPROCESSABLE_ENTITY).body(e.getErrors());
}
}
}
e.getErrors() devolve a lista de VariableValidationError(variable, code, message); e.getNodeId()
diz em qual nó a validação falhou.
Um Map.of que omita "nome" não chega a iniciar a instância. Nesse caso, a resposta é um 422 com um erro
REQUIRED para nome.
O Initializr do Craft gera, a partir do catálogo, uma classe <Processo>Variables no subpacote variables
— aqui, com.empresa.processo.variables.AberturaDeContaVariables — com uma constante por key
(AberturaDeContaVariables.RENDA_DECLARADA) e, para os selects, uma classe aninhada com os valores das opções
(AberturaDeContaVariables.Decisao.APROVADA). Renomear uma key no catálogo e regenerar vira erro de
compilação nos handlers que usavam o nome antigo.
outputs numa EXECUTABLE_TASK
Declarar score como output obrigatório de CALCULAR_SCORE garante que nenhum gateway adiante decida sobre
uma instância sem score — se o handler terminar sem gravá-lo, ou gravar "setecentos", a etapa falha como se
o handler tivesse lançado uma exceção. É especialmente útil quando o handler repassa a resposta de um serviço
que você não controla (uma API externa, um modelo de linguagem): o formato esperado fica declarado no
processo, não escondido no código.
Por padrão, essa falha segue o retry do nó — o que faz sentido se a próxima chamada ao serviço pode vir
certa. Se não pode, liste io.kikwiflow.exception.VariableValidationException em
kikwiflow.retry.fatal-exceptions para abrir o incidente na primeira falha (ver
Configuração Essencial).
Formulários vindos do backend
Como o catálogo já descreve cada campo, o motor monta a descrição do formulário de cada etapa — e o seu
frontend só a renderiza, sem duplicar regra nenhuma. Com kikwi-management-rest-spring-boot-starter no
classpath:
| Endpoint | Devolve |
|---|---|
GET /process-definitions/one-by-key/{key}/start-form | Os campos para iniciar a versão mais recente do processo (sem valores). |
GET /external-tasks/{id}/form | Os campos para concluir a tarefa, com value pré-preenchido pelas variáveis atuais da instância. |
{
"processDefinitionId": "fc451f6f-a944-4dbb-b2ba-b5fb6433f40a",
"processDefinitionKey": "abertura-de-conta",
"nodeId": "APROVAR_ABERTURA",
"nodeName": "Aprovar Abertura",
"fields": [
{ "key": "decisao", "label": "Decisão", "description": null, "format": "SINGLE_SELECT", "required": true,
"options": [ { "value": "APROVADA", "label": "Aprovar" }, { "value": "RECUSADA", "label": "Recusar" } ],
"currency": null, "validationRegex": null, "validationMessage": null, "extensionProperties": null,
"value": null },
{ "key": "observacoes", "label": "Observações", "description": null, "format": "LONG_TEXT", "required": false,
"options": [], "currency": null, "validationRegex": null, "validationMessage": null,
"extensionProperties": null, "value": null }
]
}
Os campos vêm na ordem dos inputs do nó. Um nó sem inputs devolve fields vazio. Na conclusão via
POST /external-tasks/{id}/complete ou no início via
POST /process-instances, uma falha de validação volta como
422 Unprocessable Entity, com um erro por campo:
{
"code": "VARIABLE_VALIDATION_FAILED",
"message": "Variable validation failed for node 'APROVAR_ABERTURA': [decisao=REQUIRED]",
"nodeId": "APROVAR_ABERTURA",
"errors": [ { "variable": "decisao", "code": "REQUIRED", "message": "Variable 'Decisão' is required." } ]
}
O motor valida os dados de uma etapa, mas não restringe quem a executa: qualquer chamador que alcance a API
consegue ler o formulário (com os valores atuais das variáveis) e concluir a tarefa — não há como declarar no
.kikwi que "Aprovar Abertura" é só para analistas de crédito. Essa decisão fica na sua aplicação. Não exponha
a API de gestão diretamente ao navegador do usuário final: faça o frontend chamar um backend seu, que autentica
o usuário, confere se ele pode atuar naquela tarefa e só então chama o motor (ver
Quem pode assumir e concluir uma tarefa externa).
Quando usar / quando não usar
Use o catálogo quando:
- dados entram no processo vindos de pessoas — o formulário de início, a decisão de um analista — e você quer que o motor recuse dado faltando ou malformado antes de a instância andar;
- um frontend precisa montar a tela de cada etapa sem duplicar as regras do processo;
- um
TaskHandlerrepassa a resposta de um serviço externo, e as etapas seguintes dependem de ela ter um formato garantido (outputs).
Não use:
- para regras que dependem de mais de um campo ("data final depois da inicial") ou de consulta externa ("CPF
com dígito verificador válido", "cliente não está em lista restritiva") — o catálogo valida cada campo
isoladamente, por formato e regex. Coloque essas regras num
TaskHandlere sinalize a recusa com um erro de negócio; - para declarar toda variável interna que um handler grava e só outro handler lê — isso é código conversando com código, e o teste unitário do handler cobre melhor.
Limitações a conhecer:
- o conjunto de campos de cada etapa é estático — não varia por tipo de produto ou por outra variável;
inputssó existem no início e emEXTERNAL_TASK;EVENT_CATCHEReBOUNDARY_INTERRUPTIVE_CATCH_EVENTnão validam as variáveis que chegam por correlação;- num
EVENT_CATCHERem modoGROUP, variáveis de correlações parciais são gravadas sem passar por validação; - um subprocesso (
CALL_ACTIVITY_COORDINATOR) inicia o filho pelo mesmo caminho do start: se o filho exigeinputsque o pai não tem, a falha aparece no coordenador do pai.
Próximo passo
O detalhamento do mecanismo — regras exatas de cada formato, o que o deploy rejeita e onde cada validação roda no motor — está em Catálogo de Variáveis e Contrato de Dados.