Pular para o conteúdo principal
Página não listada
Esta página não está listada. Mecanismos de busca não irão indexá-la, e somente usuários que possuam o link direto poderão acessá-la

Eventos e Observabilidade

O Kikwiflow expõe dois canais de eventos completamente independentes, com garantias e propósitos diferentes. Confundir os dois é o erro de design mais comum ao integrar observabilidade — escolha o canal errado e você acaba com telemetria que promete consistência que não entrega, ou com um outbox transacional pesado demais para simples métricas de performance.

Eventos leves (lightweight)Eventos críticos (outbox)
Flag de ativaçãokikwiflow.stats.enabledkikwiflow.outbox.events-enabled
EntregaFire-and-forget, assíncrona, em memóriaPersistida na mesma transação do UnitOfWork (implementação MongoDB)
GarantiaNenhuma — pode se perder em crash/restartDurável — commit atômico junto com a mudança de estado
Uso recomendadoMétricas de performance, dashboards, APMAuditoria/histórico de execução, e como base para um consumidor de relay (ver nota sobre escopo abaixo)

:::info Escopo: esta lib entrega a persistência, não o relay O Kikwiflow OSS garante que o evento crítico existe e é consultável — de forma transacional e durável. A drenagem/entrega desses eventos para um sistema externo (Kafka, webhook, outro banco) — o "R" do padrão outbox-relay — é deliberadamente deixada como um ponto de extensão (kikwi-critical-event-relay-api), sem uma implementação embutida nesta versão. O schema da coleção outbox_events (campos relayStatus/lockedUntil, descritos abaixo) já foi desenhado para suportar um consumidor de relay — seja um construído pela sua equipe, seja uma futura oferta enterprise do Kikwiflow — sem precisar de migração de dados. :::

Eventos leves: ExecutionEventListener

A interface pública que uma aplicação implementa para observar a execução é:

@FunctionalInterface
public interface ExecutionEventListener {
void onEvents(List<LightweightEvent> events);
}

public interface LightweightEvent {
Instant getTimestamp();
}

Qualquer bean Spring que implemente ExecutionEventListener é automaticamente registrado no AsynchronousEventPublisher pelo KikwiflowAutoConfiguration (via ObjectProvider<List<ExecutionEventListener>>) — não é preciso fiação manual:

@Component
public class MetricsListener implements ExecutionEventListener {
@Override
public void onEvents(List<LightweightEvent> events) {
events.forEach(event -> {
if (event instanceof SyncContinuationFailed failure) {
log.warn("Continuação síncrona falhou: instância={}, nó={}, erro={}",
failure.processInstanceId(), failure.failedNodeDefinitionId(), failure.errorMessage());
}
});
}
}

AsynchronousEventPublisher: entrega via pool de virtual threads

public class AsynchronousEventPublisher implements EventPublisher {
private final List<ExecutionEventListener> listeners = new CopyOnWriteArrayList<>();
private final ExecutorService listenerExecutor; // Executors.newVirtualThreadPerTaskExecutor()

public void publishEvents(List<LightweightEvent> events) {
for (ExecutionEventListener listener : listeners) {
listenerExecutor.submit(() -> listener.onEvents(events));
}
}
}

Cada listener é notificado em uma submissão de executor separada — um listener lento ou que lança exceção não bloqueia nem afeta outros listeners, nem a execução do processo em si, já que a publicação acontece de forma totalmente desacoplada do caminho crítico de persistência.

:::danger Sem garantia de entrega SyncContinuationFailed, hoje o único evento leve emitido pelo motor (disparado quando uma continuação síncrona falha após a conclusão de uma tarefa externa — ver KikwiflowEngine.completeExternalTask), é publicado via fire-and-forget. Se a JVM cair entre a falha e a entrega do evento ao listener, o evento é perdido silenciosamente. Nunca implemente lógica de negócio essencial (ex.: reverter um efeito colateral, notificar um cliente) dentro de um ExecutionEventListener — use o outbox para isso. :::

