Skip to main content

Statuses

Every row the engine writes carries a status from a backed enum, so a host can filter on it, match on it, and read it straight out of the database.

FlowStatus — the run

CaseMeaning
PendingCreated, not yet driven.
RunningA replay pass is in progress.
WaitingSuspended on something durable: a queued action, an open signal wait, or a child.
CancellingRolling back. Not terminal — its compensations are still running.
Completedhandle() returned.
FailedA business exception ended it, after whatever rollback its policy asked for.
CancelledStopped because it was no longer wanted: cancel(), compensate(), or a child closed under ChildClosePolicy::Cancel.
ExpiredIts own expires_at passed and the deadline was enforced.

Completed, Failed, Cancelled and Expired are terminal: a run never leaves them, and it refuses signals and cancellation once there.

Cancelling is not terminal, but no step starts under it. A rollback plans the stack it will undo once, so a step that began afterwards would finish outside that plan — its compensation in no stack, never run, under a run reporting a complete unwind. A job already queued for such a step is refused when it tries to claim the row, a signal-gated retry will not start another cycle, and the doctor sends no replacement. Settling what already started is a different question and carries on as usual: a step past its own deadline is still expired, and the rollback's own compensations still run.

The plan it unwinds is made once the run is here, which is what makes "afterwards" mean afterwards: a step whose owed queue attempt completed while an earlier plan was being drawn is in it. That earlier plan is drawn before anything is written, so a run whose rollback cannot be planned at all is left where it was found rather than stranded mid-rollback. It is also what fills the gaps in the later one, and the journal says so: an ordinal the second plan came back without is restored from the first, and a second replay that throws leaves the first standing on its own.

A step is not the only thing that begins, and the rest is reached from inside a pass rather than by a job of its own: a child workflow and a side effect both start work at an ordinal the run has not reached before. A pass still replaying when the rollback committed carries the run as it was before it, so both seams read the status from the writing connection and end the pass instead of starting anything. That is a check taken immediately before the work, not a lock — a run that enters Cancelling in the moment between them still starts that one.

Neither is the run itself driven. A pass begins only for a run in one of the three statuses mayStartWork() names, decided on the run as the writing connection holds it, and its deadline is weighed after that. A job that arrives for a run outside them — a redelivery, a resume queued while the run was still Waiting, a manual saga-flow:kick — ends without entering the pass, and the executor hands its caller the run as the writer holds it rather than throwing. So a run is expired once: the rollback the sweep planned is the only one, and each compensation on it runs once.

It takes no signal either, for the same reason read from the other end: the resume a delivery queues cannot drive a rolling-back run, and terminal settlement closes wait-markers, not received rows, so the delivery would sit unread forever. Delivery is held to the three statuses signalable() names, decided on the run as the writing connection holds it. That is a check at delivery time, not a lock: a run that enters Cancelling immediately afterwards still takes the signal, which then stays a floating Received row like any other nobody consumed.

ActionStatus — one step

CaseMeaning
PendingScheduled. Its job has not settled it yet.
RunningClaimed by a worker that is executing it.
CompletedReturned a result, which replay hands back on every later pass.
FailedThrew. Replay surfaces it as a business error.
AwaitingRetryFailed and parked by retryOnSignal(), waiting for its signal.
OptionalFailedAn optional step gave up; the seam returns its fallback.
ExpiredThe monitor enforced this step's own expires_at.
CancelledThe run it belongs to finished before the step reached an outcome of its own.

The difference between Expired and Cancelled is whose deadline ran out. Expired always carries the step's own exception and an action.expired event; Cancelled carries neither, because nothing happened to the step — the run under it ended.

SignalStatus — one signal row

CaseMeaning
WaitingAn open wait-marker: the run is parked at this sequence until the signal arrives.
ReceivedDelivered, not yet taken by a replay pass.
ConsumedReplay read its payload.
TimedOutThe monitor enforced the wait's timeout_at; replay surfaces that as a business error.
CancelledThe run finished while the wait was still open.

A delivered signal that no awaitSignal() ever matched keeps its Received status for good — it records that a signal arrived and nobody used it. Such a row can only come from a run that was still open to one: a delivery to a finished or rolling-back run writes nothing at all.

What a finished run leaves behind

Reaching a terminal state settles the work the run was still holding, so nothing under it keeps reading as live:

  • steps in Pending, Running or AwaitingRetry become Cancelled;
  • open wait-markers (Waiting) become Cancelled.

Steps that already reached an outcome are untouched — a cancelled run still shows which of its steps ran, and what each of them did.

One thing is deliberately left unsettled: a compensation still in Pending or Running when the rollback stopped. That is not leftover state but an open operational item — a run that did not fully unwind — and it is recorded on the run's own exception as CompensationUnfinishedException. See sagas & compensations.

A step that is Cancelled is inert: a late job cannot claim it, and no monitor or repair sweep selects it. A step under a run that is Cancelling is inert in the narrower sense above — no job claims it and no repair sends another — while the deadline sweeps still settle it.

CompensationStatus and ChildStatus

CompensationStatus (Pending, Running, Completed, Failed) tracks one compensation of a rollback. ChildStatus (Pending, Running, Completed, Failed, Cancelled) tracks the link between a parent and a child workflow — the child's own run row carries a FlowStatus of its own.