Reference

Compatibility matrix

Every feature of GitHub Actions workflow syntax, what Apex Actions does with it, and the conformance fixture that proves it.

Compatibility is not a promise; it is a test suite. Every row below comes from the conformance corpus, and a row may only name a fixture that exists — so this table is the scope of what is covered rather than a summary of it. If a behaviour is not listed, it is not claimed.

Of 44 features, 35 are supported, 7 are partial and 2 are not supported. The Proved by column names the fixtures. A row with no fixture is claimed and not yet demonstrated, and says so — "supported" and "proved supported" are different sentences.

You can check any of this yourself against your own repositories, before moving anything:

apex migrate scan your-org        # or: apex migrate scan --path .

The apex CLI is on npm as @apex-actions/cli, and needs Node 24 or later.

Triggers

FeatureStatusProved byNotes
on: push — branches, branches-ignore, tags, tags-ignoreSupportedbranch-glob-release, trigger-refused-branch-filterA trigger that refuses says so in a form a consumer can act on: the plan carries not_triggered.{code, subject, filter} — the first filter to refuse, in GitHub's evaluation order, and the list it was tested against as written. GitHub reports nothing at all here.
on: push / on: pull_request — paths, paths-ignoreSupportedpaths-filter-skip, paths-zero-depth-globA push payload's commit lists supply the changed files when the caller does not; a pull request's files come from the API. With no file information the workflow runs, which is GitHub's behaviour.
on: pull_request / pull_request_target — activity types, base-branch filtersSupportedpaths-filter-skip, pull-request-targetDefaults to opened, synchronize, reopened when types is absent. pull_request_target runs the default branch's copy of the workflow at its last commit, never the pull request's head, with the repository's secrets and no approval for a fork, as on GitHub; every pull request activity type can trigger it, closed and labeled included.
on: workflow_dispatch — typed inputs (string, boolean, number, choice, environment)Supportedworkflow-dispatch-inputs—
on: workflow_call — inputs, secrets, outputsSupportedreusable-local, reusable-remote—
on: workflow_run — workflows, activity types, branch filtersSupportedworkflow-run-downstreamEmitted by the control plane when a run is created and when it finishes. Always planned from the default branch, as on GitHub.
on: repository_dispatch — types, client_payloadSupportedrepository-dispatchPOST /api/v1/repos/:owner/:repo/dispatches. Always planned from the default branch.
on: schedule — five-field cron, lists, ranges, steps, month and day namesSupportedschedule-nightlyUTC, as on GitHub. The non-standard @daily, ?, L, W and # extensions are rejected, because GitHub rejects them.
Other webhook events (issues, release, create, …)Partial— no fixture yetThe engine matches any event name and its activity types generically, so planning is correct. The control plane only ingests push, pull_request, workflow_dispatch, workflow_run and repository_dispatch today; the App does not subscribe to the rest, so they never arrive.

Workflow-level keys

FeatureStatusProved byNotes
name, run-nameSupportedneeds-dagAn unnamed workflow is named after its file, as on GitHub.
env, defaults.run.shell, defaults.run.working-directorySupportednode-ci-matrix—
concurrency — group, cancel-in-progressSupportedconcurrency-groups—
permissions — read-all, write-all, per-scope mappingPartialpermissions-oidcResolved as on GitHub: a mapping is total, and a scope it does not name is none. It narrows GITHUB_TOKEN, which is minted per job for its own repository, and gates the OIDC token. The token can carry only what the Apex App holds: issues, statuses and deployments are not among them.

Jobs

FeatureStatusProved byNotes
needs — DAG, cycle detection, skipped-dependency propagationSupportedneeds-dag—
if at job level, including status functions with needsSupportedneeds-dag, workflow-run-downstreamDecided at plan time where it depends only on github, inputs and vars; deferred to the scheduler otherwise.
runs-on — label, label list, group + labelsSupportednode-ci-matrix—
strategy.matrix — axes, include, exclude, include-only matrices, whole-matrix expressions, and expressions in individual valuesSupportednode-ci-matrix, matrix-include-only, matrix-named-job, matrix-value-expressionA matrix with only include is exactly its entries — one job each. It planned as a single job carrying the last entry until 2026-09-05, which is what matrix-include-only pins. A single value may also be an expression, and GitHub evaluates it before the leg is named or scheduled; until 2026-09-08 only a whole-axis expression was evaluated, so a list of expressions passed through as literal text and the job carried ${{ … }} as its runs-on — unschedulable, and silent. matrix-value-expression pins it.
strategy.fail-fast, strategy.max-parallel, strategy.job-index, strategy.job-totalSupportednode-ci-matrix—
outputs — job outputs and needs.<job>.outputsSupportedneeds-dag, reusable-remote—
environment — name and urlSupportedrepository-dispatch, permissions-oidcProtection rules — reviewers, wait timers, branch policies — are configuration outside the workflow file and are enforced by the control plane.
concurrency at job levelSupportedconcurrency-groups—
permissions at job levelPartialpermissions-oidcReplaces the workflow's set outright; the two are never merged. Enforced as the workflow-level row says.
container, servicesSupportednode-ci-matrixPassed to the runner verbatim; the executor owns the semantics. A service answers by its id and on the job's own localhost:<host port> — GitHub's container-job and VM-job forms both.
timeout-minutes, continue-on-errorSupportednode-ci-matrix—
uses — reusable workflows, local (./…) and remote (owner/repo/…@ref)Supportedreusable-local, reusable-nesting-depth, reusable-remoteNesting runs to the caller plus ten reusable levels and refuses the next, as github.com did when measured on 2026-09-26.
with, secrets, secrets: inherit on a reusable-workflow jobSupportedreusable-local, reusable-remote—