O catálogo de eventos críticos: CriticalEventType

Todo evento crítico conhecido pelo motor está registrado em um único enum, usado tanto para construir eventos (em vez de literais de string soltos pelo código) quanto para o dispatch polimórfico na camada de persistência:

public enum CriticalEventType {
FLOW_NODE_FINISHED(FlowNodeFinished.class),
GATEWAY_ANSWER_RESOLVED(GatewayAnswerResolved.class),
PROCESS_INSTANCE_FINISHED(ProcessInstanceFinished.class),
PROCESS_INSTANCE_STARTED(ProcessInstanceStarted.class),
INCIDENT_CREATED(IncidentCreated.class),
INCIDENT_RESOLVED(IncidentResolved.class),
EXTERNAL_TASK_CLAIMED(ExternalTaskClaimed.class),
EXTERNAL_TASK_UNCLAIMED(ExternalTaskUnclaimed.class),
EXTERNAL_TASK_COMPLETED(ExternalTaskCompleted.class),
RETRY_SCHEDULED(RetryScheduled.class),
PROCESS_VARIABLE_CHANGED(ProcessVariableChanged.class),
TIMER_FIRED(TimerFired.class),
ORPHANED_CHILD_COMPLETION(OrphanedChildCompletion.class);

private final Class<? extends CriticalEvent> payloadType;
}
public interface CriticalEvent {
String processInstanceId();
String processDefinitionId();
}

Toda implementação de CriticalEvent expõe processInstanceId()/processDefinitionId() — isso é o que permite à camada de persistência indexar e consultar eventos por instância sem precisar de uma cadeia de instanceof.

Eventos críticos: o padrão Outbox

Cada mudança de estado relevante do processo é serializada como um OutboxEventEntity — com identidade própria (id, gerado automaticamente) — e incluída no mesmo UnitOfWork transacional que persiste a mudança de estado em si:

public class OutboxEventEntity {
private String id; // UUID gerado no construtor — vira o _id do documento persistido
private Instant timestamp;
private String event; // nome do CriticalEventType, ex. "FLOW_NODE_FINISHED"
private CriticalEvent payload;

public OutboxEventEntity(CriticalEventType type, CriticalEvent payload) { /* ... */ }
}

Quando kikwiflow.outbox.events-enabled=true, a implementação MongoDB (MongoKikwiEngineRepository.commitWork) grava cada OutboxEventEntity do UnitOfWork na coleção outbox_events, dentro da mesma transação nativa (ClientSession.withTransaction) que persiste a mudança de estado da instância:

if (outboxPersistenceEnabled && unitOfWork.events() != null && !unitOfWork.events().isEmpty()) {
List<InsertOneModel<Document>> eventWrites = unitOfWork.events().stream()
.map(OutboxEventMapper::toDocument)
.map(InsertOneModel::new)
.toList();
outboxEvents.bulkWrite(clientSession, eventWrites);
}

Isso significa que, ao contrário dos eventos leves, um evento crítico nunca é perdido silenciosamente: ou a transação inteira (mudança de estado + evento) é commitada, ou nenhuma das duas é — consistência garantida pelo mesmo mecanismo transacional descrito em Persistência e Consistência Transacional.

:::info A checagem da flag acontece de novo, no ponto de persistência kikwiflow.outbox.events-enabled é verificada tanto na construção do evento (dentro de CriticalEventRecorder, ver seção abaixo) quanto, independentemente, dentro de commitWork, imediatamente antes da escrita física na coleção. Isso garante que "outbox desabilitado" signifique de fato "nada é escrito", mesmo que algum caminho upstream decida montar o UnitOfWork com eventos preenchidos. :::

Construindo os eventos: CriticalEventRecorder

