Skip to main content
AutoPilot for CI chooses the runner each GitHub Actions job runs on. You add one small routing job to a workflow. It asks CostGraph which runner label fits each job best, and the jobs that need it run there. Every choice is recorded with the reason, and its cost and time are measured after the job finishes.
AutoPilot for CI is free during preview. It routes between GitHub-hosted runners and runner providers such as Blacksmith, Depot, Ubicloud, Namespace, WarpBuild, RunsOn, Jog and Avrea.

Requirements

  • The GitHub organization connected with the CostGraph GitHub App. AutoPilot needs the app’s repository Actions permission set to Read and write, so it can cancel and re-run a run whose routed job never gets a runner. Until an organization owner approves that in GitHub, every job keeps its default runner.
  • Job history. A job is routed only after it has run successfully at least 5 times on its default runner in the last 30 days. A runner counts as measured for a job after 5 successful runs there, or after one trial that passed.
  • A way to sign in: GitHub OIDC (recommended) or a CostGraph API key with the ci:route scope.

Add the routing job

.github/workflows/ci.yml
jobs maps each job’s key under jobs: to its default runner label. For a single job, leave jobs out and use the runner output: runs-on: ${{ needs.route.outputs.runner || 'ubuntu-latest' }}. The routing job’s log and job summary list each job’s runner and why it was chosen. All inputs are listed in the action’s README.

It always falls back

The routing job never blocks a workflow. When CostGraph cannot answer within 3 seconds, answers with an error, is rate limited, is turned off or paused, or the job does not qualify, every job gets its default label and the routing job stays green. The YAML above adds three more guards:
  • timeout-minutes: 2 stops a routing job that cannot start.
  • if: ${{ !cancelled() }} runs the jobs even when the routing job fails.
  • || '<default label>' gives each job its default label when the output is empty.
Set fail-on-error: true on the action if you would rather fail the routing job. The one failure that always fails the routing job is billing: after the trial, if billing is not set up, it fails with a link to set it up. The routing job’s log lists why each job got its runner. The reasons include:

Which runners AutoPilot can choose

A runner label can be chosen when any of these is true:
  • It is a label GitHub documents for its hosted runners, such as ubuntu-latest, ubuntu-22.04, ubuntu-24.04-arm, windows-2025, windows-11-arm, macos-15, macos-15-intel, macos-latest-large or macos-15-xlarge. Larger Linux and Windows runners have names your organization chooses, so turn them on in settings.
  • Its provider is integrated with CostGraph: it has a working connection, or CostGraph reads its runner jobs through your GitHub Actions connection (as for Ubicloud). Every runner the provider publishes then counts.
  • It is turned on in settings.
A label blocked in settings is never chosen. The job must also fit the runner: same operating system and architecture, at least as many vCPUs, and enough memory, GPUs and Docker support. If a chosen runner never picks the job up, AutoPilot moves it back to the default runner (see Stuck jobs).

Policies

Each workflow follows one policy. The default is balanced. Time is the 75th percentile of queue plus run time over the last 30 days, and cost comes from each provider’s published price for that runner, after any included minutes you have left. For a runner the job has not been measured on, time is estimated from public benchmarks. When several runners are within 5% of the best balanced choice, AutoPilot spreads jobs between them so each keeps being measured. Release jobs always take the best one.

Trials

A runner the job has not been measured on that is cheaper than the default runner runs the job once, with reason trial. When that run costs more or takes longer than the default runner, the runner is skipped for this job (trial_failed) and the next run picks the next candidate. It runs again only when its price drops below the price it had at the trial. A trial that passed counts as measured and competes under the policy like any other runner.

Settings

Enabled, pause, policy, providers, runners and runner limits can be set for the organization, a repository or one workflow. The workflow file can set the policy and narrow the providers and runner limits (max-cores, allowed-os, allowed-arch, allow-gpu); provider limits are set for the organization. See AutoPilot for CI settings.

Stuck jobs

If a routed job is still waiting for a runner after its deadline, AutoPilot cancels the run and re-runs the routing job, so the re-run starts every job on its default runner. The deadline is the provider’s usual 95th percentile queue time plus one minute, and never less than 90 seconds. AutoPilot cancels only when no other job of the run has started work. If any job has started, or GitHub cannot be read, it leaves the run alone. A re-run of a run AutoPilot cancelled always uses the default runners. Release and deploy jobs (release: true) are never cancelled or re-run.

Secrets

Runner providers receive jobs with their secrets by default, like any runner your workflow uses. To keep jobs that use secrets off a provider, set allow_secrets: false for it. Jobs then go there only when the routing job says they use no secrets with secrets: false.

Pull requests from forks

Pull requests from forks get no secrets and no OIDC token, so their jobs always run on their default labels. Runs triggered by pull_request_target are never routed either, even though they can sign in.

macOS and Windows

The routing job runs on a Linux or macOS runner. The jobs it routes can run on any operating system: a macOS or Windows job is routed only to runners of the same operating system and architecture with at least as many vCPUs. Use the os, arch, cpu, ram, gpu and docker inputs when the default label does not say what the job needs.

Sign in with GitHub OIDC

The routing job can sign in to CostGraph with the workflow’s own GitHub identity, so you store no CostGraph key in the repository. Give the job permissions: id-token: write and leave api-key empty. GitHub issues a token for https://api.costgraph.ai that names the repository and the run. CostGraph checks it with GitHub, matches the repository’s organization to your GitHub App connection, and returns a CostGraph token that lasts at most an hour and only covers this repository: choosing runners for this run, reading the repository’s CI cost, and looking up prices. The CI cost and Terraform cost actions sign in the same way. An api-key input always wins over OIDC. Use a key with the ci:route scope for self-hosted runners that cannot request OIDC tokens.

Savings

CostGraph compares every routed job with the same job on its default runner over the 30 days before AutoPilot first routed it: the 75th percentile cost and time, fixed at that point. When the job ran fewer than 10 times on its default runner, CostGraph instead prices the job’s own measured run at the default runner’s list price and marks those savings as estimated, with no time saving claimed. Only when even that is unknown does the job add no savings. Savings count what each routed job cost and how long it took once it finished, never the routing estimate. A job that cost more than its baseline counts as added cost, not as negative savings. What the routing job itself costs is subtracted from the total. Read them with GET /api/v1/tenant/ci/autopilot/savings, in total and per workflow.