Events
Subscribe to job, worker, queue, workflow, and predicate lifecycle events emitted from the worker.
Subscribe to job, worker, queue, workflow, and predicate lifecycle events emitted from the worker.
Subscribe to lifecycle events with queue.on(event, handler). Events fire
from the worker process after the core decides an outcome — job, worker,
queue, workflow, and predicate lifecycle events all flow through the same
API.
queue.on("job.completed", (e) => console.log("done", e.jobId));
queue.on("job.dead", (e) => alertOps(e));
queue.on("worker.offline", (e) => alertOps(e));
function onRetry(e: EventMap["job.retrying"]) {
metrics.increment(`retry.${e.taskName}`);
}
queue.on("job.retrying", onRetry);
queue.off("job.retrying", onRetry); // same reference required to unsubscribequeue.on and queue.off are generically typed over EventMap — the event
name narrows the handler's payload type automatically, so e above is
OutcomeEvent for job.retrying and WorkerEvent for worker.offline with
no casts needed. The package barrel also exports EVENT_NAMES (all 29 wire
names, as a const tuple) and the EventMap / EventPayload types for typing
handlers declared elsewhere:
import type { EventMap, EventPayload, WorkerEvent } from "@byteveda/flexiq";| Event | Fires when | Payload |
|---|---|---|
job.enqueued | After enqueue/enqueueMany, once per job. Jobs created internally — by workflow orchestration or topic fan-out — don't emit it. | EnqueuedEvent |
job.completed | A job finished successfully. | OutcomeEvent |
job.failed | Every task-attempt failure, before the retry/dead decision is made — queue and retryCount aren't known yet. | OutcomeEvent |
job.retrying | A failed job is being retried. | OutcomeEvent |
job.dead | A job exhausted its retries and dead-lettered. | OutcomeEvent |
job.cancelled | A job was cancelled. | OutcomeEvent |
Every outcome event shares the same shape, OutcomeEvent. Only jobId and
taskName are always present — the rest is optional and filled in as it
becomes known:
interface OutcomeEvent {
jobId: string;
taskName: string;
queue?: string; // absent on job.failed (fires before it's decided)
error?: string;
retryCount?: number; // absent on job.failed
timedOut?: boolean;
durationMs?: number; // absent when nothing measured the run
}durationMs reports how long the job ran, as the runtime measured it —
absent when nothing measured the run, e.g. a job the runtime recovered
rather than a worker finishing it.
queue.on("job.completed", (e) => metrics.timing(e.taskName, e.durationMs));| Event | Fires when | Payload |
|---|---|---|
worker.started | The worker starts. | WorkerEvent |
worker.online | The worker's first successful heartbeat. | WorkerEvent |
worker.stopped | stop() is called. | WorkerEvent |
worker.offline | A dead worker is reaped during a heartbeat sweep — leader-elected, so exactly one process observes each death. | WorkerEvent |
worker.unhealthy | One of the worker's resources transitions healthy → unhealthy. | WorkerUnhealthyEvent |
| Event | Fires when | Payload |
|---|---|---|
queue.paused | pauseQueue pauses a named queue. | QueueEvent |
queue.resumed | resumeQueue resumes it. | QueueEvent |
| Event | Fires when | Payload |
|---|---|---|
workflow.submitted | A run is submitted. Sub-workflow children carry parentRunId. | WorkflowEvent |
workflow.completed | Terminal — every node succeeded. | WorkflowEvent |
workflow.completed_with_failures | Terminal — the run finished but at least one node failed. | WorkflowEvent |
workflow.failed | Terminal — the run failed outright. | WorkflowEvent |
workflow.cancelled | Terminal — the run was cancelled. See note below. | WorkflowEvent |
workflow.gate_reached | The run parks at a manual gate. | GateEvent |
workflow.compensating | Saga rollback starts. | WorkflowEvent |
workflow.compensated | Saga rollback finished successfully. | WorkflowEvent |
workflow.compensation_failed | Saga rollback itself failed. | WorkflowEvent |
workflow.node_compensating | A single node's compensation starts. | NodeCompensationEvent |
workflow.node_compensated | A single node's compensation finished. | NodeCompensationEvent |
workflow.node_compensation_failed | A single node's compensation failed. | NodeCompensationEvent |
Terminal events (workflow.completed, workflow.completed_with_failures,
workflow.failed, workflow.cancelled) fire exactly once per run, carrying
that run's final state and error.
workflow.cancelled has no dedicated run-cancellation API in this SDK
today — it only fires if the workflow finalizer itself reports a cancelled
run.
Gates run at enqueue time; each of the three non-allow decisions has its own
event. See Predicates for the decision API.
| Event | Fires when | Payload |
|---|---|---|
predicate.rejected | A gate returns Decision.reject() (or bare false), just before PredicateRejectedError throws. | PredicateEvent |
predicate.skipped | A gate returns Decision.skip() — the enqueue is dropped without throwing, and tryEnqueue returns null. | PredicateEvent |
predicate.deferred | A gate returns Decision.defer(delayMs) — the job is enqueued, delayed by delayMs. | PredicateEvent |
predicate.cancelled | Reserved — see note below. | PredicateEvent |
predicate.cancelled means a dispatch-time predicate cancelled a job that
was already enqueued — an outcome only the Python SDK produces. This SDK's
gates run at enqueue only, where a terminal skip is predicate.skipped (no
job exists yet), so nothing emits it here. It stays in EVENT_NAMES so a
webhook subscription written against another SDK still loads and validates.
interface EnqueuedEvent {
jobId: string;
taskName: string;
queue: string;
}
interface WorkerEvent {
workerId: string;
queues?: string[];
}
interface WorkerUnhealthyEvent {
workerId: string;
resource: string;
}
interface QueueEvent {
queue: string;
}
interface WorkflowEvent {
runId: string;
name?: string;
state?: string;
error?: string;
parentRunId?: string;
}
interface GateEvent {
runId: string;
node: string;
message?: string;
}
interface NodeCompensationEvent {
runId: string;
node: string;
error?: string;
}
interface PredicateEvent {
taskName: string;
/** The gate's reason, when it gave one (`predicate.rejected` / `.skipped`). */
reason?: string;
/** How long the enqueue was held back (`predicate.deferred` only). */
delayMs?: number;
}Events are fire-and-forget notifications. To wrap execution (timing, context, error transformation) use middleware; to deliver events to external HTTP endpoints use webhooks.