---
url: https://ratchet.run/api-reference/event-system.md
description: >-
  Complete reference for job lifecycle events, batch events, chain events, and
  event observation patterns.
---

# Event System Reference

Observing and reacting to job lifecycle events. Ratchet publishes an event after each state transition commits. Use them for monitoring, alerting, and custom integrations. Events are notifications, not a durable record: see [Delivery guarantees](#delivery-guarantees).

## Listening to events

### CDI observers

Use CDI `@Observes` for type-safe event handling:

```java
@ApplicationScoped
public class JobMonitor {

    public void onJobStarted(@Observes JobStartedEvent event) {
        log.info("Job {} started on node {}", event.getJobId(), event.getNodeId());
    }

    public void onJobFailed(@Observes JobFailedEvent event) {
        log.error("Job {} failed (attempt {}): {}",
            event.getJobId(), event.getRetryAttempt(), event.getErrorMessage());
    }

    public void onJobCompleted(@Observes JobCompletedEvent event) {
        log.info("Job {} completed in {} ms",
            event.getJobId(), event.getExecutionTimeMs());
    }
}
```

### Programmatic listeners

Register listeners via [`JobSchedulerService.addEventListener()`](./job-scheduler-service#addeventlistener):

```java
scheduler.addEventListener(event -> {
    if (event instanceof JobFailedEvent failed) {
        metrics.counter("jobs.failed").increment();
    } else if (event instanceof JobCompletedEvent completed) {
        metrics.timer("jobs.duration")
            .record(completed.getExecutionTimeMs(), TimeUnit.MILLISECONDS);
    }
});
```

## Delivery guarantees

* **After commit.** A state-transition event raised inside a transaction is delivered after that transaction commits. A rollback suppresses it, and an observer cannot roll the transaction back.
* **Synchronous and local.** Observers run synchronously on the node that made the change, though not always on the same thread. Other nodes in the cluster do not see the event.
* **At most once.** An event is held in memory until it is delivered. If the node stops between the commit and the delivery, for example because its process is killed, the event is lost and is not delivered again.
* **Failed registration.** When Ratchet cannot register an after-commit callback in an active transaction, an API call that changes state before any job runs (submitting a signal-waiting job, cancel, replace, pause, resume, retry, signal) throws so the transaction rolls back with the change. A change the engine makes after a job runs (completion, dead letter, timeout, chain and batch events) still commits, and the event is lost with a warning in the log. Ratchet does not throw in a rollback-only transaction, because it will not commit. MongoDB writes do not join the caller's transaction, so with the MongoDB store the change stays saved both when Ratchet throws and when the transaction is rollback-only. The event is lost, and a retried or resumed job waits for the next poll instead of starting at once.
* **Stored state is the source of truth.** When every outcome matters, for example for billing or outgoing webhooks, check the job's stored status with the [Job Query Service](./job-query-service) instead of relying on events alone.

## Event base class

All job lifecycle events extend `AbstractJobSchedulerEvent`:

```java
public abstract class AbstractJobSchedulerEvent implements Serializable {
    public UUID getJobId()
    public String getBusinessKey()
    public JobType getJobType()
    public JobPriority getPriority()
    public String getNodeId()
    public Instant getTimestamp()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getJobId()` | `UUID` | UUIDv7 database ID of the job that triggered this event |
| `getBusinessKey()` | `String` | Human-readable business key (may be null) |
| `getJobType()` | `JobType` | Job category: SINGLE, BATCH, CHAIN, WORKFLOW, RECURRING, SYSTEM |
| `getPriority()` | `JobPriority` | Priority level of the job |
| `getNodeId()` | `String` | Identifier of the cluster node that processed this job |
| `getTimestamp()` | `Instant` | When this event was created |

## Job lifecycle events

### JobStartedEvent

Fired when a job begins execution.

```java
public class JobStartedEvent extends AbstractJobSchedulerEvent
```

No additional fields beyond the base class.

```java
public void onStarted(@Observes JobStartedEvent event) {
    log.info("[{}] Job {} ({}) started on node {}",
        event.getTimestamp(), event.getJobId(),
        event.getJobType(), event.getNodeId());
}
```

### JobCompletedEvent

Fired when a job completes successfully.

```java
public class JobCompletedEvent extends AbstractJobSchedulerEvent {
    public Long getExecutionTimeMs()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getExecutionTimeMs()` | `Long` | Execution duration in milliseconds (may be null) |

```java
public void onCompleted(@Observes JobCompletedEvent event) {
    metrics.timer("job.duration").record(
        event.getExecutionTimeMs(), TimeUnit.MILLISECONDS);
}
```

### JobFailedEvent

Fired when a job reaches terminal `FAILED` state. This includes execution failures that exhaust
retry handling and terminal timeout transitions; retryable per-attempt failures do not publish it.

```java
public class JobFailedEvent extends AbstractJobSchedulerEvent {
    public String getErrorMessage()
    public int getRetryAttempt()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getErrorMessage()` | `String` | Error message from the failure |
| `getRetryAttempt()` | `int` | Final retry attempt number |

```java
public void onFailed(@Observes JobFailedEvent event) {
    log.error("Job {} failed on attempt {}: {}",
        event.getJobId(), event.getRetryAttempt(), event.getErrorMessage());
}
```

### JobRetryingEvent

Fired when a job is being retried after a failure.

```java
public class JobRetryingEvent extends AbstractJobSchedulerEvent {
    public String getErrorMessage()
    public int getRetryAttempt()
    public Instant getScheduledTime()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getErrorMessage()` | `String` | Error from the failure that triggered the retry |
| `getRetryAttempt()` | `int` | Current retry attempt number |
| `getScheduledTime()` | `Instant` | When the retry is scheduled (after backoff) |

```java
public void onRetrying(@Observes JobRetryingEvent event) {
    log.warn("Job {} retrying (attempt {}), next run at {}",
        event.getJobId(), event.getRetryAttempt(), event.getScheduledTime());
}
```

### JobExecutionTimedOutEvent

Fired after a `RUNNING` job exceeds its configured execution timeout and Ratchet successfully
applies the resulting retry or terminal-failure transition. It is not fired for the soft warning
threshold or when another path wins the transition race.

```java
public class JobExecutionTimedOutEvent extends AbstractJobSchedulerEvent {
    public Duration getExecutionTimeout()
    public Duration getElapsedTime()
    public int getRetryAttempt()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getExecutionTimeout()` | `Duration` | Configured maximum execution duration that was exceeded |
| `getElapsedTime()` | `Duration` | Observed execution duration when the watchdog fired |
| `getRetryAttempt()` | `int` | 1-based failed-attempt count recorded by the transition |

A retrying timeout also publishes `JobRetryingEvent`. A terminal timeout also publishes
`JobFailedEvent` and follows the normal dead-letter path. Observe this event when the application
needs timeout-specific handling without parsing a generic failure message:

```java
public void onExecutionTimeout(@Observes JobExecutionTimedOutEvent event) {
    timeoutAlertExecutor.execute(() -> alertService.notifyTimeout(event));
}
```

Event delivery is synchronous, so observers must offload network calls and other blocking work.
Ratchet does not select an alert channel; that remains application configuration.

### JobCancelledEvent

Fired when a job cancellation is confirmed.

```java
public class JobCancelledEvent extends AbstractJobCancellationEvent
```

```java
public String getPreviousStatus()
public Long getExecutionTimeMs()
```

| Method | Return Type | Description |
|---|---|---|
| `getPreviousStatus()` | `String` | Status before cancellation |
| `getExecutionTimeMs()` | `Long` | Execution duration in milliseconds when known |

```java
public void onCancelled(@Observes JobCancelledEvent event) {
    log.info("Job {} canceled", event.getJobId());
}
```

### JobsBulkCancelledEvent

Fired at most once per successful bulk cancel-by-tag operation, when at least one job was cancelled. Produced by [`cancelJobsByTag()`](./job-scheduler-service) and [`cancelRecurringJobsByTag()`](./job-scheduler-service).

Bulk cancellation does not carry a single job id, business key, or priority, so this event does **not** extend `AbstractJobSchedulerEvent`.

```java
public class JobsBulkCancelledEvent implements Serializable {
    public String getTag()
    public int getCount()
    public Instant getCancelledAt()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getTag()` | `String` | Tag used to select jobs for cancellation |
| `getCount()` | `int` | Number of jobs successfully cancelled |
| `getCancelledAt()` | `Instant` | When the bulk operation completed |

```java
public void onBulkCancelled(@Observes JobsBulkCancelledEvent event) {
    log.info("Cancelled {} jobs tagged {}", event.getCount(), event.getTag());
}
```

### JobsBulkRetriedEvent

Fired at most once when `retryJobs(JobFilter, int)` resets at least one failed job. Bulk retry does
not publish a `JobRetryingEvent` for each selected job.

```java
public class JobsBulkRetriedEvent implements Serializable {
    public JobFilter getFilter()
    public int getLimit()
    public int getCount()
    public Instant getRetriedAt()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getFilter()` | `JobFilter` | Selection requested by the caller |
| `getLimit()` | `int` | Maximum jobs allowed in the recovery batch |
| `getCount()` | `int` | Number of failed jobs reset to pending |
| `getRetriedAt()` | `Instant` | When the bulk operation completed |

```java
public void onBulkRetried(@Observes JobsBulkRetriedEvent event) {
    log.info("Recovered {} failed jobs", event.getCount());
}
```

### JobPausedEvent

Fired when a job is paused.

```java
public class JobPausedEvent extends AbstractJobSchedulerEvent
```

No additional fields.

### JobResumedEvent

Fired when a paused job is resumed.

```java
public class JobResumedEvent extends AbstractJobSchedulerEvent
```

No additional fields.

### JobDlqEvent

Fired when a job enters terminal dead-letter handling. This can follow retry exhaustion, a
non-retryable failure such as poison data, or a protective runtime limit such as the retry-buffer
hard cap.

```java
public class JobDlqEvent extends AbstractJobSchedulerEvent {
    public String getErrorMessage()
    public int getRetryAttempt()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getErrorMessage()` | `String` | Error from the final failure |
| `getRetryAttempt()` | `int` | Final recorded retry count before DLQ; zero means no retry was consumed |

```java
public void onDlq(@Observes JobDlqEvent event) {
    alertService.sendDlqAlert(event.getJobId(), event.getErrorMessage());
    metrics.counter("jobs.dlq").increment();
}
```

### JobCallbackFailedEvent

Fired when a lifecycle callback (`onSuccess` / `onFailure`) throws an exception. The callback failure does not affect the job's recorded outcome.

```java
public class JobCallbackFailedEvent extends AbstractJobSchedulerEvent {
    public CallbackType getCallbackType()
    public String getErrorMessage()
    public String getCauseClassName()
    public int getCallbackAttempt()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getCallbackType()` | `CallbackType` | Which callback failed: `ON_SUCCESS` or `ON_FAILURE` |
| `getErrorMessage()` | `String` | Message from the thrown callback exception (may be null) |
| `getCauseClassName()` | `String` | Class name of the thrown callback exception |
| `getCallbackAttempt()` | `int` | 1-based callback invocation attempt |

```java
public void onCallbackFailed(@Observes JobCallbackFailedEvent event) {
    log.warn("Job {} {} callback failed: {} ({})",
        event.getJobId(), event.getCallbackType(),
        event.getErrorMessage(), event.getCauseClassName());
}
```

## Batch events

### BatchCompletingEvent

Fired when a batch is finishing (the last child job is completing).

```java
public class BatchCompletingEvent extends AbstractJobSchedulerEvent
```

```java
public int getTotalItems()
public int getCompletedItems()
public int getFailedItems()
```

| Method | Return Type | Description |
|---|---|---|
| `getTotalItems()` | `int` | Total child jobs in the batch |
| `getCompletedItems()` | `int` | Successfully completed child jobs so far |
| `getFailedItems()` | `int` | Failed child jobs so far |

### BatchCompletedEvent

Fired when a batch is fully complete (all child jobs have finished).

```java
public class BatchCompletedEvent extends AbstractJobSchedulerEvent {
    public int getTotalItems()
    public int getCompletedItems()
    public int getFailedItems()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getTotalItems()` | `int` | Total child jobs in the batch |
| `getCompletedItems()` | `int` | Successfully completed child jobs |
| `getFailedItems()` | `int` | Failed child jobs |

```java
public void onBatchCompleted(@Observes BatchCompletedEvent event) {
    double successRate = event.getTotalItems() > 0
        ? (double) event.getCompletedItems() / event.getTotalItems()
        : 1.0;

    log.info("Batch {} complete: {}/{} succeeded, {} failed",
        event.getJobId(), event.getCompletedItems(),
        event.getTotalItems(), event.getFailedItems());

    if (event.getFailedItems() > 0) {
        alertService.batchPartialFailure(event.getJobId(), event.getFailedItems());
    }
}
```

### BatchChunkFailureEvent {#batchchunkfailureevent}

Fired when a streaming-batch chunk fails to persist (the chunk's bulk insert threw) on the invocation-mode submission path (`InvocationStreamingBatchBuilder`). Lambda-mode streaming batches do not emit it.

```java
@Incubating
public class BatchChunkFailureEvent extends AbstractJobSchedulerEvent {
    public int getChunkIndex()
    public int getChunkSize()
    public String getFailureReason()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getChunkIndex()` | `int` | Zero-based index of the chunk that failed |
| `getChunkSize()` | `int` | Number of items in the failed chunk |
| `getFailureReason()` | `String` | Failure description from the underlying store exception |

This is a best-effort, pre-rollback diagnostic. Streaming submission runs in one transaction, so a chunk failure rolls back the batch parent and every previously inserted chunk with it; the event fires before that rollback and may reference a batch parent id (`getJobId()`) that never commits. Treat it as an operational signal of the failed submission, not as a pointer to durable rows.

## Chain event types {#chainevent-types}

### ChainStartedEvent

Fired when a workflow chain begins execution.

```java
public class ChainStartedEvent extends AbstractJobSchedulerEvent {
    public UUID getParentJobId()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getParentJobId()` | `UUID` | UUIDv7 ID of the parent job that owns this chain |

### ChainCompletedEvent

Fired when a workflow chain succeeds.

```java
public class ChainCompletedEvent extends AbstractJobSchedulerEvent {
    public UUID getParentJobId()
}
```

### ChainFailedEvent

Fired when a workflow chain fails.

```java
public class ChainFailedEvent extends AbstractJobSchedulerEvent {
    public UUID getParentJobId()
    public String getErrorMessage()
}
```

```java
public void onChainFailed(@Observes ChainFailedEvent event) {
    log.error("Chain for parent job {} failed: {}",
        event.getParentJobId(), event.getErrorMessage());
}
```

## Workflow events

### WorkflowBranchTriggeredEvent

Fired when a workflow condition matches and a branch is triggered.

```java
public class WorkflowBranchTriggeredEvent extends AbstractJobSchedulerEvent
```

```java
public String getBranchCondition()
public UUID getNextJobId()
```

| Method | Return Type | Description |
|---|---|---|
| `getBranchCondition()` | `String` | Description of the branch condition that matched |
| `getNextJobId()` | `UUID` | Child job ID scheduled for the branch |

## Signal events

These events accompany the [signal-waiting job](./job-scheduler-service) lifecycle (`WAITING` status and `deliverSignal()`).

### JobSignalWaitingEvent

Fired when a job has been created in `WAITING` state, blocked on a named signal.

```java
public class JobSignalWaitingEvent extends AbstractJobSchedulerEvent {
    public String getSignalKey()
    public Duration getSignalTimeout()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getSignalKey()` | `String` | Signal key the job is waiting on |
| `getSignalTimeout()` | `Duration` | Maximum wait duration, or null for no timeout |

### JobSignaledEvent

Fired after a signal is successfully delivered to a `WAITING` job, transitioning it to `PENDING`. Published only on a successful delivery; a delivery that finds the job already terminal or non-`WAITING` produces no event.

```java
public class JobSignaledEvent extends AbstractJobSchedulerEvent {
    public String getSignalKey()
    public String getSignalDeliveredBy()
    public SignalDecision.Outcome getOutcome()
    public String getRejectionReason()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getSignalKey()` | `String` | Signal key delivered to the waiting job |
| `getSignalDeliveredBy()` | `String` | Principal or component that delivered the signal |
| `getOutcome()` | `SignalDecision.Outcome` | Approval/rejection outcome |
| `getRejectionReason()` | `String` | Rejection reason, or null when approved |

### JobsBulkSignaledEvent

Fired at most once per successful key-based signal delivery when at least one `WAITING` job is unblocked. Like [`JobsBulkCancelledEvent`](#jobsbulkcancelledevent), a key-based delivery can unblock many jobs, so this event does **not** extend `AbstractJobSchedulerEvent`.

```java
public class JobsBulkSignaledEvent implements Serializable {
    public String getSignalKey()
    public int getCount()
    public String getSignalDeliveredBy()
    public SignalDecision.Outcome getOutcome()
    public String getRejectionReason()
    public Instant getSignaledAt()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getSignalKey()` | `String` | Signal key that was delivered |
| `getCount()` | `int` | Number of WAITING jobs unblocked |
| `getSignalDeliveredBy()` | `String` | Principal or component that delivered the signal |
| `getOutcome()` | `SignalDecision.Outcome` | Approval/rejection outcome |
| `getRejectionReason()` | `String` | Rejection reason, or null when approved |
| `getSignaledAt()` | `Instant` | When the signal was delivered |

### JobSignalTimedOutEvent

Fired when a `WAITING` job's signal timeout elapses and it is transitioned to `FAILED`.
The same terminal transition publishes `JobFailedEvent` immediately after this timeout-specific
event.

```java
public class JobSignalTimedOutEvent extends AbstractJobSchedulerEvent {
    public String getSignalKey()
    public Duration getSignalTimeout()
}
```

| Method | Return Type | Description |
|---|---|---|
| `getSignalKey()` | `String` | Signal key the job was waiting on |
| `getSignalTimeout()` | `Duration` | Configured maximum wait duration that elapsed (never null) |

## System metrics

Ratchet does not publish aggregate system-metrics through the event listener API. For per-job metrics (start, completion, failure, retry timings), implement the [`MetricsCollector`](./spi-interfaces#metricscollector) SPI and wire it to your monitoring backend (Micrometer, StatsD, Prometheus, etc.).

## Example: comprehensive monitoring

```java
@ApplicationScoped
public class SchedulerMonitoring {

    @Inject Logger log;
    @Inject MeterRegistry metrics;
    @Inject AlertingService alerts;

    public void onStarted(@Observes JobStartedEvent e) {
        metrics.counter("jobs.started",
            "type", e.getJobType().name(),
            "priority", e.getPriority().name()).increment();
    }

    public void onCompleted(@Observes JobCompletedEvent e) {
        metrics.counter("jobs.completed", "type", e.getJobType().name()).increment();
        if (e.getExecutionTimeMs() != null) {
            metrics.timer("jobs.duration", "type", e.getJobType().name())
                .record(e.getExecutionTimeMs(), TimeUnit.MILLISECONDS);
        }
    }

    public void onFailed(@Observes JobFailedEvent e) {
        metrics.counter("jobs.failed", "type", e.getJobType().name()).increment();
    }

    public void onExecutionTimeout(@Observes JobExecutionTimedOutEvent e) {
        metrics.counter("jobs.execution.timeout", "type", e.getJobType().name()).increment();
    }

    public void onDlq(@Observes JobDlqEvent e) {
        metrics.counter("jobs.dlq", "type", e.getJobType().name()).increment();
        alerts.notify("Job " + e.getJobId() + " moved to DLQ: " + e.getErrorMessage());
    }

    public void onBatchCompleted(@Observes BatchCompletedEvent e) {
        metrics.gauge("batch.success_rate",
            (double) e.getCompletedItems() / Math.max(e.getTotalItems(), 1));
    }

    public void onBulkCancelled(@Observes JobsBulkCancelledEvent e) {
        metrics.counter("jobs.cancelled.bulk", "tag", e.getTag())
            .increment(e.getCount());
    }

    public void onBulkRetried(@Observes JobsBulkRetriedEvent e) {
        metrics.counter("jobs.retried.bulk").increment(e.getCount());
    }
}
```

## See also

* [JobSchedulerService Event Methods](./job-scheduler-service#event-listener-management)
* [Job Lifecycle](/concepts/job-lifecycle)
* [SPI Interfaces](./spi-interfaces)
