Skip to content

Job DSL reference

Automax jobs are external YAML files. They are not stored inside the Python package and are passed explicitly with --job.

The supported schema is intentionally strict and versioned:

apiVersion: automax.io/v1
kind: Job
metadata:
  name: deploy-app
vars:
  app_name: myapp
targets: group:web
strategy:
  mode: serial
failurePolicy:
  onFailure: stop_job
  onUnreachable: stop_host
tags: [deploy]
tasks:
  - id: install
    targets: group:web
    tags: [install]
    steps:
      - id: packages
        tags: [packages]
        substeps:
          - id: install_nginx
            use: os.package.install
            with:
              name: nginx
              sudo: true

Hierarchy

The execution hierarchy has three explicit levels below the job:

Job
  Task[]
    Step[]
      Substep[]

A task groups related work. A step opens one fresh SSH connection per target and runs all of its substeps through that connection. A substep calls one plugin.

Required fields

At job level:

apiVersion: automax.io/v1
kind: Job
tasks: non-empty list

At task level:

id
steps: non-empty list

At step level:

id
substeps: non-empty list

At substep level:

id
use
with: optional mapping

Canonical Automax job files should use use and with at substep level. plugin and params remain accepted as input aliases for older local job definitions, but they are not used in public examples.

Targets

targets can be declared at job, task, step or substep level. More specific scopes override broader scopes:

targets: all
tasks:
  - id: web
    targets: group:web
    steps:
      - id: lb
        targets: group:loadbalancer
        substeps:
          - id: reload_lb
            targets: server:lb01
            use: system.service.reload
            with:
              service: haproxy.service
              sudo: true

Supported selectors:

all
server:web01
web01
group:web
web

When substeps in the same step resolve to different target sets, Automax still preserves the step-scoped SSH rule: it creates one execution group per target for that step and runs only the matching substeps for that target.

Strategy

Strategy can be declared at job, task or step level:

strategy:
  mode: serial
strategy:
  mode: parallel
  max_parallel: 5
strategy:
  mode: rolling
  batch_size: 2
  pause_between_batches: 10

parallel and rolling group execution by step and substep while preserving the step-scoped SSH connection model.

Tags

Tags are inherited from job, task, step and substep. The effective tag set is used by --tags and --skip-tags:

automax run --job job.yaml --inventory inventory.yaml --tags install --skip-tags dangerous

--skip-tags wins over --tags when a substep has both.

Failure policy

Failure policy can be declared at job, task or step level:

failurePolicy:
  onFailure: stop_job
  onUnreachable: stop_host
  maxFailedHosts: 1

Supported actions:

stop_job
stop_task
stop_host
continue

maxFailedHosts stops the job after too many target hosts have failed.

Error policy

errorPolicy can be declared at job, task, step or substep level. It runs before retry and failure policy. Use it when a command returns a non-zero code but some diagnostics are expected and should not block the workflow.

errorPolicy:
  acceptedRc: [1, 2, 3]
  expected:
    - stream: combined
      pattern: "PRVF-5436.*NTP"
      reason: "Expected Oracle RAC precheck diagnostic"
  fail:
    - stream: combined
      pattern: "ORA-[0-9]+"
  unmatched: fail
  acceptedStatus: warning

A result that remains failed after errorPolicy is then handled by retry and finally by failurePolicy. See Error policy.

Conditions and flow control

A substep can include a when expression. The expression is rendered with Jinja2 and considered false only when it renders to "", 0, false, no or none:

- id: restart_only_when_enabled
  use: system.service.restart
  when: "{{ vars.restart_enabled }}"
  with:
    service: myapp.service
    sudo: true

Use scalar if with then and optional else to branch inside a step. Branches contain normal substeps and can use previous registered outputs:

- id: check_service
  use: system.service.active.check
  with:
    service: myapp.service
  register: service_state

- id: restart_or_start
  if: "{{ outputs.service_state.data.active }}"
  then:
    - id: restart
      use: system.service.restart
      with:
        service: myapp.service
        sudo: true
  else:
    - id: start
      use: system.service.start
      with:
        service: myapp.service
        sudo: true

For multi-branch logic, if can also be a list of ordered branches. Automax executes the first branch whose when expression is true; else is optional and must be the last branch when present:

