Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/BabySid/aether/llms.txt

Use this file to discover all available pages before exploring further.

Every task run and container (DAG or Loop scope) in Aether moves through a well-defined state machine. The engine is the sole writer of Phase values — executors return integer exit codes, and the engine translates those codes into phases. This separation keeps the state machine coherent: no executor can express engine-level concerns like “skip” or “cancel,” and no outside actor can set a phase that contradicts the engine’s view of the world.

The state machine

Created → Ready → Running → (terminal)

                 Suspended → (Resume) → Running → ...
All task types — leaf tasks, DAG containers, and Loop containers — share the same state machine. The meaning of each transition differs slightly by type, as described below.

All nine phases

const (
    PhaseCreated   Phase = "Created"
    PhaseReady     Phase = "Ready"
    PhaseRunning   Phase = "Running"
    PhaseSuspended Phase = "Suspended"
    PhaseSucceeded Phase = "Succeeded"
    PhaseFailed    Phase = "Failed"
    PhaseError     Phase = "Error"
    PhaseTimeout   Phase = "Timeout"
    PhaseSkipped   Phase = "Skipped"
    PhaseCancelled Phase = "Cancelled"
)

Created

The task has been persisted to the store but the engine has not yet committed to run it. Dependencies may be unsatisfied, or a concurrency limit may be in effect.

Ready

The engine has committed to execute this task. For a leaf task: Broker.Dispatch() has been called. For a container (DAG/Loop): its first child leaf has been dispatched.

Running

Execution has actually begun. For a leaf task: OnTaskStarted has been called by the broker. For a container: its first child leaf has transitioned to Running.

Suspended

The executor returned ExecCodeSuspended. The task is paused, waiting for an external Engine.Resume() call. Only leaf tasks can enter this state.

Succeeded

The task completed successfully (ExecCodeSucceeded → 0).

Failed

The task completed with a business failure (ExecCodeFailed → 2). This signals an expected, application-level negative outcome rather than a system error.

Error

A system-level error occurred (ExecCodeError → 3): executor panic, OOM, container killed, network failure, or similar.

Timeout

The task exceeded its deadline (ExecCodeTimeout → 4). In local mode the broker sets this when the task context expires; in distributed mode the engine’s timeout watcher may set it directly.

Skipped

The task’s when condition evaluated to false. Set exclusively by the engine — no executor exit code maps to this phase.

Cancelled

The workflow or task was stopped by a user-initiated Engine.Cancel() call. Set exclusively by the engine.

Terminal phases

Phase.IsTerminal() returns true for the following six phases:
func (p Phase) IsTerminal() bool {
    switch p {
    case PhaseSucceeded, PhaseFailed, PhaseError, PhaseTimeout, PhaseSkipped, PhaseCancelled:
        return true
    default:
        return false
    }
}
Once a task is in a terminal phase, the engine will not transition it again. The workflow itself reaches a terminal phase when its root scope (the entrypoint container) reaches a terminal phase.

ExecCode → Phase mapping

Executor plugins return an ExecOutputs struct whose Code field is an integer constant:
ExecCodeValueMaps to Phase
ExecCodeSucceeded0PhaseSucceeded
ExecCodeSuspended1PhaseSuspended
ExecCodeFailed2PhaseFailed
ExecCodeError3PhaseError
ExecCodeTimeout4PhaseTimeout
PhaseSkipped and PhaseCancelled never appear in ExecOutputs — they are set directly by the engine and have no corresponding ExecCode.

PhaseSkipped vs PhaseCancelled

These two terminal phases are often confused. The distinction is important:
Condition-driven, automatic. A task enters PhaseSkipped when its when expression evaluates to false at dispatch time. The engine skips the task silently; downstream tasks that depend on it are unblocked as if it had succeeded. This is intended for conditional branching within a DAG.Users cannot set PhaseSkipped via phaseConditions. Attempting to do so is a no-op — the engine reserves this transition.

PhaseSuspended and Resume

PhaseSuspended is a special non-terminal, non-running state available exclusively to leaf tasks. When an executor returns ExecCodeSuspended, the task is paused in place with partial outputs accumulated:
1

Executor suspends

The executor returns ExecCodeSuspended along with any partial outputs produced so far.
2

Task enters PhaseSuspended

The engine writes PhaseSuspended to the task run and triggers the onSuspend hook if configured.
3

External event arrives

An external system calls Engine.Resume() with a payload of new outputs.
4

Outputs are merged

The engine merges the new payload into the task’s existing outputs (last-writer-wins per parameter).
5

Task re-dispatches

The task transitions back to PhaseRunning and the executor is called again with the merged inputs.
PhaseSuspended models human-approval gates, external callback patterns, and long-running interactive tasks. Only leaf tasks can enter this state; DAG and Loop containers cannot suspend.

phaseConditions: user-defined phase overrides

The phaseConditions field on a template or DAG task lets you write expressions that override the engine’s default phase mapping. This is useful when a task uses a non-standard exit code convention or when business logic needs to reclassify an error as a failure (or vice versa):
type PhaseConditions struct {
    Succeeded string `json:"succeeded,omitempty"`
    Failed    string `json:"failed,omitempty"`
    Error     string `json:"error,omitempty"`
}
Each field is a boolean expression. The engine evaluates them in order (succeeded first, then failed, then error) and uses the phase whose condition returns true first.
{
    "phaseConditions": {
        "succeeded": "tasks.run.code == 0 || tasks.run.code == 4",
        "failed":    "tasks.run.code == 1 || tasks.run.code == 2"
    }
}
phaseConditions cannot produce PhaseSkipped or PhaseCancelled. Those phases are reserved for engine-internal use. Any expression that would attempt to map to these phases is ignored.

Container phase propagation

DAG and Loop containers derive their own phase from their children:
A DAG container transitions to PhaseSucceeded when all its tasks reach a terminal phase and none is in a failure state that would propagate. If any task reaches PhaseFailed, PhaseError, or PhaseTimeout without a matching continueOn policy, the container itself transitions to the same phase. Tasks that were not yet dispatched transition to PhaseCancelled.
A Loop container tracks the terminal phase of each iteration. The aggregate phase follows the same rules as DAG: if any iteration fails without a continueOn policy, the loop fails. The aggregate strategy controls how outputs are merged, but not phase propagation.
Because containers share the same state machine as leaf tasks, nesting is fully recursive. An inner DAG that fails propagates PhaseFailed to its parent task node, which the outer DAG processes the same way it would any other failed task. The advanceScope() scheduling loop walks upward through the scope tree until the root scope reaches a terminal phase.

Build docs developers (and LLMs) love