Actio

Introduction

Write clean GitHub Actions YAML with a handful of compile-time macros; actio build expands them into the verbose workflow YAML you'd otherwise hand-write — zero runtime, zero lock-in.

Actio is a strict superset of GitHub Actions YAML. Write ordinary workflow files and reach for a macro only where raw YAML turns into boilerplate; actio build expands every macro at build time into the verbose YAML you'd otherwise hand-write. Nothing survives into the output — no runtime, no lock-in — and a macro-free .actio.yml is already a valid workflow.

Here's a real CI→deploy workflow. Three macros — typed params, a shared-setup _anchors block reused across both jobs, and a retry on the flaky deploy — collapse into the plumbing you'd otherwise hand-write and keep in sync:

.actio.yml
name: Deploy
on:
  push:
    branches: [main]

params:
  environment:
    type: enum
    values: [staging, production]
    default: production

_anchors:
  setup: &setup
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: 20

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - *setup
      - run: npm ci
      - run: npm test

  deploy:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - *setup
      - name: Publish to {{ params.environment }}
        uses: cloudflare/wrangler-action@v3
        retry:
          attempts: 3
          delay: 10s
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
generated .yml
name: Deploy
on:
  push:
    branches:
      - main
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test
  deploy:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - name: Publish to production (attempt 1/3)
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
        id: step_publish_to_production_attempt_1
        continue-on-error: true
      - name: Retry backoff (10s) before attempt 2/3
        run: sleep 10
        if: steps.step_publish_to_production_attempt_1.outcome == 'failure'
      - name: Publish to production (attempt 2/3)
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
        id: step_publish_to_production_attempt_2
        if: steps.step_publish_to_production_attempt_1.outcome == 'failure'
        continue-on-error: true
      - name: Retry backoff (10s) before attempt 3/3
        run: sleep 10
        if: steps.step_publish_to_production_attempt_2.outcome == 'failure'
      - name: Publish to production (attempt 3/3)
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
        id: step_publish_to_production_attempt_3
        if: steps.step_publish_to_production_attempt_2.outcome == 'failure'
.actio.yml
name: Deploy
on:
  push:
    branches: [main]

params:
  environment:
    type: enum
    values: [staging, production]
    default: production

_anchors:
  setup: &setup
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with:
        node-version: 20

jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - *setup
      - run: npm ci
      - run: npm test

  deploy:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - *setup
      - name: Publish to {{ params.environment }}
        uses: cloudflare/wrangler-action@v3
        retry:
          attempts: 3
          delay: 10s
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
generated .yml
name: Deploy
on:
  push:
    branches:
      - main
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npm test
  deploy:
    runs-on: ubuntu-latest
    needs: test
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - name: Publish to production (attempt 1/3)
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
        id: step_publish_to_production_attempt_1
        continue-on-error: true
      - name: Retry backoff (10s) before attempt 2/3
        run: sleep 10
        if: steps.step_publish_to_production_attempt_1.outcome == 'failure'
      - name: Publish to production (attempt 2/3)
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
        id: step_publish_to_production_attempt_2
        if: steps.step_publish_to_production_attempt_1.outcome == 'failure'
        continue-on-error: true
      - name: Retry backoff (10s) before attempt 3/3
        run: sleep 10
        if: steps.step_publish_to_production_attempt_2.outcome == 'failure'
      - name: Publish to production (attempt 3/3)
        uses: cloudflare/wrangler-action@v3
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
        id: step_publish_to_production_attempt_3
        if: steps.step_publish_to_production_attempt_2.outcome == 'failure'

actio build baked {{ params.environment }} into production, flattened the shared setup into both jobs, and fanned the deploy step out into gated retry attempts with backoff. ${{ secrets.CF_API_TOKEN }} passed straight through — {{ ... }} resolves at build time, ${{ ... }} is the runner's (token rules).

Try it before installing anything

Paste this into the playground and watch the generated workflow update as you type — right in your browser, no install.

Why Actio

  • Zero lock-in. The output is ordinary workflow YAML. Delete Actio and everything keeps running — no runtime, no action on the runner, nothing to install.
  • Adopt it incrementally. It's a strict superset: rename a .yml to .actio.yml and it already builds. Reach for a macro only where raw YAML hurts.
  • Catch mistakes before CI runs. actio build validates against a schema and resolves typed params locally, so a typo fails your build instead of burning a red run.
  • Stop copy-pasting steps. Reuse a step list with _anchors/templates — no separate composite-action or reusable-workflow file to maintain.
  • No third-party runtime deps. Retry, try/catch, and dynamic matrices compile to inline Actions YAML — nothing extra to trust at runtime.
  • One source, many workflows. Typed params and templates keep env names, versions, and defaults consistent across every job and repo.
  • The output is honest. Generated YAML is what you'd have written by hand, and source maps trace each line back to the macro that produced it.

Each macro erases a specific pain you'd otherwise hand-write — the syntax reference has the full set:

Pain in raw YAMLMacro
A runtime matrix needs a hand-built setup job that prints JSON to $GITHUB_OUTPUT, consumed downstream via fromJSON(), with escaping and empty-guard gotchasdynamic-matrix
Reusing a block of steps forces a separate composite action or reusable workflow file_anchors / templates
Retrying a flaky step means hand-rolled bash loops or a third-party actionretry
try/catch means smearing if: failure() and continue-on-error across stepsfallback
Keeping typed compile-time constants and defaults consistent across a workflowparams

Prior art (github-actions-workflow-ts, github-actions-wac, projen) serializes a typed object graph 1:1 into YAML. Actio is a macro/transform compiler: the source stays YAML and macros rewrite it at build time, so you keep plain workflow files and just stop hand-writing the boilerplate. See the syntax reference for every keyword and architecture for the two-phase compile model.

Next steps

On this page