Appearance
States Reference
Every state that a run, step, or schedule can occupy, with transition conditions and terminal definitions.
Run States
There is no pending run state. Submit and Enqueue insert the run and its steps in one transaction. A run therefore starts running. The database CHECK constraint on cyclone_runs.state permits only the five values below.
| State | Value | Description |
|---|---|---|
RunRunning | "running" | The run was created and has not reached a terminal state |
RunCompleted | "completed" | All steps completed, or an OnDepFailure(CancelBranch) branch contained every discarded dependency. See Per-Branch Failure Policy |
RunFailed | "failed" | At least one step was discarded, and an OnDepFailure(CancelBranch) policy did not contain its downstream. Cyclone cancelled the other non-terminal steps. The WithRunDeadline scanner also reaches this state when it stops a still-running run. Those steps land in discarded with error_kind = "deadline" |
RunCancelled | "cancelled" | The operator called client.Cancel. Cyclone cancelled all non-terminal steps |
RunPaused | "paused" | The operator called run.Pause. The run starts no new work until run.Resume. It is not terminal. See Pause and Resume Runs |
go
switch run.State {
case cyclone.RunCompleted:
// all done
case cyclone.RunFailed:
// at least one step discarded; check ListSteps for cyclone.StepExhausted nodes
case cyclone.RunRunning:
// still in progress
case cyclone.RunCancelled:
// operator cancelled
case cyclone.RunPaused:
// operator paused; run.Resume continues it
}RunState.Terminal reports whether a run state is terminal. Use it instead of a comparison with RunRunning. A paused run is not terminal either.
Step States
| State | Value | Description |
|---|---|---|
StepPending | "pending" | The step waits for upstream dependencies to complete. A fan-out node also stays pending while its item tasks execute. An item task stays pending until the WithMaxParallel limit has a free place |
StepAvailable | "available" | Dependencies are met. A worker can claim the step |
StepRunning | "running" | A worker claimed the step. The handler is executing |
StepWaitingSignal | "waiting_signal" | The step waits until a named signal arrives or the max wait ends |
StepWaitingTimer | "waiting_timer" | The step waits until a timer fires |
StepWaitingChild | "waiting_child" | A child node waits for its child run. No worker claims it. No worker holds it |
StepPaused | "paused" | The run is paused. The step was available or waiting at the pause. Or a failed attempt, a snooze, or a reclaim made it available again during the pause. An item task that a fan-out node expands during the pause is also paused. A waiting child node is paused too. No worker claims it. No scanner moves it |
StepCompleted | "completed" | The handler returned nil. Cyclone stored the output |
StepExhausted | "discarded" | Attempts are exhausted, or the handler returned Permanent(err). Or the run's WithRunDeadline elapsed. Then error_kind = "deadline" and error = "run deadline exceeded". Or the lease expired again after WithMaxReclaims reclaims. Then error_kind = "reclaim_limit" |
StepCancelled | "cancelled" | The cause is run cancellation, fail-fast from another discarded node, or branch-cancellation. Branch-cancellation uses OnDepFailure(CancelBranch). In that case, this node's dependency was discarded and this node declared CancelBranch |
go
steps, _ := client.ListSteps(ctx, runID)
for _, s := range steps {
switch s.State {
case cyclone.StepCompleted:
// can read output
case cyclone.StepExhausted:
fmt.Printf("node %s failed: %s\n", s.Node, s.Error)
case cyclone.StepRunning:
fmt.Printf("node %s in-flight (attempt %d)\n", s.Node, s.Attempts+1)
}
}Schedule States
Schedules have an Enabled boolean field, not a separate state enum. A schedule is either enabled or disabled. There is no state between those two.
client.PauseSchedule sets enabled = false. client.ResumeSchedule sets it back. client.Unschedule is a deprecated alias for pause. It issues the same UPDATE.
sql
// From cyclone_schedules table
SELECT id, kind, enabled, consecutive_failures, next_run_at FROM cyclone_schedules;A schedule with consecutive_failures > 0 has had a scanner failure, an enqueue failure, or a workflow submit failure. A handler failure after a run is created is reported on the run and the steps, not on the schedule.
Step State Transition Table
| From | To | Trigger |
|---|---|---|
| (inserted) | pending | Submitted with upstream dependencies not yet completed |
| (inserted) | available | Submitted with no dependencies, or no deps for standalone job |
pending | available | Last upstream dependency reaches completed |
available | running | A worker claims the step with FOR UPDATE SKIP LOCKED |
running | completed | Handler returns nil |
running | available | Retryable error and attempt < maxAttempts. scheduled_at is set to now() + backoff |
running | available | The handler returned cyclone.Snooze(d). scheduled_at is set to now() + d. The attempt is not incremented |
pending | waiting_signal | Promotion moves a WaitSignal or WaitAny node straight from pending. No worker claims it. It is never running. No worker executes a wait node |
pending | waiting_timer | Promotion moves a WaitTimer node straight from pending and sets scheduled_at = now() + wait_for |
waiting_signal | completed | The signal was consumed, or the WithMaxWait bound lapsed. A lapse is not a re-queue. The node completes with TimedOut true and emits wait_lapsed. A WaitN quorum completes with the payloads it gathered. Downstream steps branch on the value |
waiting_timer | completed | Timer fired |
running | discarded | The handler returned Permanent(err), or attempt == maxAttempts. Or the run's WithRunDeadline elapsed. The deadline scanner sets non-terminal steps to discarded with error_kind = "deadline" |
running | available | The lease expired. The recovery sweep reclaims the step. The attempt is not incremented. reclaims is incremented. There is no backoff |
running | discarded | The lease expired on a step already reclaimed WithMaxReclaims times. The recovery sweep dead-letters it with error_kind = "reclaim_limit". The attempt is not incremented |
running | cancelled | Fail-fast from another discarded node, or client.Cancel. Or this node's dependency was discarded and this node declared OnDepFailure(CancelBranch). The same triggers apply to waiting_signal and waiting_timer nodes in the cancelled branch |
available | cancelled | Fail-fast, branch-cancellation under CancelBranch, or client.Cancel |
pending | cancelled | Fail-fast, branch-cancellation under CancelBranch, or client.Cancel |
waiting_signal | cancelled | Branch-cancellation under OnDepFailure(CancelBranch), or run-level cancellation (client.Cancel or fail-fast) |
waiting_timer | cancelled | The same triggers as waiting_signal. Cyclone cancels a parked timer with the rest of the branch or the run |
discarded | available | client.Retry or client.RetryAll. The attempt is preserved. max_attempts is raised to at least attempt + RetryAdditionalAttempts. reclaims is reset to 0 |
available | paused | run.Pause |
waiting_signal | paused | run.Pause. The wait keeps the payloads it gathered and the time it had left |
waiting_timer | paused | run.Pause. The timer keeps the time it had left |
running | paused | A failed attempt, a snooze, or a reclaim while the run is paused. The step goes to available and then to paused in one transaction |
paused | available | run.Resume for a step that is not a wait. Its scheduled_at does not change |
paused | waiting_signal | run.Resume for a signal wait. A WithMaxWait limit moves forward by the length of the pause |
paused | waiting_timer | run.Resume for a timer. Its scheduled_at moves forward by the length of the pause |
paused | cancelled | client.Cancel, fail-fast, or branch-cancellation under CancelBranch |
Child Nodes
A child node is never claimed and never running: the engine starts its child run and ends it. These transitions are added:
| From | To | Trigger |
|---|---|---|
| (inserted) | waiting_child | A child node with no dependencies |
pending | waiting_child | The last dependency of the child node completed. A WithMaxWait limit starts |
waiting_child | completed | The child run completed. The node stores the result of the child run |
waiting_child | discarded | The child run failed, was cancelled, or completed while its result node did not complete. Or it completed with no result node, and the node needs a result. Or the WithMaxWait limit elapsed. Or the child workflow did not build. The error kind is permanent |
waiting_child | discarded | The run deadline of the parent run elapsed. Error kind deadline |
waiting_child | cancelled | client.Cancel, fail-fast, or branch-cancellation under CancelBranch. The active child run is cancelled in the same transaction |
waiting_child | paused | run.Pause. The child run continues |
paused | waiting_child | run.Resume. The WithMaxWait limit keeps the time it had left |
discarded, cancelled | pending | client.Retry or client.RetryAll. The node reuses its child run. A failed or cancelled child run starts again |
Fan-Out Nodes and Item Tasks
A fan-out node is never claimed and never running: the engine expands and finishes it. Its item tasks are ordinary steps and use the transitions above. These transitions are added:
| From | To | Trigger |
|---|---|---|
| (inserted) | available | The engine expands a fan-out node: it inserts its item tasks, available up to the WithMaxParallel limit |
| (inserted) | paused | The engine expands a fan-out node of a paused run. Item tasks up to the WithMaxParallel limit are paused, not available |
| (inserted) | pending | Item tasks after the WithMaxParallel limit |
pending | available | An item task of the same fan-out node finished, the limit has a free place, and the run is not paused. A paused item task holds its place in the limit |
pending | completed | Fan-out node: its last item task completed, or its list is empty. The output is the item outputs in item order |
pending | discarded | Fan-out node: an item task was discarded. The node gets that item task's error kind. Or the list has more than MaxFanOutItems items. Or the collected output is larger than the OutputBytes limit. Or the upstream output is not a list. Those three give error kind permanent |
pending, available, running, paused | cancelled | Item task: its fan-out node was discarded. Every item task must succeed, so the others stop |
discarded | pending | Fan-out node: client.Retry or client.RetryAll. A retry with the fan-out node ID or an item task ID reopens the node and its discarded item tasks together |
Run State Transition Table
| From | To | Trigger |
|---|---|---|
| (inserted) | running | Submit or Enqueue called |
running | completed | All steps reach completed. Or an OnDepFailure(CancelBranch) branch contained every discarded dependency, and no non-terminal step remains |
running | failed | A step reaches discarded and OnDepFailure(CancelBranch) does not contain all of its downstream. The default FailRun policy fails the run. One FailRun dependent anywhere in the branch also fails the run. The WithRunDeadline scanner also reaches this state when it stops a still-running run |
running | cancelled | client.Cancel called |
failed | running | client.Retry / client.RetryAll reopens discarded nodes |
running | paused | run.Pause called |
paused | running | run.Resume called |
paused | completed | A step that was in progress at the pause completed the last node. Or a contained discard left no non-terminal step |
paused | failed | A step that was in progress at the pause was discarded, and OnDepFailure(CancelBranch) did not contain the discard |
paused | cancelled | client.Cancel called |
Terminal States
A step or a run in a terminal state does not change unless an operator acts on it.
Terminal step states: StepCompleted, StepExhausted, StepCancelled
Terminal run states: RunCompleted, RunFailed, RunCancelled
RunRunning and RunPaused are not terminal. RunState.Terminal gives this classification, and Wait, Signal, Result, and Output use it.
client.Cancel on a terminal run returns ErrAlreadyTerminal. client.Retry on a node that is not an exhausted (discarded) step returns ErrNoExhaustedNode. A retry that reopens a child node whose child run completed with no result for the node returns ErrChildNotRetryable. run.Pause on a terminal run returns ErrAlreadyTerminal. run.Pause on a paused run returns ErrAlreadyPaused. run.Resume on a run that is not paused returns ErrNotPaused.
Retryable States
Steps move back to a claimable state in three ways:
Automatic retry: the handler returned a retryable error and
attempt < maxAttempts. The step goes fromrunningtoavailablewithscheduled_at = now() + backoff(attempt).Lease reclaim:
lease_expiry < now(). The recovery sweep moves the step fromrunningtoavailable. The attempt counter is not incremented. This is a reclaim, not a failure. A reclaim does not apply backoff. It does incrementreclaims(StepStatus.Reclaims). WithWithMaxReclaims(n), the expiry after thenth reclaim discards the step instead.Operator retry:
client.Retrymoves the step fromdiscardedtoavailable. The attempt counter stays as it is.max_attemptsis raised to at leastattempt + RetryAdditionalAttempts, so the node has executions left.reclaimsrestarts at 0.
CLI and API State Name Mapping
The Go constants use RunState and StepState string types. Each constant's value is exactly the value stored in the database and shown by the CLI. One constant name does not match its value. StepExhausted is stored as "discarded".
go
// Go constant // DB/CLI value
cyclone.RunRunning // "running"
cyclone.RunCompleted // "completed"
cyclone.RunFailed // "failed"
cyclone.RunCancelled // "cancelled"
cyclone.RunPaused // "paused"
cyclone.StepPending // "pending"
cyclone.StepAvailable // "available"
cyclone.StepRunning // "running"
cyclone.StepWaitingSignal // "waiting_signal"
cyclone.StepWaitingTimer // "waiting_timer"
cyclone.StepWaitingChild // "waiting_child"
cyclone.StepCompleted // "completed"
cyclone.StepExhausted // "discarded"
cyclone.StepCancelled // "cancelled"
cyclone.StepPaused // "paused"SQL queries can use these string values directly:
sql
SELECT * FROM cyclone_steps WHERE state = 'discarded';
SELECT * FROM cyclone_runs WHERE state IN ('failed', 'running');Error Kinds
error_kind is a separate axis from state. It classifies the last error recorded on a step. Cyclone shows it as StepStatus.ErrorKind, RunStatus.ErrorKind, StepEvent.ErrorKind, and ExhaustedStep.Reason. There are six values. Each value has an exported Go constant:
| Go constant | Value | Written when | Terminal? |
|---|---|---|---|
cyclone.ErrorKindRetryable | "retryable" | A handler returned an ordinary error, panicked, or its timeout fired. Cyclone retries the step, or discards it once attempts are exhausted | Both |
cyclone.ErrorKindPermanent | "permanent" | The handler returned Permanent(err), or the output exceeded the payload limit. Cyclone discards the step with no further attempt. A fan-out node that fails a fan-out limit also gets this kind. A child node whose child run gave no result gets it too | Yes |
cyclone.ErrorKindDeadline | "deadline" | The run passed its WithRunDeadline cutoff. The scanner then dead-lettered every non-terminal step | Yes |
cyclone.ErrorKindReclaimLimit | "reclaim_limit" | The step's lease expired again after WithMaxReclaims reclaims. The recovery sweep dead-lettered it instead of reclaiming it once more. No attempt is charged | Yes |
cyclone.ErrorKindSnooze | "snooze" | The handler returned Snooze(d). Cyclone delays that same attempt. No attempt is charged | No |
cyclone.ErrorKindTransient | "transient" | The step's input could not be read (a database error, not bad data). The step goes back to available at the same attempt ordinal | No |
Only retryable, permanent, deadline, and reclaim_limit can appear on a discarded step. Those four values are exactly what cyclone dlq --reason and ExhaustedQuery.Reason accept:
sh
cyclone dlq list --reason deadline
cyclone dlq list --reason reclaim_limiterror_kind describes the last failure, not the current state. A later success does not clear error_kind. It also does not clear error or panic_stack. cyclone retry clears all three when it reopens a node.