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.

Templates are the building blocks of every Aether workflow. A spec.templates array holds all the templates that a workflow can reference, and each template is a union type: exactly one of dag, task, or loop must be set. This design keeps templates composable — a DAG can reference a Loop template as one of its tasks, which in turn can run an inner DAG as its body, up to the configured nesting depth.

Template as a union type

// Template is a pure union container. Exactly one of DAG, Task, or Loop must be set.
type Template struct {
    DAG  *DAG  `json:"dag,omitempty"`
    Task *Task `json:"task,omitempty"`
    Loop *Loop `json:"loop,omitempty"`
}
The helper methods GetName(), GetInputs(), GetOutputs(), GetTimeout(), and GetExecutor() delegate to whichever sub-type is set, so the engine can inspect templates uniformly.
The validator rejects any template where zero or more than one of dag/task/loop is set. Every template must have exactly one.

DAG template

A DAG template ("dag") defines a directed acyclic graph of tasks. It is the primary composition mechanism — the entrypoint of a workflow is almost always a DAG.
type DAG struct {
    Name            string           `json:"name"`
    Inputs          *Inputs          `json:"inputs,omitempty"`
    Outputs         *Outputs         `json:"outputs,omitempty"`
    Entrypoints     any              `json:"entrypoints,omitempty"` // string or []string
    Tasks           []Task           `json:"tasks"`
    ContinueOn      *ContinueOn      `json:"continueOn,omitempty"`
    PhaseConditions *PhaseConditions `json:"phaseConditions,omitempty"`
    Timeout         string           `json:"timeout,omitempty"`
}

Tasks within a DAG

Each entry in dag.tasks is a Task struct operating as a call site — it references a template by name (or declares an inline executor) and passes arguments:
type Task struct {
    Name            string           `json:"name"`
    Inputs          *Inputs          `json:"inputs,omitempty"`
    Template        string           `json:"template,omitempty"`
    Executor        *Executor        `json:"executor,omitempty"`
    Dependencies    []string         `json:"dependencies,omitempty"`
    Arguments       *Arguments       `json:"arguments,omitempty"`
    When            string           `json:"when,omitempty"`
    Timeout         string           `json:"timeout,omitempty"`
    Retry           *Retry           `json:"retry,omitempty"`
    ContinueOn      *ContinueOn      `json:"continueOn,omitempty"`
    Resources       *Resources       `json:"resources,omitempty"`
    PhaseConditions *PhaseConditions `json:"phaseConditions,omitempty"`
    Hooks           *Hooks           `json:"hooks,omitempty"`
    Outputs         *Outputs         `json:"outputs,omitempty"`
}
Each DAG task must have either a template reference or an inline executor. Both at the same time, or neither, is a validation error.

How dependencies create the DAG graph

The dependencies field on each task lists the names of other tasks in the same DAG that must reach a terminal phase before this task is eligible to run. A task with no dependencies is a root node and starts immediately.
{
    "dag": {
        "name": "main",
        "tasks": [
            {"name": "fetch",     "template": "fetch-data"},
            {
                "name": "transform",
                "template": "transform-data",
                "dependencies": ["fetch"],
                "arguments": {
                    "parameters": [
                        {"name": "raw",   "valueFrom": {"parameter": "tasks.fetch.outputs.parameters.body"}},
                        {"name": "count", "valueFrom": {"parameter": "tasks.fetch.outputs.parameters.count"}}
                    ]
                }
            },
            {
                "name": "notify",
                "template": "send-notify",
                "dependencies": ["transform"],
                "arguments": {
                    "parameters": [
                        {"name": "summary", "valueFrom": {"parameter": "tasks.transform.outputs.parameters.summary"}}
                    ]
                }
            }
        ]
    }
}

DAG outputs

A DAG template can declare its own outputs that lift individual child task outputs into the parent scope:
{
    "dag": {
        "name": "main",
        "outputs": {
            "parameters": [
                {
                    "name": "summary",
                    "type": "string",
                    "valueFrom": {"parameter": "tasks.transform.outputs.parameters.summary"}
                }
            ]
        },
        "tasks": [ ... ]
    }
}

when conditions

The when field on a DAG task is a boolean expression evaluated just before the task would be dispatched. If it evaluates to false, the task transitions to PhaseSkipped and any downstream tasks that depend on it are unblocked (the dependency is treated as satisfied):
{"name": "send-alert", "template": "alert", "dependencies": ["check"],
 "when": "tasks.check.outputs.parameters.status == 'failed'"}

continueOn

The ContinueOn policy allows a DAG to proceed even when a task fails, errors, or times out:
type ContinueOn struct {
    Failed  bool `json:"failed,omitempty"`
    Error   bool `json:"error,omitempty"`
    Timeout bool `json:"timeout,omitempty"`
}

entrypoints

By default the engine starts all tasks that have no dependencies. entrypoints (a single name or a list) restricts the initial dispatch to a named subset, useful when a DAG has multiple independent roots but you want to control which ones start first.

Task template

