> ## Documentation Index
> Fetch the complete documentation index at: https://docs.costgraph.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# AutoPilot for CI

> Run each GitHub Actions job on the runner that fits its policy, and fall back to its default runner whenever AutoPilot cannot answer.

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.

> [!NOTE]
>
> 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](https://docs.costgraph.ai/costgraph/integrations/github.md#connect-with-the-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](#trials) that
  passed.
* A way to sign in: [GitHub OIDC](#sign-in-with-github-oidc) (recommended) or a
  CostGraph API key with the `ci:route` scope.

## Add the routing job

```yaml .github/workflows/ci.yml theme={null}
name: CI

on:
  pull_request:
  push:
    branches: [main]

jobs:
  route:
    runs-on: ubuntu-latest
    timeout-minutes: 2
    permissions:
      id-token: write
    outputs:
      runners: ${{ steps.route.outputs.runners }}
    steps:
      - id: route
        uses: baselinehq/costgraph-action/route@v0
        with:
          default: blacksmith-4vcpu-ubuntu-2404
          jobs: '{"build":"blacksmith-4vcpu-ubuntu-2404","lint":"blacksmith-2vcpu-ubuntu-2404"}'

  build:
    needs: [route]
    if: ${{ !cancelled() }}
    runs-on: ${{ fromJSON(needs.route.outputs.runners || '{}').build || 'blacksmith-4vcpu-ubuntu-2404' }}
    steps:
      - uses: actions/checkout@v5
      - run: make build

  lint:
    needs: [route]
    if: ${{ !cancelled() }}
    runs-on: ${{ fromJSON(needs.route.outputs.runners || '{}').lint || 'blacksmith-2vcpu-ubuntu-2404' }}
    steps:
      - uses: actions/checkout@v5
      - run: make lint
```

`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' }}`.

| Output | Value |
| - | - |
| `runner` | The runner label for a single job, or the `default` input. |
| `runners` | JSON map of job name to runner label, for the jobs in `jobs`. |
| `decision-id` | The id of the routing decision, empty when no decision was recorded. |

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](https://github.com/baselinehq/costgraph-action#autopilot-for-ci).

## 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:

| Reason | Meaning |
| - | - |
| `trial` | The job runs on a runner it has not been measured on yet. See [Trials](#trials). |
| `trial_failed` | Shown for a runner AutoPilot skipped: its trial for this job cost more or took longer than the default runner. |
| `no_better_candidate` | No runner fits the policy better than the default. |
| `no_history` | The job has not run often enough on its default runner yet. |
| `disabled`, `paused` | AutoPilot is turned off or paused for this workflow. |
| `fork_event` | A pull request from a fork, or a `pull_request_target` run. |
| `cancelled_rerun` | A re-run of a run AutoPilot cancelled. |
| `github_app_reapproval` | Approve the updated CostGraph GitHub App permissions in GitHub. |
| `github_not_connected` | Connect the GitHub organization to CostGraph. |
| `billing_required` | Billing is not set up after the trial. The routing job fails with a link to set up billing. |
| `rate_limited`, `error` | CostGraph could not answer this time. |

## 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](https://docs.costgraph.ai/costgraph/autopilot/ci-settings.md#runners).

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](#stuck-jobs)).

## Policies

Each workflow follows one policy. The default is `balanced`.

| Policy | Picks | Only among runners that |
| - | - | - |
| `cheapest` | the lowest cost per job | take at most twice the default runner's time |
| `fastest` | the shortest time per job | cost no more than the default runner |
| `balanced` | the best combined saving in cost and time | are both no more expensive and no slower than the default runner |

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](https://docs.costgraph.ai/costgraph/autopilot/ci-settings.md).

## 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`](https://docs.costgraph.ai/costgraph/autopilot/ci-settings#provider-limits) 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](https://docs.costgraph.ai/costgraph/integrations/github.md#show-ci-cost-on-pull-requests) and
[Terraform cost](https://docs.costgraph.ai/costgraph/integrations/infracost.md) 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.

## Related

* [CostGraph AutoPilot for Kubernetes](https://docs.costgraph.ai/costgraph/autopilot/overview.md)
* [GitHub integration](https://docs.costgraph.ai/costgraph/integrations/github.md)
* [Workflows](https://docs.costgraph.ai/costgraph/workflows/overview.md), for changes CostGraph plans and runs for you


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.