Pular para o conteúdo principal

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

TipoPapel
VariableDeclaration(key, label, description, format, options, currency, validationRegex, validationMessage, extensionProperties)Entrada do catálogo.
VariableFormatSHORT_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.
VariableBindingAwareImplementada pelos nós que aceitam variableBindings.
  • ProcessDefinition tem List<VariableDeclaration> variableDeclarations e findVariableDeclaration(key).
  • StartEventDefinition, ExternalTaskDefinition e ExecutableTaskDefinition têm VariableBindings variableBindings e implementam VariableBindingAware. 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.
  • extensionProperties de 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:

  1. key fora do catálogo → UNDECLARED.
  2. Valor ausente ou vazio → REQUIRED se o vínculo é required; senão o campo é ignorado. Vazio é null, coleção vazia ou string composta só de caracteres em branco segundo Character.isWhitespace.
  3. Valor presente → checagem de formato → INVALID_FORMAT ou INVALID_OPTION.
  4. Texto com validationRegex → precisa casar com o valor inteiro, sem trim (Matcher.matches()) → PATTERN_MISMATCH, com o validationMessage declarado 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​

FormatoAceitoRecusado (exemplos)
SHORT_TEXT, LONG_TEXTCharSequencequalquer não-texto
INTEGERInteger/Long/Short/Byte/BigInteger; Double/Float/BigDecimal sem parte fracionária; string aceita por new BigInteger(s.trim())"42.0", "1e3"
DECIMAL, MONEYqualquer Number finito; string aceita por new BigDecimal(...) (+1.5, .5, 1., 1e3)NaN, Infinity, "10,50"
BOOLEANBooleana string "true"
DATELocalDate, java.util.Date, string yyyy-MM-dd de uma data real (resolver estrito)"2026-02-30"
DATETIMEOffsetDateTime, ZonedDateTime, Instant, LocalDateTime, Date; string aceita por OffsetDateTime.parse (offset obrigatório)"2026-09-28T10:00:00"
SINGLE_SELECTString igual a um options[].valuevalor fora das opções (INVALID_OPTION), não-string (INVALID_FORMAT)
MULTI_SELECTcoleção de String, todas entre os options[].valueo 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​

HookOndeContra quais variáveisEfeito da falha
StartProcessStarter.execute(), antes de executeFlow, se o defaultStartPoint for VariableBindingAware — só inputsAs passadas em withVariables(...)Exceção na chamada; nada é persistido.
Conclusão de EXTERNAL_TASKcompleteExternalTask, logo após o merge das variáveis recebidas — inputs e outputs, erros acumulados numa só exceçãoO estado mesclado da instância, numa cópiaExceção na chamada; nada é persistido e a tarefa continua pendente.
Fim de EXECUTABLE_TASKProcessExecutionManager.executeFlow, logo após o TaskHandler e antes de navegar — só outputsO estado da instância depois do handlerTratada 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.
  • outputs num DEFAULT_START_EVENT sã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, liste io.kikwiflow.exception.VariableValidationException em kikwiflow.retry.fatal-exceptions (ver Retry e Resiliência).
  • EVENT_CATCHER e afins também concluem pelo caminho de completeExternalTask (via correlateMessage ou EVENT_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_COORDINATOR passam pelo mesmo ProcessStarter: se o processo filho declarar inputs obrigatórios que o pai não repassa, a falha sobe para a execução do coordenador no pai.
  • Correlação GROUP parcial (antes de o matchPolicy ser 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 key não-vazia e única, label não-vazio e format;
  • options só (e sempre) em SINGLE_SELECT/MULTI_SELECT, com value não-vazio e único;
  • MONEY com currency ISO-4217 válida (java.util.Currency);
  • validationRegex só em texto, com no máximo 512 caracteres, e compilável;
  • todo vínculo referencia uma key do catálogo, no máximo uma vez por lista;
  • EXECUTABLE_TASK não aceita inputs — 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ó, com value pré-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étodoRotaRetorna
GET/process-definitions/one-by-key/{key}/start-formFormDescriptor do start da versão mais recente.
GET/external-tasks/{id}/formFormDescriptor 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}/form segue o mesmo modelo de acesso de GET /external-tasks/{id} — sem checagem de tenant no lado de consulta — e não passa pelo VariableSecurityPolicyManager (ver Segurança e Multi-tenancy). Não exponha a API de gestão diretamente ao navegador do usuário final.