Timers e Agendamento
O Kikwiflow modela dois padrões de timer de borda (boundary events), cada um resolvido por um evaluator diferente porque respondem a perguntas distintas: "quando este timer dispara pela primeira (e única) vez?" versus "qual é a próxima ocorrência deste timer recorrente?".
| Nó | Dispara | Efeito sobre o nó pai |
|---|---|---|
BOUNDARY_INTERRUPTIVE_TIMER (InterruptiveTimerEventDefinition) | Uma vez, em um instante calculado | Interrompe o nó pai — a tarefa original é cancelada e a execução desvia para as arestas de saída do timer |
BOUNDARY_NON_INTERRUPTIVE_TIMER (NonInterruptiveTimerEventDefinition) | Repetidamente, segundo uma SchedulePolicy | Não interrompe o nó pai — dispara em paralelo (ex.: para notificações periódicas de SLA) e se reagenda sozinho |
Timer interruptivo: TimeProviderType
Um InterruptiveTimerEventDefinition precisa resolver um único instante-alvo (dueDate). Assim como os
gateways de decisão, ele suporta três estratégias via providerType:
public enum TimeProviderType {
STATIC,
VARIABLE,
BEAN
}
{
"id": "interruptive-timer-EXTERNAL_72040E2C",
"type": "BOUNDARY_INTERRUPTIVE_TIMER",
"providerType": "STATIC",
"staticValue": "PT1M",
"attachedToRef": "wait-customer-registry",
"outgoing": [ { "targetNodeId": "invalidate-invite" } ]
}
providerType | Origem do valor | Campo relevante |
|---|---|---|
STATIC | Valor fixo declarado no JSON | staticValue |
VARIABLE | Variável de processo já calculada | providerVariable |
BEAN | Bean Spring implementando DueDateProvider | providerBean |
DueDateProvider: prazos calculados em Java
Quando o prazo depende de regra de negócio (ex.: SLA diferenciado por tipo de cliente, dia útil seguinte,
feriados), implemente DueDateProvider:
public interface DueDateProvider {
/** ISO-8601: duração ("PT1H") ou data absoluta ("2026-12-25T20:00:00Z") */
String resolve(EvaluationContext execution);
}
@Component("slaCriticoBean")
public class SlaCriticoDueDateProvider implements DueDateProvider {
@Override
public String resolve(EvaluationContext execution) {
boolean vip = Boolean.TRUE.equals(execution.getVariableValue("clienteVip").orElse(false));
return vip ? "PT30M" : "PT2H";
}
}
Parsing híbrido de tempo
O valor resolvido — seja de staticValue, de uma variável, ou do retorno de um DueDateProvider — passa por
TimerDueDateEvaluator.parseDynamicTime, que tenta interpretá-lo primeiro como data absoluta ISO-8601 e,
falhando, como duração relativa (somada a Instant.now()):
private Instant parseDynamicTime(String timeValue) {
try {
return Instant.parse(timeValue); // ex: "2026-12-31T23:59:59Z"
} catch (DateTimeParseException e1) {
try {
return Instant.now().plus(Duration.parse(timeValue)); // ex: "PT1M"
} catch (DateTimeParseException e2) {
throw new IllegalArgumentException("Formato de tempo inválido: '" + timeValue + "'...");
}
}
}
:::tip Developer experience
Isso significa que a mesma variável de processo, ou o mesmo DueDateProvider, pode retornar tanto "PT1H"
quanto "2026-08-01T09:00:00Z" sem que o autor do processo precise declarar qual formato está usando — o motor
resolve isso de forma transparente e falha com uma mensagem de erro explícita se nenhum dos dois formatos bater.
:::
Timer não-interruptivo: SchedulePolicy
Diferente do interruptivo, o timer não-interruptivo não calcula um único dueDate — ele calcula
repetidamente a próxima ocorrência, via TimerDueDateEvaluator.calculateNextSchedule(SchedulePolicy), e o
ContinuationService reagenda automaticamente um novo ciclo cada vez que o timer anterior é concluído (mais
detalhes em Execução Síncrona e Assíncrona).
public record SchedulePolicy(
ScheduleType type,
String expression,
List<String> fixedDates,
Integer maxOccurrences
) {}
public enum ScheduleType {
RATE_DURATION,
FIXED_DATES
}
type | expression / fixedDates | Comportamento |
|---|---|---|
RATE_DURATION | expression = duração ISO-8601 (ex.: "PT6H") | Próxima ocorrência = now + expression, indefinidamente (sujeito a maxOccurrences, ver abaixo). |
FIXED_DATES | fixedDates = lista de instantes ISO-8601 | Próxima ocorrência = a primeira data da lista que ainda está no futuro. Quando todas as datas já passaram, calculateNextSchedule retorna null e o motor não reagenda — o ciclo de recorrência termina naturalmente. |
maxOccurrences: limitando a quantidade de ciclos
maxOccurrences é opcional e nullable — null preserva o comportamento legado (recorrência indefinida,
bounded só pelo nó pai completar). Quando declarado, TimerDueDateEvaluator.calculateNextSchedule passa a
receber também o número (1-based) do ciclo que está prestes a ser agendado e retorna null — o mesmo sinal de
"laço esgotado" que FIXED_DATES já usa — assim que esse número ultrapassa maxOccurrences:
public Instant calculateNextSchedule(SchedulePolicy policy, int occurrenceAboutToFire) {
if (policy == null) return null;
if (policy.maxOccurrences() != null && occurrenceAboutToFire > policy.maxOccurrences()) {
return null;
}
return switch (policy.type()) { /* ... */ };
}
O número do ciclo em voo é carregado no próprio runtime, no campo ExecutableTask.occurrence (1 no primeiro
ciclo, incrementado a cada reagendamento em ContinuationService) — não em SchedulePolicy, que é estático
(parte da definição do processo, compartilhado por toda instância). Com maxOccurrences: 3, o timer dispara
nos ciclos 1, 2 e 3; o 4º nunca chega a ser criado. maxOccurrences: 0 (ou negativo) esgota já no 1º ciclo —
o boundary event nem chega a existir em runtime, mesmo efeito de schedulePolicy nulo.
{
"id": "regua-cobranca-timer",
"type": "BOUNDARY_NON_INTERRUPTIVE_TIMER",
"attachedToRef": "aguardar-pagamento",
"schedulePolicy": { "type": "RATE_DURATION", "expression": "P1D", "maxOccurrences": 3 }
}
:::info ScheduleType.CRON foi removido do vocabulário do motor
Uma versão anterior deste enum tinha um terceiro valor, CRON, implementado apenas como alias de
RATE_DURATION (nunca avaliava expressão cron de verdade — o parsing real ficou como código morto/comentado
no motor). Em vez de terminar essa implementação, o valor foi removido: dar suporte a cron de verdade exigiria
um SPI vendor-specific (ex.: integrar com o parser de cron do Spring), o que contraria o princípio de a engine
ser vendor-neutral. DeployValidator/o próprio compilador Java já impedem "type": "CRON" — o JSON Schema
(schema/kikwi-process.schema.json) também não lista mais esse valor. Use RATE_DURATION ou FIXED_DATES.
:::
{
"id": "sla-warning-timer",
"type": "BOUNDARY_NON_INTERRUPTIVE_TIMER",
"attachedToRef": "wait-customer-registry",
"schedulePolicy": { "type": "RATE_DURATION", "expression": "PT30M" }
}
Escolhendo entre interruptivo e não-interruptivo
- Use interruptivo para SLAs que, ao vencer, devem abortar a espera atual e seguir um caminho
alternativo do processo — ex.: expirar um convite não confirmado (
wait-customer-registry→invalidate-invite, vercustomer-invite-activation.jsonnosample-onboarding-process). - Use não-interruptivo quando o timer segue uma regra de recorrência (
SchedulePolicy) e apenas produz um sinal em paralelo, sem afetar o nó pai — ex.: disparar um alerta de SLA a cada 30 minutos enquanto uma tarefa externa continua aberta. - Use
TIMER_TASKquando o prazo não é um evento de borda de outro nó, mas o próprio próximo passo do caminho feliz — ex.: "aguardar 24h antes de enviar o lembrete seguinte". Reaproveita o mesmoTimeProviderType/staticValue/providerVariable/providerBeando timer interruptivo, mas como nó de fluxo principal em vez de evento de borda.