Todo evento crítico é construído por um único colaborador, CriticalEventRecorder (kikwi-core, io.kikwiflow.execution.event), injetado tanto em ProcessExecutionManager quanto em ContinuationService — antes, cada um construía seus próprios FlowNodeFinished/GatewayAnswerResolved/ProcessInstanceFinished inline, cada um com sua própria checagem de kikwiflowConfig.isOutboxEventsEnabled(), o que já causou uma inconsistência real (ProcessExecutionManager também aceitava stats.enabled como gatilho, ContinuationService não). O flag é resolvido uma única vez, no construtor do recorder:

public class CriticalEventRecorder {
private final KikwiflowConfig kikwiflowConfig;

public CriticalEventRecorder(KikwiflowConfig kikwiflowConfig) {
this.kikwiflowConfig = kikwiflowConfig;
}

public boolean isEnabled() {
return kikwiflowConfig.isOutboxEventsEnabled();
}

public void recordFlowNodeFinished(List<OutboxEventEntity> events, FlowNodeExecutionSnapshot snapshot, RuntimeException error) { /* ... */ }
public void recordGatewayAnswerResolved(List<OutboxEventEntity> events, ProcessInstanceExecution processInstance, ProcessDefinition processDefinition, ExclusiveGatewayDefinition gateway, Continuation continuation) { /* ... */ }
public void recordProcessInstanceFinished(List<OutboxEventEntity> events, ProcessInstanceExecution processInstanceExecution, ProcessDefinition processDefinition) { /* ... */ }
public void recordInterruptedFlowNode(List<OutboxEventEntity> events, ProcessInstanceExecution processInstanceExecution, ProcessDefinition processDefinition, String interruptedNodeDefinitionId, String interruptedByNodeDefinitionId) { /* ... */ }
public void recordProcessInstanceStarted(List<OutboxEventEntity> events, ProcessInstanceExecution processInstanceExecution, ProcessDefinition processDefinition) { /* ... */ }
public void recordIncidentCreated(List<OutboxEventEntity> events, Incident incident, String tenantId) { /* ... */ }
public void recordIncidentResolved(List<OutboxEventEntity> events, Incident resolvedIncident, String tenantId, String actorId) { /* ... */ }
public void recordExternalTaskClaimed(List<OutboxEventEntity> events, ExternalTask task, String assignee, String actorId) { /* ... */ }
public void recordExternalTaskUnclaimed(List<OutboxEventEntity> events, ExternalTask task, String actorId) { /* ... */ }
public void recordExternalTaskCompleted(List<OutboxEventEntity> events, ExternalTask task, String actorId) { /* ... */ }
public void recordRetryScheduled(List<OutboxEventEntity> events, ExecutableTask task, long executionsSoFar, long retriesLeft, Instant nextDueDate, String errorMessage, String tenantId) { /* ... */ }
public void recordProcessVariableChanged(List<OutboxEventEntity> events, String processInstanceId, String processDefinitionId, String tenantId, Map<String, ProcessVariable> variables, String actorId) { /* ... */ }
public void recordVariablesUnset(List<OutboxEventEntity> events, String processInstanceId, String processDefinitionId, String tenantId, Set<String> variableNames, String actorId) { /* ... */ }
public void recordTimerFired(List<OutboxEventEntity> events, ProcessInstanceExecution processInstanceExecution, String flowNodeDefinitionId, Instant nextDueDate) { /* ... */ }
}

Os parâmetros tenantId/actorId presentes em vários desses métodos (recordIncidentCreated, recordIncidentResolved, recordExternalTaskClaimed/Unclaimed, recordRetryScheduled, recordProcessVariableChanged, recordVariablesUnset) são threaded pelo chamador — Incident/ExecutableTask não carregam tenantId no domínio, e actorId só é conhecido na borda que recebeu o IdentityContext (ver as notas sobre actorId e tenantId mais abaixo).