Steps

FeatureStatusProved byNotes
run, shell, working-directorySupportednode-ci-matrix—
uses — remote actions (owner/repo@ref), local actions (./path), Docker actionsSupportednode-ci-matrixFetched by the runner over HTTPS as source tarballs; JavaScript, composite and Docker actions all execute.
with, env, id, name, if, continue-on-error, timeout-minutesSupportednode-ci-matrix—
Workflow commands — set-output, add-mask, add-path, set-env, group, error, warning, notice, step summarySupported— no fixture yetRunner-side; proved by the runner's own conformance suite rather than by a plan golden. As on github.com: set-output and save-state still work, with a deprecation warning; set-env and add-path fail their step unless ACTIONS_ALLOW_UNSECURE_COMMANDS is true.

Expressions

FeatureStatusProved byNotes
Operators, literals, indexing, * filtersSupportedcore.jsonl—
contains, startsWith, endsWith, format, join, toJSON, fromJSON, hashFilesSupportedfunctions.jsonl—
success(), always(), cancelled(), failure()Supportedfunctions.jsonl—
Contexts — github, env, vars, job, steps, runner, secrets, strategy, matrix, needs, inputsSupportedcontexts.jsonl—
Error reporting for malformed expressionsSupportederrors.jsonlGitHub's arithmetic-free grammar: ${{ 2 * 3 }} is a syntax error there and here.

Services around a workflow

FeatureStatusProved byNotes
actions/cache and every setup-* action's cache: optionSupported— no fixture yetThe control plane serves GitHub's v1 _apis/artifactcache protocol; proved by a contract test against the real @actions/cache package.
actions/upload-artifact, actions/download-artifactSupported— no fixture yetTwirp ArtifactService plus Azure block-blob upload; proved against the real @actions/artifact at two client generations, including upload-artifact@v7's archive: false, which uploads one file as it is and downloads it back as that file.
OIDC — id-token: write, getIDToken(), cloud federationSupportedpermissions-oidcThe control plane is the issuer: a JWKS document, a discovery document and a per-job token endpoint the runner advertises as ACTIONS_ID_TOKEN_REQUEST_URL.
Cross-run download-artifact inside a workflowNot supported— no fixture yetIt does not use the artifact protocol at all, but the public GitHub REST API through octokit. Serving it needs a GitHub-API-shaped surface; apex artifact download covers the scriptable case. Tracked in the backlog.
GitHub Packages (npm.pkg.github.com, ghcr.io) with the job's own GITHUB_TOKENNot supported— no fixture yetGitHub Packages accepts a personal access token (classic) or GitHub Actions' own GITHUB_TOKEN, and nothing else. On Apex a job's GITHUB_TOKEN is a GitHub App installation token, and the registry refuses it for publishing and for installing, whatever permissions: packages asks for. Create a personal access token (classic) with write:packages to publish or read:packages to install, store it as a secret, and pass that instead, e.g. NODE_AUTH_TOKEN: ${{ secrets.PACKAGES_TOKEN }}.
concurrency: and schedule: in local mode (apex run)Partial— no fixture yetA local run is one run with nothing to wait for: concurrency: groups are accepted and have no effect, and a schedule: trigger never fires on its own — apex run runs the workflow once, as a schedule would.
actions/cache and artifacts in local mode (apex run)Partial— no fixture yetA local run serves both protocols itself: upload-artifact and download-artifact within the run, written to .apex/artifacts/, and actions/cache kept across runs in .apex/cache/ (10 GiB, least recently used first). Proved with the stock actions on Docker Desktop and on a native Linux Docker engine; rootless Docker is not yet proven. One cache scope per checkout, whatever the branch.
Environments — required reviewers, wait timers, branch and tag policies, deployment recordsPartial— no fixture yetEnforced by the control plane and exercised by its own tests rather than a plan golden, but set only through its API, which customer tokens cannot reach. GitHub's own environment rules are not read. A required reviewer is a GitHub user or an Apex Account role, never a GitHub team, and must also hold the role's deployments:write.
github.actor, github.triggering_actor — who started or re-ran a runPartial— no fixture yetThe person's own GitHub login, as on GitHub. What it no longer implies is write access to the repository: on GitHub whoever dispatches or re-runs holds write, and on Apex they hold the Account role that grants actions:write, which an Owner may give to someone with only read on GitHub. A workflow that treats the actor as "someone who can push here" — to skip a check, or to decide whether to publish — should check the permission itself. Deliberate (Apex decides access, not GitHub).

Runners

Not part of the matrix above, because it is a decision about what we run rather than a question about workflow syntax: Apex Actions offers Linux runners on x86-64 and arm64. Windows and macOS are not offered and are not on the roadmap.