Catálogo de Variáveis e Contrato de Dados
Esta página descreve como o motor implementa o catálogo de variáveis (variableDeclarations) e o contrato de
dados por nó (variableBindings): o modelo, as regras de validação de cada formato, onde cada validação roda,
o que o deploy rejeita e como os formulários são descritos pela API. O uso do dia a dia, com exemplo completo,
está no guia: Catálogo de Variáveis e Formulários.
O objetivo do mecanismo é deixar o backend determinar os campos e o formato dos dados que precisam ser
coletados em cada etapa, e o motor garantir que esses dados existam. ProcessVariable continua com
Object value, sem sistema de tipos: o formato é checado no momento em que a variável é exigida por um nó,
não carregado adiante.
Modelo
Pacote io.kikwiflow.model.definition.process.variable (kikwi-model):
| Tipo | Papel |
|---|---|
VariableDeclaration(key, label, description, format, options, currency, validationRegex, validationMessage, extensionProperties) | Entrada do catálogo. |
VariableFormat | SHORT_TEXT, LONG_TEXT, INTEGER, DECIMAL, MONEY, BOOLEAN, DATE, DATETIME, SINGLE_SELECT, MULTI_SELECT. |
VariableOption(value, label) | Opção de um select: value é o que é gravado, label o que é exibido. |
VariableBinding(variable, required) | Vínculo de um nó a uma key do catálogo. required ausente = false. |
VariableBindings(inputs, outputs) | Contrato de dados de um nó; listas ausentes viram listas vazias. |
VariableBindingAware | Implementada pelos nós que aceitam variableBindings. |
ProcessDefinitiontemList<VariableDeclaration> variableDeclarationsefindVariableDeclaration(key).StartEventDefinition,ExternalTaskDefinitioneExecutableTaskDefinitiontêmVariableBindings variableBindingse implementamVariableBindingAware. Nenhum outro tipo de nó aceita vínculos.- O catálogo e os vínculos fazem parte do JSON da definição — entram no checksum de deploy como qualquer outro campo, então mudar o catálogo cria uma nova versão do processo.
extensionPropertiesde uma declaração é um mapa livre de dicas para o frontend (placeholder, máscara, ordem de colunas); o motor não interpreta.
Por que um enum fechado, e não um nome de classe Java
Aceitar um nome de classe vindo do JSON de deploy e resolvê-lo por reflection abriria um vetor de
carregamento de classe controlado por quem escreve o processo. O enum fechado cobre os formatos que um
formulário precisa e é despachado para parsers nativos (BigDecimal, LocalDate.parse...), sem reflection
e sem avaliação de expressão.
Validação: VariableContractValidator
io.kikwiflow.variable.VariableContractValidator (kikwi-core, sem Spring) valida uma lista de
VariableBinding contra o catálogo e um mapa de variáveis. Para cada vínculo, nesta ordem, gerando no máximo
um erro por campo:
keyfora do catálogo →UNDECLARED.- Valor ausente ou vazio →
REQUIREDse o vínculo érequired; senão o campo é ignorado. Vazio énull, coleção vazia ou string composta só de caracteres em branco segundoCharacter.isWhitespace. - Valor presente → checagem de formato →
INVALID_FORMATouINVALID_OPTION. - Texto com
validationRegex→ precisa casar com o valor inteiro, sem trim (Matcher.matches()) →PATTERN_MISMATCH, com ovalidationMessagedeclarado como mensagem.
Os erros são acumulados e lançados juntos como
io.kikwiflow.exception.VariableValidationException(nodeId, List<VariableValidationError>), onde
VariableValidationError(variable, code, message) está em io.kikwiflow.variable. As mensagens geradas pelo
motor são em inglês e usam o label da declaração ("Variable 'Valor Solicitado' is required.").
Regras de cada formato
| Formato | Aceito | Recusado (exemplos) |
|---|---|---|
SHORT_TEXT, LONG_TEXT | CharSequence | qualquer não-texto |
INTEGER | Integer/Long/Short/Byte/BigInteger; Double/Float/BigDecimal sem parte fracionária; string aceita por new BigInteger(s.trim()) | "42.0", "1e3" |
DECIMAL, MONEY | qualquer Number finito; string aceita por new BigDecimal(...) (+1.5, .5, 1., 1e3) | NaN, Infinity, "10,50" |
BOOLEAN | Boolean | a string "true" |
DATE | LocalDate, java.util.Date, string yyyy-MM-dd de uma data real (resolver estrito) | "2026-02-30" |
DATETIME | OffsetDateTime, ZonedDateTime, Instant, LocalDateTime, Date; string aceita por OffsetDateTime.parse (offset obrigatório) | "2026-09-28T10:00:00" |
SINGLE_SELECT | String igual a um options[].value | valor fora das opções (INVALID_OPTION), não-string (INVALID_FORMAT) |
MULTI_SELECT | coleção de String, todas entre os options[].value | o primeiro item fora das opções gera INVALID_OPTION |
MONEY carrega a moeda na declaração (currency, ISO-4217); o valor da variável é só o número.
Onde a validação roda
| Hook | Onde | Contra quais variáveis | Efeito da falha |
|---|---|---|---|
| Start | ProcessStarter.execute(), antes de executeFlow, se o defaultStartPoint for VariableBindingAware — só inputs | As passadas em withVariables(...) | Exceção na chamada; nada é persistido. |
Conclusão de EXTERNAL_TASK | completeExternalTask, logo após o merge das variáveis recebidas — inputs e outputs, erros acumulados numa só exceção | O estado mesclado da instância, numa cópia | Exceção na chamada; nada é persistido e a tarefa continua pendente. |
Fim de EXECUTABLE_TASK | ProcessExecutionManager.executeFlow, logo após o TaskHandler e antes de navegar — só outputs | O estado da instância depois do handler | Tratada como falha do handler: FLOW_NODE_FINISHED(ERROR), retry pela retryPolicy do nó e, esgotado, incidente FAILED_JOB. |
Consequências que vale conhecer:
- No start, a validação usa só o que veio em
withVariables(...); na conclusão de uma tarefa, o estado acumulado — um campo obrigatório pode ter sido preenchido numa etapa anterior. outputsnumDEFAULT_START_EVENTsão aceitos pelo deploy, mas não são verificados em momento nenhum.- Numa
EXECUTABLE_TASK, nenhum gateway chega a avaliar uma saída fora do contrato. Para tornar a falha não retentável, listeio.kikwiflow.exception.VariableValidationExceptionemkikwiflow.retry.fatal-exceptions(ver Retry e Resiliência). EVENT_CATCHERe afins também concluem pelo caminho decompleteExternalTask(viacorrelateMessageouEVENT_THROWER), mas sóExternalTaskDefinitioné validada.- Variáveis enviadas mas não vinculadas continuam aceitas; um processo sem catálogo nem vínculos se comporta exatamente como antes.
Limitações:
- Filhos de
CALL_ACTIVITY_COORDINATORpassam pelo mesmoProcessStarter: se o processo filho declararinputsobrigatórios que o pai não repassa, a falha sobe para a execução do coordenador no pai. - Correlação
GROUPparcial (antes de omatchPolicyser satisfeito) grava variáveis sem concluir o nó — não passa pela validação; só a conclusão efetiva valida. - Regras entre campos ou dependentes de dados externos não são expressáveis no catálogo; o conjunto de campos de cada etapa é estático.
Validação no deploy
O DeployValidator aplica as regras KIKWI-060 a KIKWI-065 (ver
Regras de Processo Válido):
- toda declaração tem
keynão-vazia e única,labelnão-vazio eformat; optionssó (e sempre) emSINGLE_SELECT/MULTI_SELECT, comvaluenão-vazio e único;MONEYcomcurrencyISO-4217 válida (java.util.Currency);validationRegexsó em texto, com no máximo 512 caracteres, e compilável;- todo vínculo referencia uma
keydo catálogo, no máximo uma vez por lista; EXECUTABLE_TASKnão aceitainputs— não há ator fornecendo dados ali; sóoutputs.
Formulários: FormDescriptorFactory
io.kikwiflow.variable.FormDescriptorFactory (kikwi-core) monta um
io.kikwiflow.model.execution.form.FormDescriptor(processDefinitionId, processDefinitionKey, nodeId, nodeName, fields)
a partir dos inputs do nó, na ordem declarada:
forStart(processDefinition)— o nódefaultStartPoint, sem valores;forNode(processDefinition, nodeId, node, currentVariables)— qualquer nó, comvaluepré-preenchido a partir das variáveis informadas.
Cada FormField(key, label, description, format, required, options, currency, validationRegex, validationMessage, extensionProperties, value) achata a declaração do catálogo com o required do vínculo.
Um nó sem inputs (ou que não seja VariableBindingAware) gera fields vazio.
A API REST de gestão expõe as duas formas:
| Método | Rota | Retorna |
|---|---|---|
GET | /process-definitions/one-by-key/{key}/start-form | FormDescriptor do start da versão mais recente. |
GET | /external-tasks/{id}/form | FormDescriptor da tarefa, com os valores atuais da instância. |
O catálogo completo também sai em GET /process-definitions/... (é parte de ProcessDefinition) e no
snapshot de estatísticas (Pulse).
VariableValidationException vira 422 Unprocessable Entity na API:
{
"code": "VARIABLE_VALIDATION_FAILED",
"message": "Variable validation failed for node 'REVIEW': [amount=REQUIRED]",
"nodeId": "REVIEW",
"errors": [ { "variable": "amount", "code": "REQUIRED", "message": "Variable 'Valor Solicitado' is required." } ]
}
422, e não 400, porque o payload é sintaticamente válido — o que falha é a regra declarada no processo.
Segurança
- Nenhuma avaliação de expressão (SpEL, ScriptEngine) nem reflection sobre classe arbitrária.
- O risco real do
validationRegexé ReDoS, não execução de código: um padrão patológico contra a entrada de um usuário pode prender uma thread. A mitigação é o limite de 512 caracteres e a compilação obrigatória no deploy — coerente com o modelo de confiança, já que quem escreve o regex já tem permissão de deploy. Não há timeout de avaliação. Os padrões compilados ficam em cache. - Os endpoints de formulário devolvem valores atuais de variáveis.
GET /external-tasks/{id}/formsegue o mesmo modelo de acesso deGET /external-tasks/{id}— sem checagem de tenant no lado de consulta — e não passa peloVariableSecurityPolicyManager(ver Segurança e Multi-tenancy). Não exponha a API de gestão diretamente ao navegador do usuário final.