Cada método record* recebe a lista acumuladora (List<OutboxEventEntity>) e os dados necessários, e decide sozinho — internamente — se deve montar o payload e adicioná-lo à lista; os chamadores nunca checam o flag por conta própria. recordInterruptedFlowNode também unifica o que antes eram duas construções quase idênticas em ContinuationService (uma para boundary timer anexado a ExecutableTask, outra a ExternalTask). Chamadores que fariam trabalho caro só para descartá-lo (ex.: montar um FlowNodeExecutionSnapshot completo) podem consultar isEnabled() antes, para pular esse trabalho — é o único lugar onde o flag "vaza" para fora do recorder, e só para evitar alocação desnecessária, não para decidir o quê construir.

Tipos de evento crítico emitidos hoje

eventPayload (CriticalEvent)Quando é emitido
FLOW_NODE_FINISHEDFlowNodeFinishedAo final de cada execução de nó — sucesso, erro ou interrupção (boundary timer/error handler). Inclui nodeExecutionStatus (SUCCESS/INTERRUPTED/ERROR), flowNodeName/flowNodeDescription, startedAt/finishedAt, interruptedByNodeDefinitionId quando aplicável, e errorType/errorMessage/errorStackTrace quando nodeExecutionStatus == ERROR.
GATEWAY_ANSWER_RESOLVEDGatewayAnswerResolvedSempre que um EXCLUSIVE_GATEWAY resolve uma resposta — carrega providerType, providerBean/providerVariable, a resposta resolvida e o id da aresta escolhida. Ver Navegação e Gateways de Decisão.
PROCESS_INSTANCE_FINISHEDProcessInstanceFinishedQuando uma instância atinge ProcessInstanceStatus.COMPLETED. Carrega o snapshot final de variáveis, businessValue, tenantId, origin, parentInstanceId/callerTaskId/callerBranchId (call activity) e getDurationMs() derivado de startedAt/endedAt.
PROCESS_INSTANCE_STARTEDProcessInstanceStartedContraparte de PROCESS_INSTANCE_FINISHED — emitido uma única vez, na primeira continuação da instância (ProcessStarter.execute()). Carrega o mesmo tipo de snapshot inicial (variáveis, businessValue, tenantId, origin), sem endedAt/status. actorId vem de ProcessStarter.byActor(...)null se não informado.
INCIDENT_CREATEDIncidentCreatedQuando FailureHandler.handleFailure abre um incidente (retries esgotados, ou ProcessErrorException não tratado por nenhum boundary). actorId é sempre IdentityContext.system().
INCIDENT_RESOLVEDIncidentResolvedQuando KikwiflowEngine.retryIncident resolve manualmente um incidente aberto. actorId vem do IdentityContext do chamador.
EXTERNAL_TASK_CLAIMEDExternalTaskClaimedQuando KikwiflowEngine.claim atribui uma external/human task a um assignee. actorId vem do IdentityContext do chamador — normalmente igual a assignee, mas podem divergir (ex.: um supervisor atribuindo em nome de outro).
EXTERNAL_TASK_UNCLAIMEDExternalTaskUnclaimedQuando KikwiflowEngine.unclaim remove o assignee de uma external/human task — carrega previousAssignee e actorId (do IdentityContext do chamador).
EXTERNAL_TASK_COMPLETEDExternalTaskCompletedQuando KikwiflowEngine.completeExternalTask completa a tarefa. Carrega assignee (quem estava atribuído no momento, null se nunca foi clamada) e actorId (quem comandou o complete) lado a lado — o motor não valida um contra o outro, o complete é soberano; esse par existe só para que quem consome o evento detecte "completado por alguém diferente do atribuído" (ver nota abaixo).
RETRY_SCHEDULEDRetryScheduledQuando FailureHandler.handleFailure reagenda uma tarefa que falhou mas ainda tem retries disponíveis (o ramo que não abre incidente). actorId é sempre IdentityContext.system().
PROCESS_VARIABLE_CHANGEDProcessVariableChangedUm evento por variável alterada via KikwiflowEngine.setVariables ou removida via KikwiflowEngine.unsetVariables (recordVariablesUnset). No caso de remoção, o payload traz removed=true e value=null; caso contrário, removed=false e o valor bruto — masking é responsabilidade de quem consome o outbox, não deste ponto de construção (ver nota sobre escopo/masking abaixo). actorId vem do IdentityContext do chamador.
TIMER_FIREDTimerFiredQuando um timer não-interruptivo dispara e se reagenda (ContinuationService, ramo NON_INTERRUPTIVE_TIMER). Timers interruptivos continuam cobertos por FLOW_NODE_FINISHED(INTERRUPTED) via recordInterruptedFlowNode. actorId é sempre IdentityContext.system().
ORPHANED_CHILD_COMPLETIONOrphanedChildCompletionQuando uma instância filha de um CALL_ACTIVITY_COORDINATOR conclui, mas a coordenadora do pai já não existe mais (cancelada por timeout antes). processInstanceId/processDefinitionId/tenantId identificam a instância filha — não o pai. Construído diretamente pela camada de persistência (InMemoryKikwiEngineRepository/MongoKikwiEngineRepository), não por CriticalEventRecorder. Sem actorId — nunca é uma ação de usuário.

