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:
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 }}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'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 }}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
.ymlto.actio.ymland it already builds. Reach for a macro only where raw YAML hurts. - Catch mistakes before CI runs.
actio buildvalidates against a schema and resolves typedparamslocally, 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
paramsand 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 YAML | Macro |
|---|---|
A runtime matrix needs a hand-built setup job that prints JSON to $GITHUB_OUTPUT, consumed downstream via fromJSON(), with escaping and empty-guard gotchas | dynamic-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 action | retry |
try/catch means smearing if: failure() and continue-on-error across steps | fallback |
| Keeping typed compile-time constants and defaults consistent across a workflow | params |
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.