YAML Compatibility
May 19, 2026 · View on GitHub
Agent CI aims to run real GitHub Actions workflows locally. The table below shows current support against the official workflow syntax.
✅ = Supported ⚠️ = Partial ❌ = Not supported 🟡 = Ignored (no-op) 🚫 = Not planned
Workflow-Level Keys
| Key | Status | Notes |
|---|---|---|
name | ✅ | |
run-name | 🟡 | Parsed but not displayed anywhere |
on (push, pull_request) | ✅ | Branch and path filters are evaluated when using --all |
on.<event>.branches / branches-ignore | ✅ | Branch-name glob filters; --all only runs workflows whose filter matches the current branch (main for non-PR runs). |
on.<event>.paths / paths-ignore | ✅ | Path-pattern filters. With --all, a workflow is skipped if none of the changed files match paths: or all of them match paths-ignore:. |
on.<event>.tags / tags-ignore | ❌ | Tag-pattern filters for push events are not evaluated — --all treats a tagged run as an untagged push. Workflows gated only by tag patterns will run when you don't expect them to. |
on.<event>.types | 🟡 | Activity-type filters (e.g. pull_request.types: [opened, synchronize]) are parsed but not applied — --all runs the workflow regardless of event sub-type. |
on.workflow_dispatch.inputs | 🟡 | workflow_dispatch itself is not simulated, so its declared inputs are parsed but unreachable — the CLI cannot inject them. |
on.workflow_call.inputs.* (type / required / default) | ✅ | Reusable workflows resolve caller-side with: values against the callee's inputs: declarations, honoring default: when a caller omits an input. |
on.workflow_call.outputs.*.value | ✅ | Reusable-workflow callees can expose outputs.<name>.value: ${{ jobs.<id>.outputs.<name> }}, and callers read them the same way as a normal needs.*.outputs.*. |
on (schedule, workflow_dispatch) | 🟡 | Accepted without error, but Agent CI does not simulate event triggers — workflows must be run manually |
on (workflow_call) | ✅ | Local reusable workflows (uses: ./.github/workflows/...) are inlined into the caller's dependency graph. Remote refs are fetched from GitHub (requires --github-token or AGENT_CI_GITHUB_TOKEN). inputs:/outputs: passing and nested reusable workflows are supported |
on (other events) | 🟡 | Covers every GitHub-Actions webhook event Agent CI does not simulate (issues, issue_comment, label, pull_request_review, release, registry_package, status, deployment, deployment_status, fork, watch, star, discussion, milestone, project, repository_dispatch, check_run, check_suite, workflow_run, etc.). The parser accepts the event name but the run is not triggered — the workflow only ever runs when invoked manually via pnpm agent-ci run. |
env | ✅ | Workflow-level env is propagated to all steps |
defaults.run.shell | ✅ | Non-bash shells (sh, python, pwsh) are invoked via a parse-time heredoc wrap because the runner ignores inputs.shell for script steps. bash is the default. |
defaults.run.working-directory | ✅ | Passed through to the runner |
permissions | 🟡 | Accepted but not enforced — the mock GITHUB_TOKEN has full access |
concurrency | 🚫 | Concurrency groups are a GitHub-side queuing and cancellation mechanism. Agent CI has no persistent server to track group state across runs, so this cannot be implemented locally |
Job-Level Keys
| Key | Status | Notes |
|---|---|---|
jobs.<id> | ✅ | Multiple jobs in a single workflow |
jobs.<id>.name | ✅ | |
jobs.<id>.needs | ✅ | Jobs are sorted topologically into dependency waves |
jobs.<id>.if | ✅ | success(), failure(), always(), cancelled(), ==/!=, &&/||, !, needs.*.outputs.*, needs.*.result |
jobs.<id>.runs-on | ⚠️ | ubuntu-* / linux (and unspecified self-hosted / custom labels) run in a Linux container — the default image is minimal, see runner-image.md to add system tools. macos-* runs in a real macOS VM on Apple Silicon hosts with tart + sshpass installed; other hosts skip the job with a reason. windows-* is not yet supported and skips on every host. When a label declares a larger runner spec (e.g. ubuntu-latest-8-cores) than the local machine provides, the job is tagged degraded and a warning is printed before it starts — execution is not blocked, only annotated. |
jobs.<id>.environment | 🟡 | Accepted but not enforced — environment protection rules are GitHub-side only. Object form (environment.name / environment.url) is also accepted and ignored. |
jobs.<id>.permissions | 🟡 | Accepted but not enforced — job-level permission scopes have no effect because the mock GITHUB_TOKEN has full access to the local API emulation. |
jobs.<id>.env | ✅ | |
jobs.<id>.defaults.run | ✅ | shell and working-directory |
jobs.<id>.outputs | ✅ | Resolved after each job completes and accumulated across dependency waves |
jobs.<id>.timeout-minutes | ❌ | Not implemented. Agent CI's pause-on-failure model is the intended way to handle long-running steps — a hard timeout would destroy the container state that makes local debugging possible |
jobs.<id>.continue-on-error | ❌ | Not implemented. Agent CI pauses on failure so you can inspect and fix the container in place; continue-on-error would skip past failures and discard that debugging opportunity |
jobs.<id>.concurrency | 🚫 | See workflow-level concurrency above |
jobs.<id>.container | ⚠️ | Short and long form; image, env, ports, volumes honored. Within options:, only --env/-e and --label/-l are forwarded to the runner container; other Docker flags (--privileged, --user, --network, --cap-add, --workdir, etc.) are silently ignored because they tend to clash with agent-ci's own container orchestration. |
jobs.<id>.container.credentials | ❌ | Registry username / password credentials for pulling a private container image are not parsed. Private images fail to pull unless the host's docker already has credentials for the registry (e.g. docker login was run out-of-band). |
jobs.<id>.services | ✅ | Sidecar containers with image, env, ports, and options |
jobs.<id>.services.*.credentials | ❌ | Per-service registry credentials are not parsed — see jobs.<id>.container.credentials. Private service images fail to pull unless the host's docker has credentials out-of-band. |
jobs.<id>.uses (reusable workflows) | ✅ | Local refs (./) are expanded inline. Remote refs are fetched from GitHub (requires --github-token or AGENT_CI_GITHUB_TOKEN; public repos may work without auth). with: (inputs) and secrets: pass-through are supported |
jobs.<id>.secrets | 🚫 | Agent CI cannot access GitHub's secret storage. Secrets are resolved from a .env.agent-ci file at the project root or from shell environment variables as fallback. --github-token / AGENT_CI_GITHUB_TOKEN auto-populates secrets.GITHUB_TOKEN. All are injected as ${{ secrets.* }} expressions |
Strategy / Matrix
| Key | Status | Notes |
|---|---|---|
strategy.matrix | ✅ | Cartesian product of all array values is fully expanded |
strategy.matrix.include | ❌ | Not implemented. The matrix parser only processes array-valued keys; include entries (which are objects) are silently dropped. Adding support would require post-processing the Cartesian product |
strategy.matrix.exclude | ❌ | Not implemented — same reason as include. exclude entries are objects and are dropped by the array-only parser |
strategy.fail-fast | ✅ | Setting fail-fast: false allows remaining matrix jobs to continue after a failure |
strategy.max-parallel | ❌ | Not implemented. Parallelism is controlled by Agent CI's host-level concurrency limiter (based on CPU count), not per-workflow job limits |
Step-Level Keys
| Key | Status | Notes |
|---|---|---|
steps[*].id | ✅ | |
steps[*].name | ✅ | Expression expansion in names |
steps[*].if | ⚠️ | The condition is passed to the official runner binary, which evaluates it at runtime. Limitation: steps.*.outputs.cache-hit and similar outputs resolve to an empty string at parse time because prior steps have not yet run when the workflow is parsed |
steps[*].run | ✅ | Multiline shell scripts with ${{ }} expression expansion |
steps[*].uses | ✅ | Public actions are downloaded via the GitHub API |
steps[*].uses (local ./) | ✅ | Local actions (e.g. uses: ./.github/actions/my-action) are resolved from the workspace |
steps[*].uses (docker://…) | ❌ | Docker-image action references (uses: docker://alpine:3.19) are not resolved — agent-ci treats every uses: as a repository action. The step fails with an action-not-found error rather than running the image. |
steps[*].with | ✅ | Expression expansion in values |
steps[*].env | ✅ | Expression expansion in values |
steps[*].working-directory | ✅ | |
steps[*].shell | ✅ | See defaults.run.shell above — the same heredoc-wrap mechanism applies. |
steps[*].continue-on-error | ❌ | Not implemented — see jobs.<id>.continue-on-error above for the reasoning |
steps[*].timeout-minutes | ❌ | Not implemented — see jobs.<id>.timeout-minutes above for the reasoning |
Expressions (${{ }})
| Key | Status | Notes |
|---|---|---|
hashFiles(...) | ✅ | SHA-256 of matching files; supports multiple glob patterns. Descends into dotted directories (e.g. .github/) when the pattern asks for them. |
format(...) | ✅ | Template substitution with recursive expression expansion |
matrix.* | ✅ | |
secrets.* | ✅ | Resolved from .env.agent-ci at the project root, with shell environment variables as fallback. --github-token auto-populates secrets.GITHUB_TOKEN |
vars.* | ✅ | Repository / organization / environment variables. Resolved from --var NAME=VALUE CLI flags or JSON supplied via --var-file <path> / --var-file -; unresolved vars.* references fail the run at pre-flight with a list of every missing name. |
env.* | ✅ | The merged step environment (workflow-level → job-level → step-level, with step taking precedence) is available as env.* in with: inputs, run: scripts, name:, and env: value expressions. Not yet supported in if: conditions (the expression is evaluated by the runner, not the parser). Note: env.* references inside an env: block (one env var referencing another declared in the same block) are not evaluated — only the already-computed merged value is exposed. |
inputs.* | ✅ | Reusable-workflow and (nominally) workflow_dispatch input values. Inside a called workflow, inputs.<name> resolves to the caller's with: value or the declared default:. |
runner.os | ✅ | Linux for container-based jobs, macOS for jobs routed to the macOS VM |
runner.arch | ✅ | X64 for container-based jobs, ARM64 for macOS VM jobs (Apple Silicon) |
runner.name, runner.temp, runner.tool_cache, runner.debug, runner.environment | ❌ | Only runner.os and runner.arch are populated; the rest of the runner.* context resolves to an empty string. Workflows that read runner.tool_cache (common in tool-setup actions) will see an empty value — the actions themselves typically fall back to the RUNNER_TOOL_CACHE env var which the runner does set. |
github.sha | ✅ | Real commit SHA from HEAD. When the working tree is dirty, a synthetic tree SHA is computed so that expressions that hash-seed from github.sha still differ between working-tree states. |
github.repository, github.repository_owner | ✅ | Derived from the git remote origin URL (owner/repo and owner). |
github.ref, github.ref_name, github.head_ref, github.base_ref, github.actor, github.run_id, github.run_number, and other github.* fields | ⚠️ | Returned as static defaults: ref_name / head_ref default to main, base_ref is empty, actor is the repo owner, run_id / run_number are 1, api_url / server_url point at Agent CI's local API emulation. Anything not in this row or one of the ones above resolves to an empty string — that covers action_path, workflow_ref, workflow_sha, triggering_actor, retention_days, secret_source, token, graphql_url, job, run_attempt, ref_protected, ref_type, env, path, and the rest of the context documented by GitHub. |
github.event.* | ⚠️ | All event payload fields return empty strings — no real webhook event is triggered locally |
strategy.job-total, strategy.job-index | ✅ | |
steps.*.outputs.* | ⚠️ | Always resolves to an empty string at parse time — the producing step has not run yet and the runner does not re-evaluate ${{ }} inside run: script bodies. Use needs.*.outputs.* for cross-job values instead. |
steps.*.conclusion / steps.*.outcome | ❌ | Step result states are not exposed — neither conclusion nor outcome resolves against a meaningful value. Workflows that branch on prior-step status via these context variables will always evaluate as if the step didn't produce a result. |
job.* (job.status, job.container.id, job.services.*) | ❌ | The per-job runtime context is not populated. Expressions that read job.status etc. resolve to empty strings. |
* object-filter operator | ❌ | Array-of-object filtering like github.event.commits.*.author.name is not implemented — the entire expression resolves to an empty string. |
needs.*.outputs.* | ✅ | Resolved after dependency jobs complete. The needs context is built from actual job outputs and passed into subsequent job evaluation |
| Boolean/comparison operators | ✅ | ==, !=, <, >, <=, >=, &&, ||, !, parentheses. Case-insensitive string comparison. Numeric coercion follows GitHub Actions semantics: empty string and null become 0, numeric-looking strings are parsed, anything else becomes NaN and compares false. |
toJSON, fromJSON | ✅ | toJSON emits 2-space-indented JSON matching GitHub Actions' pretty-printed output, so hashFiles/diff uses of toJSON(x) are reproducible. |
contains(search, item) | ✅ | Case-insensitive substring for strings; element membership for JSON arrays (e.g. contains(fromJSON('["a","b"]'), 'b')). The * object-filter form (contains(labels.*.name, 'bug')) is not supported — see the * object-filter row. |
startsWith(searchString, searchValue) | ✅ | Case-insensitive. |
endsWith(searchString, searchValue) | ✅ | Case-insensitive. |
join | ✅ | Joins JSON arrays with a separator (default , ) |
success(), failure(), always(), cancelled() | ⚠️ | success() / failure() / always() are evaluated against the accumulated job status in dependency waves. cancelled() always returns false locally — there is no cancellation signal for Agent CI to observe, so steps gated by if: cancelled() never run. |
Type literals (true, false, null, numbers) | ✅ | Boolean, null, and numeric literals supported in expressions |
GitHub API Features (DTU Mock)
| Key | Status | Notes |
|---|---|---|
| Action downloads | ✅ | Action tarballs are resolved and downloaded from github.com |
actions/cache | ⚠️ | Cache is stored on the local filesystem via bind-mount, giving ~0 ms round-trip on cache hits. Scoping is by cache key only — GitHub's ref-based isolation (a branch cannot read another branch's cache, default branch is a fallback) is not implemented. If two workflows on different refs share a key they share the cache. |
actions/checkout | ✅ | The workspace is rsynced into the container with clean: false to preserve local changes |
actions/setup-node, actions/setup-python, etc. | ✅ | Tool setup actions run natively inside the runner container |
actions/upload-artifact / download-artifact | ✅ | Artifacts are stored on the local filesystem |
GITHUB_TOKEN | ⚠️ | Not injected automatically. Pass --github-token or set AGENT_CI_GITHUB_TOKEN to make it available as secrets.GITHUB_TOKEN. GitHub API calls from the runner are answered locally by Agent CI's API emulation layer. OIDC ID-token issuance (permissions.id-token: write → federated cloud auth) is not implemented — actions that exchange the OIDC token with AWS/GCP/Vault will fail. |
Workflow commands (::set-output::, ::error::, etc.) | ✅ | Handled by the official runner binary |