:::info Quem comandou a ação: actorId e o sentinel IdentityContext.system() 9 dos 13 tipos carregam actorId. Para os que nascem de uma chamada explícita de um usuário/API (claim, unclaim, completeExternalTask, retryIncident, setVariables, ProcessStarter.byActor), actorId vem do IdentityContext.actorId() do chamador. Para os que o motor dispara sozinho, sem nenhum humano por trás (IncidentCreated, RetryScheduled, TimerFired — todos originados em FailureHandler/ContinuationService reagindo a uma falha ou um timer), actorId é sempre o sentinel IdentityContext.system().actorId() ("__KIKWIFLOW_SYSTEM__"). Os 3 tipos de evento mais antigos (FLOW_NODE_FINISHED, GATEWAY_ANSWER_RESOLVED, PROCESS_INSTANCE_FINISHED) ainda não carregam actorId — eles nascem várias camadas abaixo de onde o IdentityContext está disponível hoje (ProcessExecutionManager/ContinuationService), e alguns caminhos de execução (worker em background via TaskAcquirer.executeFromTask) não têm identidade alguma para propagar. :::

:::info EXTERNAL_TASK_COMPLETED: soberania preservada, auditoria separada assignee/actorId divergentes em EXTERNAL_TASK_COMPLETED não é um errocompleteExternalTask sempre completa a tarefa, atribuída ou não, para quem quer que a chame (esse é um requisito de produto: o complete não pode depender de estar clamada). O evento só existe para que consumidores de auditoria/observabilidade possam perguntar "isso foi completado por alguém diferente do atribuído?" sem que o motor precise saber ou se importar com a resposta. :::

:::info tenantId está em todos os 13 tipos Diferente de actorId, tenantId foi retrofitado em todos os payloads (CriticalEvent.tenantId() é obrigatório na interface, não default). A fonte varia por tipo: a maioria deriva de ProcessInstanceExecution.getTenantId()/ProcessInstance.tenantId(), já disponível em todo caminho de construção; EXTERNAL_TASK_CLAIMED/EXTERNAL_TASK_UNCLAIMED usam ExternalTask.tenantId() diretamente; INCIDENT_CREATED/INCIDENT_RESOLVED/RETRY_SCHEDULED precisaram de um parâmetro extra threaded pelo chamador (FailureHandler.handleFailure/KikwiflowEngine.retryIncident), já que Incident/ExecutableTask não carregam esse campo no domínio. :::

Todos os payloads carregam tanto processDefinitionId (o id interno, específico da versão implantada) quanto, quando disponível, processDefinitionKey (o identificador estável usado em startProcess().byKey(...), o mesmo entre versões) — para agrupar/filtrar execuções de "o mesmo processo" ao longo de reimplantações, use processDefinitionKey.