A Task template ("task") is a leaf node — it delegates execution to a named executor plugin and cannot contain other templates.
// As a standalone template (in spec.templates):
type Task struct {
    Name            string           `json:"name"`
    Inputs          *Inputs          `json:"inputs,omitempty"`
    Outputs         *Outputs         `json:"outputs,omitempty"`
    Executor        *Executor        `json:"executor,omitempty"`
    Timeout         string           `json:"timeout,omitempty"`
    Retry           *Retry           `json:"retry,omitempty"`
    PhaseConditions *PhaseConditions `json:"phaseConditions,omitempty"`
    Hooks           *Hooks           `json:"hooks,omitempty"`
    // ... (Resources, ContinueOn also available)
}

The executor field

type Executor struct {
    Type string `json:"type"` // executor type identifier, e.g. "echo", "http", "shell"
}
executor.type is a string that the engine’s broker uses to look up the registered executor.Plugin implementation. The engine itself never executes task logic — that is the sole responsibility of the matched plugin.

Retry policy

type Retry struct {
    Limit      int    `json:"limit,omitempty"`
    Expression string `json:"expression,omitempty"`
}
limit sets the maximum retry count. When expression is omitted, only Error and Timeout phases trigger a retry. With a custom expression, the retry fires only when it evaluates to true; the expression can reference tasks.<name>.phase, tasks.<name>.code, tasks.<name>.msg, and output parameters.
Only leaf Task templates support retry. DAG and Loop containers do not.

Task template example

{
    "task": {
        "name": "fetch-data",
        "inputs": {
            "parameters": [
                {"name": "url", "type": "string"}
            ]
        },
        "executor": {"type": "http"},
        "outputs": {
            "parameters": [
                {"name": "body",  "type": "array"},
                {"name": "count", "type": "int"}
            ]
        }
    }
}

Loop template

A Loop template ("loop") iterates over a set of items, running a body template for each one. It supports three mutually exclusive iteration modes and parallel execution control.
type Loop struct {
    Name            string           `json:"name"`
    Inputs          *Inputs          `json:"inputs,omitempty"`
    Outputs         *Outputs         `json:"outputs,omitempty"`
    RepeatCondition string           `json:"repeatCondition,omitempty"`
    Items           []any            `json:"items,omitempty"`
    ItemsFrom       string           `json:"itemsFrom,omitempty"`
    Concurrency     int              `json:"concurrency,omitempty"`
    MaxIterations   int              `json:"maxIterations,omitempty"`
    Body            string           `json:"body,omitempty"` // template name
    Arguments       *Arguments       `json:"arguments,omitempty"`
    Aggregate       *Aggregate       `json:"aggregate,omitempty"`
    PhaseConditions *PhaseConditions `json:"phaseConditions,omitempty"`
    Timeout         string           `json:"timeout,omitempty"`
}
body is required and must reference an existing template by name — this is the template executed for each iteration.

Three iteration modes

items is a static JSON array. Each element is one iteration’s item value. Scalar elements are available as loop_iter.item; object elements are expanded into loop_iter.<field>:
{
    "loop": {
        "name": "run-loop",
        "items": ["jan.csv", "feb.csv", "mar.csv"],
        "concurrency": 2,
        "body": "process-file",
        "arguments": {
            "parameters": [
                {"name": "filename", "value": "{{iterator.item}}"},
                {"name": "index",    "value": "{{iterator.index}}"}
            ]
        }
    }
}

Aggregate strategies

When a loop completes, its body template outputs from all iterations are merged into the loop’s own outputs using an Aggregate policy:
type Aggregate struct {
    Strategy   AggregateStrategy `json:"strategy,omitempty"`
    Parameters []string          `json:"parameters,omitempty"`
}

const (
    AggregateStrategyLast  AggregateStrategy = "last"   // default: use last iteration's outputs
    AggregateStrategyFirst AggregateStrategy = "first"  // use first iteration's outputs
    AggregateStrategyList  AggregateStrategy = "list"   // collect all into JSON array
)
Parameters narrows which output parameter names are included in the aggregate. Omit it to include all declared outputs.
{
    "aggregate": {
        "strategy": "list",
        "parameters": ["status", "rows"]
    },
    "outputs": {
        "parameters": [
            {"name": "status", "type": "array"},
            {"name": "rows",   "type": "array"}
        ]
    }
}
With strategy: "list", each parameter’s value across all iterations is collected into a JSON array ordered by iteration index.

Template composability

Templates compose by name reference. A DAG task sets "template": "<name>" to invoke any template in the flat spec.templates list. This means:
  • A DAG can contain tasks that run other DAGs (nested DAGs)
  • A DAG can contain tasks that run Loop templates
  • A Loop’s body can reference a DAG template, which itself can contain Loops
{
    "dag": {
        "name": "outer",
        "tasks": [
            {"name": "sub", "template": "inner-dag"},
            {
                "name": "final",
                "template": "finalize",
                "dependencies": ["sub"],
                "arguments": {
                    "parameters": [
                        {"name": "result", "valueFrom": {"parameter": "tasks.sub.outputs.parameters.result"}}
                    ]
                }
            }
        ]
    }
}
The maximum static nesting depth is controlled by spec.maxNestedDepth (default 3, absolute ceiling 10). The validator walks the entire template reference tree from the entrypoint and rejects workflows that exceed this limit before they are submitted.
Outputs from a nested DAG propagate upward: the inner DAG declares its own outputs.parameters using valueFrom references to its child task outputs. The parent scope then reads those via tasks.<node-name>.outputs.parameters.<param>, exactly as it would for a leaf task. See Parameter Binding for the complete data-flow model.

Build docs developers (and LLMs) love