Correlação de Eventos — EVENT_CATCHER e EVENT_THROWER
Pense num guarda-volumes de aeroporto. Você deixa a mala e recebe uma ficha com um número. Mais tarde, qualquer pessoa que apresente aquele número retira a mala — o atendente não precisa saber quem você é, em que voo chegou, nem onde estava sentado. O número basta.
Uma
EVENT_CATCHERé o balcão esperando: a instância para e diz "vou seguir quando aparecer a chaveconta-criada-11122233344". UmEVENT_THROWER, em outra instância ou noutro processo, é quem chega com essa chave na mão. Onde a analogia quebra: no guarda-volumes há sempre uma mala esperando; no Kikwiflow, se ninguém está esperando a chave quando oEVENT_THROWERa apresenta, o nó falha (ver a política de v1 mais abaixo).
Uma EVENT_CATCHER modela um passo que aguarda um evento externo de negócio — um webhook, um
callback assíncrono, uma mensagem Kafka/RabbitMQ — identificado por uma chave de correlação,
não por um taskId interno do motor. É a resposta do Kikwiflow para o padrão Message Catch /
Receive Task, e para o padrão Scatter-Gather quando é preciso aguardar várias correlações antes
de seguir.
Diferente de uma EXTERNAL_TASK, que só pode ser destravada
sabendo o taskId gerado pelo motor, uma EVENT_CATCHER é destravada por uma chave de negócio
que o sistema que envia o evento já conhece — por exemplo "conta-criada-11122233344" — sem precisar
consultar processInstanceId/taskId nenhum antes.
No fio de abertura de conta, o encaixe natural é a confirmação do core bancário: depois de Aprovar Abertura, o processo pede ao core que crie a conta de fato e para, aguardando o callback assíncrono de "conta criada" daquele CPF antes de dar a conta como ativa.
EVENT_CATCHER — aguardando uma correlação
"type": "EVENT_CATCHER". Todo campo é lido do JSON com esse nome exato — não há aliases.
| Campo | Tipo | Obrigatório? | Descrição |
|---|---|---|---|
id | string | Sim | Identificador único do nó. Referenciado por targetNodeId de outras arestas e por attachedToRef de eventos de borda. |
name / description | string | Não | Rótulo e texto livre — sem efeito em tempo de execução. |
outgoing | array | Sim (≥1) | Exatamente uma aresta é usada (outgoing[0]) — este nó não ramifica. |
catchType | STANDALONE | GROUP | Sim | Ver abaixo. |
providerType | STATIC | VARIABLE | BEAN | TEMPLATE | Sim | Estratégia de resolução da(s) chave(s). |
staticKey | string | Só se STATIC | Chave fixa. Só faz sentido em STANDALONE. |
providerVariable | string | Só se VARIABLE | Nome da variável. Em STANDALONE, valor escalar; em GROUP, deve ser uma List em tempo de execução. |
providerBean | string | Só se BEAN | Bean @Component que implementa CorrelationKeysProvider. |
correlationTemplates | array | Só se TEMPLATE | Encadeamento de segmentos LITERAL/VARIABLE (ver abaixo). |
keyPrefix / keySuffix | string | Não | Só com VARIABLE — concatenados antes/depois do valor da variável. |
displayNamePrefix / displayNameSuffix | string | Não | Rótulo humano opcional; sem nenhum, o rótulo é a própria chave técnica. |
matchPolicy | ALL | ANY | Sim quando GROUP | Ver "Política de casamento". |
boundaryEventIds | array | Não | IDs de BOUNDARY_INTERRUPTIVE_TIMER/BOUNDARY_NON_INTERRUPTIVE_TIMER anexados. Não aceita BOUNDARY_INTERRUPTIVE_CATCH_EVENT nem BOUNDARY_ERROR_HANDLER. |
:::info STATIC em modo GROUP não é suportado
STATIC sempre resolve exatamente 1 item. Em GROUP, use VARIABLE, BEAN ou TEMPLATE.
:::
Os dois modos: STANDALONE e GROUP
catchType | Comportamento | Uso típico |
|---|---|---|
STANDALONE | Aguarda uma única chave. Ao recebê-la, segue direto pelo outgoing. | Confirmação de pagamento, callback único de um parceiro. |
GROUP | Aguarda N chaves (de uma lista/bean). Gera uma tarefa-mãe coordenadora + N tarefas-filhas. | Aguardar a ativação de vários produtos contratados na abertura; cotação com múltiplos fornecedores. |
EVENT_CATCHER STANDALONE
{
"id": "AGUARDAR_CONTA_CRIADA",
"name": "Aguardar Confirmação do Core",
"type": "EVENT_CATCHER",
"catchType": "STANDALONE",
"providerType": "VARIABLE",
"providerVariable": "cpf",
"keyPrefix": "conta-criada-",
"outgoing": [ { "targetNodeId": "CONTA_ATIVA" } ]
}
Se cpf = "11122233344", a chave resolvida em tempo de execução é "conta-criada-11122233344".
EVENT_CATCHER GROUP
{
"id": "AGUARDAR_PRODUTOS_ATIVADOS",
"name": "Aguardar Ativação de Todos os Produtos",
"type": "EVENT_CATCHER",
"catchType": "GROUP",
"providerType": "VARIABLE",
"providerVariable": "produtos",
"keyPrefix": "produto-ativado-",
"matchPolicy": "ALL",
"boundaryEventIds": ["TIMEOUT_ATIVACAO"],
"outgoing": [ { "targetNodeId": "NOTIFICAR_CLIENTE" } ]
}
Se produtos = ["conta-corrente", "cartao-credito"], o motor cria uma tarefa-mãe aguardando
["produto-ativado-conta-corrente", "produto-ativado-cartao-credito"] e duas tarefas-filhas, uma
por chave.
Estratégias de resolução da chave (providerType)
providerType | Origem | Campos |
|---|---|---|
STATIC | Chave fixa no JSON | staticKey |
VARIABLE | Variável de processo. STANDALONE: escalar; GROUP: List de tamanho só conhecido em tempo de execução. | providerVariable, keyPrefix, keySuffix |
BEAN | Bean Spring CorrelationKeysProvider | providerBean |
TEMPLATE | Encadeamento de texto fixo + variáveis, declarado no .kikwi | correlationTemplates |
É o mesmo padrão de estratégias dos timers (DueDateProvider), agora
aplicado a chaves de correlação.
TEMPLATE — quando você já sabe exatamente quais eventos vai aguardar
VARIABLE (list) serve para quando o tamanho da lista só é conhecido em tempo de execução. Quando você
já sabe, ao desenhar o processo, quais eventos vai aguardar — só precisa combinar texto fixo
com uma ou mais variáveis — use TEMPLATE:
{
"id": "AGUARDAR_CONTA_CRIADA",
"type": "EVENT_CATCHER",
"catchType": "STANDALONE",
"providerType": "TEMPLATE",
"correlationTemplates": [
{
"keySegments": [
{ "type": "LITERAL", "value": "conta-criada-" },
{ "type": "VARIABLE", "value": "cpf" }
],
"displayNameSegments": [ { "type": "LITERAL", "value": "Confirmação do core bancário" } ]
}
],
"outgoing": [ { "targetNodeId": "CONTA_ATIVA" } ]
}
Cada segmento é LITERAL (texto fixo) ou VARIABLE (nome de uma variável de processo). Em
catchType: GROUP, correlationTemplates é uma lista de templates — um por evento esperado,
declarada de forma fixa no .kikwi, sem depender de nenhuma variável de lista.
CorrelationKeysProvider — resolução programática
Quando a regra de geração das chaves é complexa demais para prefix/suffix:
package com.empresa.processo.correlacao;
import io.kikwiflow.execution.api.context.EvaluationContext;
import io.kikwiflow.execution.api.correlation.CorrelationItem;
import io.kikwiflow.execution.api.correlation.CorrelationKeysProvider;
import org.springframework.stereotype.Component;
import java.util.List;
@Component("produtoCorrelationResolver")
public class ProdutoCorrelationResolver implements CorrelationKeysProvider {
@Override
@SuppressWarnings("unchecked")
public List<CorrelationItem> resolveCorrelationItems(EvaluationContext context) {
List<Produto> produtos = (List<Produto>) context.getVariableValue("produtos").orElseThrow();
return produtos.stream()
.map(p -> new CorrelationItem("produto-ativado-" + p.id(), "Ativação: " + p.nome()))
.toList();
}
}
CorrelationItem(key, displayName) carrega a chave técnica e um rótulo amigável opcional. Em
STANDALONE, o provider deve resolver exatamente 1 item.
Política de casamento em modo GROUP (matchPolicy)
matchPolicy | Comportamento |
|---|---|
ALL | O nó só conclui quando todas as chaves forem correlacionadas. |
ANY | O nó conclui na primeira chave correlacionada (first-response-wins); as filhas restantes são canceladas. |
Correlacionando um evento
Do lado de quem recebe o webhook/mensagem, basta a chave — nada de processInstanceId/taskId:
@PostMapping("/webhooks/produtos")
public ResponseEntity<Void> onProdutoAtivado(@RequestBody ProdutoAtivadoEvent event,
IdentityContext identityContext) {
String correlationKey = "produto-ativado-" + event.produtoId();
Map<String, ProcessVariable> variaveis = Map.of(
"status_" + event.produtoId(), new ProcessVariable("status", "OK"));
kikwiflowEngine.correlateMessage(correlationKey, variaveis, identityContext);
return ResponseEntity.ok().build();
}
correlateMessage lança TaskNotFoundException sempre que a chave não corresponde a nenhuma
tarefa ativa — seja porque nunca existiu, seja porque já foi consumida. Isso dá proteção
natural contra reentrega duplicada de webhooks: a segunda entrega da mesma mensagem cai nesse erro,
e a aplicação decide o que fazer (ignorar, mandar para uma dead letter queue).
Validações
Todas acontecem em tempo de execução, quando o nó é alcançado — não na implantação. Hoje não
há validador de implantação dedicado a EVENT_CATCHER: um .kikwi com configuração inválida é
implantado normalmente e
só falha quando uma instância passa por aquele nó. Condições que lançam IllegalStateException:
providerType/campo do modo nulo ou vazio; variável ausente/null; VARIABLE+GROUP com valor
que não é List; BEAN sem bean registrado; provider que resolve lista vazia; STANDALONE com
provider que resolve mais de 1 item. Chaves repetidas são deduplicadas silenciosamente
(preservando a ordem), o que não é erro.
EVENT_THROWER — lançando uma correlação
EVENT_THROWER ("type": "EVENT_THROWER") é a contraparte de emissão: em vez de esperar por uma
correlação externa, este nó lança uma. Um processo com um EVENT_THROWER resolve uma chave de
negócio e a entrega internamente, pelo mesmo caminho que um webhook usaria — sem que quem lança
conheça o processInstanceId/taskId de quem espera.
:::info Não é Executable nem WaitState
Ao contrário de EXECUTABLE_TASK (que delega a um TaskHandler) ou EVENT_CATCHER/EXTERNAL_TASK
(que suspendem a execução), o EVENT_THROWER tem comportamento fixo, embutido no motor — não
há bean de execução para configurar, e o nó não bloqueia quem o executa esperando resposta.
:::
Anatomia
{
"id": "NOTIFICAR_RECUSA",
"name": "Notificar Recusa da Abertura",
"type": "EVENT_THROWER",
"providerType": "VARIABLE",
"providerVariable": "cpf",
"keyPrefix": "abertura-recusada-",
"outgoing": [ { "targetNodeId": "FIM_RECUSADA" } ]
}
Com cpf = "11122233344", lança a chave abertura-recusada-11122233344 — o mesmo formato que um
EVENT_CATCHER configurado com providerVariable: cpf, keyPrefix: abertura-recusada- estaria
aguardando. Os quatro providerType são os mesmos do EVENT_CATCHER (STATIC/VARIABLE/BEAN/
TEMPLATE), resolvidos pelo mesmo mecanismo. O EVENT_THROWER sempre resolve exatamente 1
chave — não há catchType/matchPolicy. Se o provider devolver mais de um item, o throw usa
apenas o primeiro.
Política v1 para "ninguém está esperando essa chave": FAIL
:::danger Sem catcher ativo, o throw falha — não é engolido silenciosamente
Se não há nenhuma tarefa ativa com a chave lançada, TaskNotFoundException propaga como falha do
próprio nó EVENT_THROWER — a mesma trilha de incidente/retentativa de qualquer outro nó. É uma
decisão de design explícita: a v1 não tem um modo "dispara e esquece" tolerante à ausência de
catcher. Se o seu processo pode legitimamente lançar um evento sem ninguém esperando (uma
notificação de melhor esforço), trate a falha via retryPolicy com maxRetries: 0 para virar
incidente na hora, em vez de reprocessar.
:::
commitBefore: o throw pode ser síncrono ou assíncrono
commitBefore no EVENT_THROWER | Comportamento |
|---|---|
false (padrão) | O throw dispara de forma síncrona, na mesma chamada que iniciou/retomou o processo lançador. |
true | O throw não dispara imediatamente — vira uma ExecutableTask do tipo EVENT_THROW, retomada depois por um worker. |
Simetricamente, o commitAfter do catcher decide se a continuação do lado de quem esperava
roda de forma síncrona durante o throw ou fica pendente para retomada assíncrona. Ver
Execução Síncrona e Assíncrona.
Validação no momento da implantação
DeployValidator valida EVENT_THROWER com a mesma checagem dos nós que resolvem chave de
correlação: providerType não pode ser nulo, e o campo do modo escolhido precisa estar preenchido
— incluindo, para providerType: BEAN, a existência real do bean CorrelationKeysProvider no
contexto Spring. Um providerBean inexistente é rejeitado no momento da implantação, não
descoberto só em produção.
:::tip No fio de abertura de conta
Um uso natural: quando Aprovar Abertura é recusada, um EVENT_THROWER "Notificar Recusa"
(providerType: VARIABLE, chave abertura-recusada-<cpf>) avisa um processo separado de
comunicação com o cliente — que já espera essa chave num EVENT_CATCHER — sem que os dois processos
precisem conhecer o processInstanceId um do outro.
:::
Quando usar / quando não usar
| Situação | Use |
|---|---|
Quem completa a tarefa já tem (ou pode consultar) o taskId do motor — fila interna, tela operada por usuário autenticado | EXTERNAL_TASK |
| Quem sinaliza a conclusão é um sistema externo que só conhece uma chave de negócio própria (CPF, protocolo, número da solicitação) | EVENT_CATCHER STANDALONE |
| O sinal precisa ser agregado a partir de várias correlações antes do fluxo prosseguir | EVENT_CATCHER GROUP |
| Você precisa iniciar outro processo completo e esperar ele concluir (não apenas aguardar um sinal) | CALL_ACTIVITY_COORDINATOR |
| Um passo do seu processo precisa avisar outra instância que já está esperando uma chave | EVENT_THROWER |
| Cancelar uma tarefa em andamento por uma chave externa (não como próximo passo) | BOUNDARY_INTERRUPTIVE_CATCH_EVENT |
Não use EVENT_THROWER para notificações que podem não ter destinatário — na v1 isso vira
incidente. Não use EVENT_CATCHER quando o taskId já está disponível: EXTERNAL_TASK é mais
simples e não exige combinar formato de chave entre dois lados.
Próximo passo
Continue para BOUNDARY_INTERRUPTIVE_CATCH_EVENT — Cancelamento por Correlação.