Todos os tipos só são construídos quando kikwiflow.outbox.events-enabled está ligado — kikwiflow.stats.enabled não afeta o outbox (ver nota acima sobre a inconsistência que isso causava antes de existir o CriticalEventRecorder).

:::info claim/unclaim/setVariables/unsetVariables não passam por UnitOfWork Diferente da maioria dos eventos (que viajam dentro de um UnitOfWork já existente), EXTERNAL_TASK_CLAIMED, EXTERNAL_TASK_UNCLAIMED e PROCESS_VARIABLE_CHANGED (tanto o set quanto o unset) são construídos em KikwiflowEngine e passados diretamente para KikwiEngineRepository.claim/unclaim/addVariables/unsetVariables — métodos que hoje mutam estado fora do padrão UnitOfWork/commitWork. A implementação MongoDB ainda garante atomicidade: cada um desses métodos abre sua própria ClientSession.withTransaction para gravar a mudança de estado e o evento crítico juntos. :::

:::warning Masking na escrita: nunca. Na leitura via API: sempre PROCESS_VARIABLE_CHANGED grava value sem aplicar VariableSecurityPolicyManager.applyReadPoliciesAndMasking — decisão de escopo deliberada: o motor não decide o que é sensível no momento de persistir o evento. Isso não significa que ninguém aplica masking: a API REST de histórico (GET /process-instances/{id}/events, abaixo) aplica o policy manager no momento da leitura, antes de devolver a resposta. Um relay/consumer que leia outbox_events diretamente (sem passar por essa API) ainda recebe o valor bruto — é responsabilidade desse consumidor aplicar sua própria política. :::

API REST de histórico: GET /process-instances/{id}/events

kikwi-management-rest expõe a timeline completa de uma instância — a mesma lista de findEventHistoryByProcessInstanceId, mapeada para HistoryEventSummary (kikwi-model, io.kikwiflow.model.event):

public record HistoryEventSummary(
String id,
CriticalEventType eventType,
String processInstanceId,
String processDefinitionId,
String tenantId,
String actorId,
Instant timestamp,
CriticalEvent payload
) {}

HistoryEventSummaryMapper (kikwi-management-rest) promove ao topo os campos comuns (para não obrigar o cliente a fazer instanceof só para montar uma linha de timeline) e mantém o payload completo, específico do tipo, para o detalhamento. Quando payload é ProcessVariableChanged, o mapper aplica VariableSecurityPolicyManager.applyReadPoliciesAndMasking antes de devolver — se a policy negar leitura da variável, value vem como null na resposta, nunca o bruto.

:::info Sem enforcement de tenant neste endpoint — de propósito Diferente do que seria natural supor, este endpoint não valida identityContext.tenantId() contra o tenantId da instância. É uma decisão consciente: este endpoint é para uso backoffice/suporte/TI (o "Cockpit" do Kikwiflow), não uma API de uso geral de aplicação cliente — quem acessa o monitor já tem permissão independente de tenant. Aplicações que expõem esse dado para usuários finais devem implementar seu próprio enforcement de tenant na camada de aplicação, não esperar que o Kikwiflow faça isso aqui. :::

Registro do endpoint é condicional a kikwiflow.history.enabled (default true, KikwiflowHistoryProperties, módulo kikwi-management-rest-spring-boot-autoconfigure) — desligar essa flag remove o endpoint inteiro, mesmo com kikwiflow.outbox.events-enabled=true (que controla se o dado existe, não se é servido). 404 se a instância não existir; lista vazia (não erro) se o outbox nunca foi habilitado para essa instância.

Reconstruindo por onde uma instância passou

A coleção outbox_events não é apenas uma fila de relay transiente — entradas nunca são deletadas quando confirmadas/relayadas (apenas marcadas), então ela dobra como histórico durável e consultável de execução. QueryRepository.findEventHistoryByProcessInstanceId(String processInstanceId) retorna todos os eventos de uma instância, ordenados por timestamp ascendente:

