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.

Real pipelines are rarely linear. Some steps should run only when a preceding check returns a specific value; others should be skipped entirely based on the deployment environment. Aether models these conditional paths directly in the DAG structure through three complementary mechanisms: when expressions that guard individual task nodes, continueOn policies that let pipelines survive non-fatal failures, and phaseConditions that let you remap what an executor’s exit code means. Together they give you precise control over execution flow without requiring imperative branching logic in a host program.

The when Field

The when field on a DAG task node is a boolean expression. If it evaluates to false after all of the task’s dependencies complete, the engine sets the task to PhaseSkipped without dispatching it. Downstream nodes that depend on the skipped task are evaluated normally — they check their own when condition and proceed if it passes.
{
  "dag": {
    "name": "main",
    "tasks": [
      { "name": "check", "template": "status-check" },
      {
        "name": "path-ok",
        "template": "handle-ok",
        "dependencies": ["check"],
        "when": "tasks.check.outputs.parameters.status == \"ok\""
      },
      {
        "name": "path-fail",
        "template": "handle-fail",
        "dependencies": ["check"],
        "when": "tasks.check.outputs.parameters.status == \"fail\""
      }
    ]
  }
}
When check completes with status: "ok", path-ok runs and path-fail is skipped. Both nodes depend on check, so they both evaluate their when guard at the same moment. The engine dispatches whichever guard returns true and marks the other PhaseSkipped.

Expression Context

when expressions can reference:
  • Task outputs: tasks.<node-name>.outputs.parameters.<param> — the output parameter value of a completed upstream task
  • Task phase: tasks.<node-name>.phase — the phase string (e.g. "Succeeded", "Failed")
  • Task exit code: tasks.<node-name>.code — the integer exit code from the executor
  • Workflow arguments: inputs.parameters.<name> — values passed at submit time
  • System variables: system.os, system.arch — injected by a configured vars.Source
PhaseSkipped is set exclusively by the engine when a when condition evaluates to false. It cannot be returned by executors and cannot be set via phaseConditions. This distinction is intentional — skipped means “conditionally excluded”, not “failed silently”.

OS and Platform Branching

A common use of when is selecting platform-specific execution paths using system variables injected via a vars.Source:
{
  "dag": {
    "name": "main",
    "tasks": [
      {
        "name": "detect-os",
        "template": "os-detector",
        "arguments": {
          "parameters": [
            { "name": "os", "valueFrom": { "parameter": "system.os" } }
          ]
        }
      },
      {
        "name": "run-on-mac",
        "template": "mac-task",
        "dependencies": ["detect-os"],
        "when": "tasks.detect-os.outputs.parameters.os == \"darwin\""
      },
      {
        "name": "run-on-linux",
        "template": "linux-task",
        "dependencies": ["detect-os"],
        "when": "tasks.detect-os.outputs.parameters.os == \"linux\""
      }
    ]
  }
}
detect-os reads system.os from the engine’s variable source and emits it as an output parameter. The two downstream nodes use that value in their when guards, so exactly one branch runs on any given host.

Failure Propagation with continueOn

By default, a task failure causes the engine to stop scheduling new tasks in that DAG scope and mark the DAG itself as failed. The continueOn field overrides this behavior at two granularities.

Task-Level continueOn

Placed on a task node, continueOn tells the engine that even if this node reaches a non-success phase, its downstream dependents should still be evaluated and potentially dispatched:
{
  "dag": {
    "name": "main",
    "continueOn": { "failed": true },
    "tasks": [
      { "name": "step-a", "template": "task-ok" },
      {
        "name": "step-b",
        "template": "task-fail",
        "dependencies": ["step-a"],
        "continueOn": { "failed": true }
      },
      {
        "name": "step-c",
        "template": "task-ok",
        "dependencies": ["step-b"]
      }
    ]
  }
}
step-b fails. Without continueOn, step-c would never run. With continueOn: {failed: true} on step-b, step-c is unblocked and dispatched normally.
For step-c to actually run, the DAG container itself must also tolerate the failure — either via its own continueOn or because at least one task declared continueOn. In the example above the DAG-level continueOn: {failed: true} prevents the DAG from failing while step-c is still running.

DAG-Level continueOn

The continueOn field on the DAG template body prevents the DAG container from transitioning to a failure phase when child task nodes fail:
{
  "dag": {
    "name": "main",
    "continueOn": { "failed": true, "error": true, "timeout": true }
  }
}
The three boolean fields can be set independently:
FieldPhase guarded
failedPhaseFailed — business-level failure from the executor
errorPhaseError — system-level error (crash, OOM, network)
timeoutPhaseTimeout — task deadline exceeded

Combining when and continueOn

when and continueOn work together. A task node can have both: when guards whether it runs at all, while continueOn controls how its outcome affects downstream nodes. A common pattern is a “cleanup” node that runs after either success or failure:
{
  "name": "cleanup",
  "template": "cleanup-task",
  "dependencies": ["risky-step"],
  "when": "tasks.risky-step.phase == \"Failed\" || tasks.risky-step.phase == \"Succeeded\""
}
Since risky-step has continueOn: {failed: true}, it unblocks cleanup regardless of outcome. The when expression explicitly matches both phases, so cleanup runs in either case.

Overriding Phase Mapping with phaseConditions

Executors return an integer ExecCode. The engine maps that code to a Phase by default:
ExecCodeDefault Phase
0Succeeded
1Suspended
2Failed
3Error
4Timeout
phaseConditions lets you override this mapping with custom boolean expressions. It can be placed on a named task template or on a DAG task node (call site):
{
  "phaseConditions": {
    "succeeded": "tasks.my-task.outputs.parameters.exit_code == \"0\"",
    "failed":    "tasks.my-task.outputs.parameters.exit_code == \"1\"",
    "error":     "tasks.my-task.outputs.parameters.exit_code == \"2\""
  }
}
The engine evaluates the succeeded, failed, and error expressions in order. The phase of the first expression that returns true is applied. If none match, the default code-to-phase mapping is used.
phaseConditions is evaluated after the executor returns, before any retry or hook logic runs. Use it to normalize exit codes from external processes or container images that use non-standard codes to signal business outcomes.

Skipped vs. Cancelled

Two phases look superficially similar but have distinct semantics:

PhaseSkipped

Set by the engine when a when condition evaluates to false. The task was intentionally excluded from this execution. Skipped tasks are not retried and do not trigger failure hooks.

PhaseCancelled

Set by the engine when Engine.Cancel() is called by the caller. The task was stopped mid-execution. Cancelled tasks are not retried, and the workflow-level onCancel hook fires.
Neither phase can be returned by an executor or set via phaseConditions — they are exclusively engine-managed states. This separation keeps the phase state machine coherent and prevents executors from expressing engine-level concerns.

Build docs developers (and LLMs) love