- id: grade_branch
  if:
    - when: "{{ x < 50 }}"
      then:
        - id: grade_f
          use: command.local.run
          with:
            command: "echo F"
    - when: "{{ x < 60 }}"
      then:
        - id: grade_d
          use: command.local.run
          with:
            command: "echo D"
    - when: "{{ x < 70 }}"
      then:
        - id: grade_c
          use: command.local.run
          with:
            command: "echo C"
    - when: "{{ x < 80 }}"
      then:
        - id: grade_b
          use: command.local.run
          with:
            command: "echo B"
    - else:
        - id: grade_a
          use: command.local.run
          with:
            command: "echo A"

Use switch / case / default when several branches compare the same value:

- id: route_status
  switch: "{{ outputs.service.data.status }}"
  case:
    running:
      - id: ok
        echo: "service is running"
    failed:
      - id: restart
        use: system.service.restart
        with:
          service: myapp.service
          sudo: true
  default:
    - id: unknown
      fail: "Unknown service status: {{ outputs.service.data.status }}"

Use flow-level retry to repeat a nested block until all its substeps succeed or attempts are exhausted:

- id: retry_download
  retry:
    attempts: 3
    interval: 5s
    do:
      - id: fetch
        use: data.download.url
        with:
          url: "{{ artifact_url }}"
          dest: /tmp/app.tar.gz

Use for / in / do to repeat substeps for each value in a list. The loop variable is available by its declared name, as item, and with loop metadata under loop (index, index0, first, last, length):

- id: list_members
  use: identity.group.member.list
  with:
    group: appusers
  register: members

- id: ensure_homes
  for: member
  in: "{{ outputs.members.data.members }}"
  do:
    - id: create_home
      use: fs.dir.create
      with:
        path: "/home/{{ member }}"
        owner: "{{ member }}"
        mode: "0750"
        sudo: true

Use set or let to store flow values without shell glue. Values are evaluated as native Jinja expressions, then become available as {{ name }}, vars.name and outputs.name for later substeps on the same target execution path:

- id: compute_grade
  set:
    score: 65
    grade: C

- id: show_grade
  echo: "grade={{ grade }} score={{ vars.score }}"

Use echo for operator-visible messages without invoking a shell command:

- id: show_member
  echo: "processing {{ item }}"

Use noop for explicit no-op branches, placeholders or documented skip paths:

- id: nothing_to_do
  noop: "No action required on Debian targets"

Use sleep for an explicit pause without shelling out to sleep:

- id: pause_after_restart
  sleep: 5s

Use assert to stop the current flow when a native Jinja condition is false:

- id: require_service_ready
  assert: "{{ outputs.service_check.data.active }}"
  message: "Service is not active"

Use fail to stop the current flow explicitly with a rendered message:

- id: reject_missing_service
  fail: "service {{ vars.service }} is missing"
  when: "{{ not outputs.service_check.data.exists }}"

Use block to group normal substeps under one condition, tag or logical id:

- id: maintenance_changes
  when: "{{ vars.maintenance_window }}"
  block:
    - id: stop_service
      use: system.service.stop
      with:
        service: myapp.service
        sudo: true
    - id: update_package
      use: os.package.install
      with:
        name: myapp
        sudo: true

Use try / rescue / always to keep recovery and teardown in YAML instead of hiding it in a script:

- id: guarded_change
  try:
    - id: apply
      use: fs.file.write
      with:
        path: /tmp/demo
        content: demo
  rescue:
    - id: rollback
      echo: "rollback would run here"
  always:
    - id: teardown
      echo: "teardown always runs"

Inside for loops, break stops the loop and continue skips the remaining substeps for the current item. Use when to make them conditional:

- id: loop_items
  for: item
  in: "{{ outputs.items.data.values }}"
  do:
    - id: skip_disabled
      continue: true
      when: "{{ item.disabled }}"
    - id: stop_at_marker
      break: true
      when: "{{ item.name == 'STOP' }}"
    - id: process
      echo: "{{ item.name }}"

Flow nodes execute on the same selected target as the parent substep. Nested substeps can register outputs normally; repeated loop registrations overwrite the same top-level name, while every execution is still stored under outputs.nodes.<node-id>.

Output registration

Plugins return a normalized result with stdout, stderr, rc, changed, skipped, message and data. Substeps can register full results or selected values:

- id: read_version
  use: command.remote.run
  with:
    command: cat /opt/myapp/VERSION
  register:
    app_version: stdout.trim

- id: render_config
  use: fs.file.template
  with:
    src: ./templates/app.conf.j2
    dest: /etc/myapp/app.conf

Registered outputs are available as outputs.* in later substeps. Per-target