List<OutboxEventEntity> history = kikwiEngineRepository.findEventHistoryByProcessInstanceId(processInstanceId);
// ex.: ENRICH_CUSTOMER_PROFILE_ST (FLOW_NODE_FINISHED) -> CALCULATE_CUSTOMER_RISK_ST (FLOW_NODE_FINISHED)
// -> GATEWAY-CLASSIFICACAO-RISCO (GATEWAY_ANSWER_RESOLVED, resposta "FRAUDE") -> ...

Isso só retorna dados quando kikwiflow.outbox.events-enabled=true — sem persistência habilitada, a lista é sempre vazia. Note que apagar uma instância concluída (deleteProcessInstanceById) não apaga seu histórico em outbox_events — é uma decisão consciente de design, documentada no próprio código-fonte do repositório.

Retenção: TTL nativo do MongoDB

Por padrão, outbox_events retém tudo indefinidamente. Se isso não for desejável (auditoria de curto prazo, custo de armazenamento, ou o consumo é feito por uma ferramenta de relay que já move os dados para outro lugar), configure kikwiflow.outbox.ttl com uma duração ISO-8601:

kikwiflow:
outbox:
events-enabled: true
ttl: P30D # eventos expiram 30 dias após o timestamp — MongoDB TTL nativo (expireAfterSeconds)

Isso cria (ou ajusta, via collMod, se o valor mudar entre deploys) um índice TTL real sobre o campo timestamp — a expiração é feita pelo próprio MongoDB em background, sem nenhum job/scheduler do Kikwiflow envolvido. Se ttl não for declarado, nenhum índice TTL é criado e a retenção é indefinida (comportamento padrão). Diminuir ou remover o TTL depois de já criado não apaga o índice automaticamente — isso exige um passo manual (db.outbox_events.dropIndex("outbox_ttl_idx")).

Ponto de extensão para relay: o schema já está pronto

OutboxEventMapper grava cada documento em outbox_events com dois campos pensados especificamente para um futuro consumidor de relay, mesmo que nenhuma implementação venha embutida nesta versão:

{
"_id": "<uuid>",
"eventType": "FLOW_NODE_FINISHED",
"processInstanceId": "...",
"processDefinitionId": "...",
"timestamp": ISODate(...),
"relayStatus": "PENDING",
"lockedUntil": null,
"payload": { "...": "..." }
}
  • relayStatus (PENDING/LOCKED/RELAYED) e lockedUntil existem para suportar um padrão de claim atômico via findOneAndUpdate — o mesmo usado por findAndLockDueTasks (ver Workers e Tarefas Externas) — sem exigir migração de schema quando um consumidor de relay for adicionado, seja construído internamente pela sua equipe, seja uma oferta futura.
  • A interface OutboxReader (kikwi-critical-event-relay-api) já define o contrato esperado (readAndLockNextBatch/confirmBatch), mas não há implementação MongoDB fornecida nesta versão do Kikwiflow — nenhum bean é registrado automaticamente, e nenhum processo de drenagem roda em background.
  • Índices já existem para dar suporte a essa consulta (relay_status_timestamp_idx, sobre relayStatus + timestamp) mesmo sem nenhum consumidor rodando hoje.

Escolhendo o canal certo

:::tip Regra prática

  • Precisa de um dashboard de latência por nó, sem garantias fortes? Eventos leves.
  • Precisa de auditoria durável, ou consultar por onde uma instância específica passou (depuração, suporte)? Outbox, via findEventHistoryByProcessInstanceId.
  • Precisa entregar esses eventos para um sistema externo (Kafka, webhook, data warehouse)? Outbox como base de dados de origem — mas você (ou uma lib de relay dedicada) precisa implementar o consumidor; esta versão não inclui um pronto para uso.
  • Só precisa consultar o estado atual de uma instância (não seu histórico)? Nenhum dos dois — use o lado de query do CQRS (QueryRepository/ExternalTaskQueryService), descrito na Visão Geral. :::