Events
Subscribe to job, worker, queue, workflow, and predicate lifecycle events — worker-scoped or across the whole client.
Subscribe to job, worker, queue, workflow, and predicate lifecycle events — worker-scoped or across the whole client.
Two surfaces subscribe to the same lifecycle events. Worker-scoped
Worker.Builder.on(...) sees only that worker's own job outcomes;
client-scoped flexiq.onEvent(...) sees everything — queue-level events plus
every event raised by workers built from the same FlexiQ.
try (FlexiQ flexiq = FlexiQ.builder().sqlite("flexiq.db").open()) {
flexiq.onEvent(EventName.WORKER_OFFLINE, event -> alertOps(event));
flexiq.onEvent(EventName.WORKFLOW_FAILED, event -> {
if (event instanceof WorkflowEvent workflow) {
alertOps(workflow.runId(), workflow.error());
}
});
try (Worker worker = flexiq.worker()
.handle(add, payload -> payload.a() + payload.b())
.on(EventName.SUCCESS, event -> log.info("done " + event.jobId))
.on(EventName.JOB_FAILED, event -> log.warn("attempt failed: " + event.taskName))
.on(EventName.DEAD, event -> alertOps(event))
.start()) {
worker.awaitShutdown();
}
}Worker.Builder.on(EventName, Consumer<OutcomeEvent>) is unchanged — still
job-outcome events only (SUCCESS, RETRY, DEAD, CANCELLED, and now
JOB_FAILED), delivered as a typed OutcomeEvent with no cast.
flexiq.onEvent(EventName, Consumer<FlexiQEvent>) is new: since one
subscription surface fans in every event type, its callback receives the
sealed FlexiQEvent interface and you narrow it yourself:
flexiq.onEvent(EventName.WORKFLOW_GATE_REACHED, event -> {
if (event instanceof GateEvent gate) {
log.info("run {} parked at {}", gate.runId(), gate.nodeName());
}
});Both surfaces are add-only. Register worker listeners on the builder before
start(); register client listeners right after open() returns (the client
must exist first) and before any event-producing work begins, then let them go
away with the worker or the client. A listener that throws is caught and
logged; it never blocks other listeners.
FlexiQEventFlexiQEvent is a sealed interface with one method, EventName name().
OutcomeEvent implements it directly; every other event is a record:
EnqueuedEvent(jobId, taskName, queue), WorkerEvent(name, queues),
QueueEvent(name, queue), WorkflowEvent(name, runId, workflowName, error),
GateEvent(runId, nodeName), NodeCompensationEvent(name, runId, nodeName, error), PredicateEvent(taskName, reason).
EventName.wireName() returns the dotted wire string used by webhooks and
the dashboard, e.g. EventName.SUCCESS.wireName() is "job.completed".
| Event | Fires when | Payload |
|---|---|---|
JOB_ENQUEUED (job.enqueued) | After enqueue/enqueueMany, once per job. | EnqueuedEvent |
SUCCESS (job.completed) | A job finished successfully. | OutcomeEvent |
JOB_FAILED (job.failed) | Each task-attempt failure, before the retry decision — retryCount is -1 and there's no duration. | OutcomeEvent |
RETRY (job.retrying) | A failed job is being retried. | OutcomeEvent |
DEAD (job.dead) | A job exhausted its retries and dead-lettered. | OutcomeEvent |
CANCELLED (job.cancelled) | A job was cancelled. | OutcomeEvent |
SUCCESS, RETRY, DEAD, and CANCELLED are the original constants and
keep working unchanged; JOB_ENQUEUED and JOB_FAILED are new siblings, not
replacements.
Every OutcomeEvent carries jobId, taskName, and outcome details: error
(null on success/cancel), retryCount (-1 when not applicable), and
timedOut (which separates a timeout from other failures). durationMs()
reports how long the job ran, as the runtime measured it:
.on(EventName.SUCCESS, event -> metrics.record(event.taskName, event.durationMs()))It returns null when nothing measured the run — JOB_FAILED firing before a
duration exists, or a job the runtime recovered rather than a worker
finishing it.
| Event | Fires when | Payload |
|---|---|---|
WORKER_STARTED (worker.started) | Just before the native worker starts. | WorkerEvent |
WORKER_ONLINE (worker.online) | Just after start() returns. | WorkerEvent |
WORKER_STOPPED (worker.stopped) | stop() / close(), once. | WorkerEvent |
WORKER_OFFLINE (worker.offline) | After close() completes, for this worker. | WorkerEvent |
WORKER_UNHEALTHY (worker.unhealthy) | Reserved — see note below. | WorkerEvent |
| Event | Fires when | Payload |
|---|---|---|
QUEUE_PAUSED (queue.paused) | pause(). | QueueEvent |
QUEUE_RESUMED (queue.resumed) | resume(). | QueueEvent |
| Event | Fires when | Payload |
|---|---|---|
WORKFLOW_SUBMITTED (workflow.submitted) | submitWorkflow (and sub-workflow submission). | WorkflowEvent |
WORKFLOW_COMPLETED (workflow.completed) | Terminal — every node succeeded. | WorkflowEvent |
WORKFLOW_COMPLETED_WITH_FAILURES (workflow.completed_with_failures) | Reserved — see note below. | WorkflowEvent |
WORKFLOW_FAILED (workflow.failed) | Terminal — the run failed. | WorkflowEvent |
WORKFLOW_CANCELLED (workflow.cancelled) | cancelWorkflow. | WorkflowEvent |
WORKFLOW_GATE_REACHED (workflow.gate_reached) | The run parks at a manual gate. | GateEvent |
WORKFLOW_COMPENSATING (workflow.compensating) | Saga rollback starts. | WorkflowEvent |
WORKFLOW_COMPENSATED (workflow.compensated) | Saga rollback finished successfully. | WorkflowEvent |
WORKFLOW_COMPENSATION_FAILED (workflow.compensation_failed) | Saga rollback itself failed. | WorkflowEvent |
WORKFLOW_NODE_COMPENSATING (workflow.node_compensating) | A single node's compensation starts. | NodeCompensationEvent |
WORKFLOW_NODE_COMPENSATED (workflow.node_compensated) | A single node's compensation finished. | NodeCompensationEvent |
WORKFLOW_NODE_COMPENSATION_FAILED (workflow.node_compensation_failed) | A single node's compensation failed. | NodeCompensationEvent |
Terminal, gate, and saga events come from the workflow tracker attached to
the worker (Worker.Builder.trackWorkflows()).
| Event | Fires when | Payload |
|---|---|---|
PREDICATE_REJECTED (predicate.rejected) | A registered predicate rejects an enqueue. | PredicateEvent |
PREDICATE_DEFERRED (predicate.deferred) | Reserved — see note below. | PredicateEvent |
PREDICATE_SKIPPED (predicate.skipped) | Reserved — see note below. | PredicateEvent |
PREDICATE_CANCELLED (predicate.cancelled) | Reserved — see note below. | PredicateEvent |
This SDK's predicates are pass/reject only, so PREDICATE_REJECTED is the
only one it emits. The other three are constants for the rest of the
cross-SDK contract — work held back for a delay (at enqueue or at dispatch,
where the payload also carries the job's id), an enqueue dropped without
raising, and a dispatch-time cancellation of an already-enqueued job. They exist here
so a webhook subscription written against another SDK still resolves through
EventName.fromWire and matches nothing rather than silently dropping.
WORKER_UNHEALTHY and WORKFLOW_COMPLETED_WITH_FAILURES are declared but
not yet emitted — the resource health-transition hook and the
partial-failure finalizer land in a follow-up. Today the workflow tracker
reports a run as either WORKFLOW_COMPLETED or WORKFLOW_FAILED.
Events are fire-and-forget notifications. To wrap execution (timing, context, error transformation) use middleware; to deliver events to external HTTP endpoints use webhooks.