Pular para o conteúdo principal

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.

Pense em um formulário de cadastro em papel

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: a key (o nome da variável de processo), um label legível, o format e, 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; numa EXTERNAL_TASK, o exigido para concluí-la.
    • outputs — o que precisa existir depois da etapa, numa EXTERNAL_TASK ou numa EXECUTABLE_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:

formatValor aceitoObservação
SHORT_TEXT, LONG_TEXTStringÚnicos que aceitam validationRegex.
INTEGERInteger/Long/BigInteger, Double sem parte fracionária, ou string numérica inteira"42.0" e "1e3" não são inteiros.
DECIMALqualquer Number finito, ou string parseável por BigDecimal"10,50" não é aceito — o separador é ponto.
MONEYcomo DECIMALA moeda é fixa na declaração (currency, ISO-4217); o valor é só o número.
BOOLEANBooleanA string "true" não é aceita.
DATELocalDate, java.util.Date, ou string yyyy-MM-dd de uma data real2026-02-30 falha.
DATETIMEOffsetDateTime/ZonedDateTime/Instant/LocalDateTime/Date, ou string ISO-8601 com offset2026-09-28T10:00:00 sem offset falha.
SINGLE_SELECTString igual a um options[].value
MULTI_SELECTcoleçã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​

MomentoContra 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_TASKO 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):

codeQuando
REQUIREDVínculo com required: true e valor ausente ou vazio.
INVALID_FORMATValor presente, mas fora do formato declarado.
INVALID_OPTIONSelect com valor que não está entre as options.
PATTERN_MISMATCHTexto que não casa com o validationRegex.
UNDECLAREDVí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.

Constantes em vez de strings soltas

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:

EndpointDevolve
GET /process-definitions/one-by-key/{key}/start-formOs campos para iniciar a versão mais recente do processo (sem valores).
GET /external-tasks/{id}/formOs 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 catálogo diz o que preencher, não quem pode preencher

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 TaskHandler repassa 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 TaskHandler e 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;
  • inputs só existem no início e em EXTERNAL_TASK; EVENT_CATCHER e BOUNDARY_INTERRUPTIVE_CATCH_EVENT não validam as variáveis que chegam por correlação;
  • num EVENT_CATCHER em modo GROUP, 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 exige inputs que 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.