Actio
Macros

job-defaults

Inject shared default settings across every job at compile time.

Define default job-level settings once at the top level; Actio merges them into every job at compile time and strips both job-defaults and executors from the output — zero residual directives.

Both jobs inherit runs-on and timeout-minutes; env is deep-merged so lint ends up with both CI and DEBUG:

.actio.yml
name: CI
on: [push]
job-defaults:
  runs-on: ubuntu-latest
  timeout-minutes: 30
  env:
    CI: "true"
jobs:
  test:
    steps:
      - run: npm test
  lint:
    env:
      DEBUG: "1"
    steps:
      - run: npm run lint
generated .yml
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      CI: "true"
    steps:
      - run: npm test
  lint:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      CI: "true"
      DEBUG: "1"
    steps:
      - run: npm run lint
.actio.yml
name: CI
on: [push]
job-defaults:
  runs-on: ubuntu-latest
  timeout-minutes: 30
  env:
    CI: "true"
jobs:
  test:
    steps:
      - run: npm test
  lint:
    env:
      DEBUG: "1"
    steps:
      - run: npm run lint
generated .yml
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      CI: "true"
    steps:
      - run: npm test
  lint:
    runs-on: ubuntu-latest
    timeout-minutes: 30
    env:
      CI: "true"
      DEBUG: "1"
    steps:
      - run: npm run lint

Merge semantics

KeyBehaviour
ifAND-combineddefault && inline. Never replaced.
permissions, concurrencyReplace-on-presence — inline value wins entirely; default only applies when the key is absent.
runs-on, timeout-minutes, continue-on-error, environmentInline wins — job value takes precedence.
env, container, services, defaultsDeep-merged — job keys overlay the default.

strategy is intentionally per-job only. Put matrix and fail-fast settings on each job so Actio does not silently turn per-job identity into a workflow-wide default.

.actio.yml
name: CI
on: [push]
job-defaults:
  if: ${{ github.ref == 'refs/heads/main' }}
  env:
    REGION: us-east-1
jobs:
  deploy:
    runs-on: ubuntu-latest
    if: ${{ success() }}
    env:
      REGION: eu-west-1
    steps:
      - run: ./deploy.sh
generated .yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && success()
    env:
      REGION: eu-west-1
    steps:
      - run: ./deploy.sh
.actio.yml
name: CI
on: [push]
job-defaults:
  if: ${{ github.ref == 'refs/heads/main' }}
  env:
    REGION: us-east-1
jobs:
  deploy:
    runs-on: ubuntu-latest
    if: ${{ success() }}
    env:
      REGION: eu-west-1
    steps:
      - run: ./deploy.sh
generated .yml
jobs:
  deploy:
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main' && success()
    env:
      REGION: eu-west-1
    steps:
      - run: ./deploy.sh

Reusable-workflow call jobs

Jobs with uses: only receive the call-compatible subset of defaults: if, permissions, and concurrency. Runner keys like runs-on, env, and timeout-minutes are silently skipped (an info diagnostic is emitted listing what was dropped). executor: is not supported on call jobs.

executors

Where job-defaults applies to every job, executors are named runner presets a job opts into with executor: — for the cases where only some jobs need a hardened runner, a container, or a service block. They share the same merge model as job-defaults (inline job keys win) and compose left-to-right when listed.

See the dedicated executors page for the full guide.

On this page