# How to set a CI budget for coding agents

> A CI budget for coding agents takes three layers: a job time limit, canceling superseded runs and a hard dollar stop. How to set each one up today.

Source: https://latchkey.dev/learn/agent-ci/cap-agent-ci-spend  
Updated: 2026-11-12

A CI budget for coding agents needs three layers: a time limit on every job, a rule that cancels runs the agent already superseded, and a hard dollar stop that refuses new work once it is reached. GitHub gives you the first two in YAML and the third as an account or repository budget; Latchkey adds a budget on each piece of work, a batch, under a workspace spend limit.

The expensive failure is rarely one big job. It is a cheap job repeated: a test the agent cannot fix, a hang it does not notice, a push per attempt that starts the full pipeline each time. Nobody is watching at 3 a.m., and an agent does not get bored.

A worked example from published numbers shows the scale. A job with no `timeout-minutes` runs for up to 360 minutes; on a GitHub-hosted Linux 2-core runner at $0.006 per minute, one hung attempt costs $2.16. With `timeout-minutes: 15` the same hang costs $0.09. Multiply either by the number of attempts in a night and the first layer has paid for itself.

## Layer 1: bound every job in time

`jobs.<job_id>.timeout-minutes` defaults to 360, and a step's own `timeout-minutes` has the same 360 maximum. Set both lower on anything an agent can trigger. The job limit is the backstop; the step limit on the test step is the one that names the hang in the log.

Pick the number from the slowest green run you have seen, with headroom, not from a guess at what is safe. A timeout that is too tight teaches the agent that the suite is flaky.

```.github/workflows/ci.yml
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
      - run: npm ci
      - run: npm test
        timeout-minutes: 10
```

> Defaults and maximums from the workflow syntax reference; the $0.006 Linux 2-core rate from the Actions runner pricing reference on docs.github.com. Both read 2026-11-12.

## Layer 2: cancel the runs the agent already superseded

An agent that pushes every attempt starts a run per push. Only the newest one matters, so let GitHub cancel the rest: a concurrency group per workflow and ref, with `cancel-in-progress: true`, cancels the running and pending runs of the same branch when a newer one arrives.

Build the group from `github.workflow` as well as the ref. GitHub's own reference warns that group names must be unique across workflows, or a run of one workflow cancels another's.

```.github/workflows/ci.yml
concurrency:
  group: ${{ github.workflow }}-${{ github.ref }}
  cancel-in-progress: true
```

## Layer 3: a dollar stop on the billing side

GitHub's hard stop lives in billing, under Budgets and alerts. For metered products such as GitHub Actions a budget can prevent usage once it is reached, but only if you select **Stop usage when budget limit is reached**. Without it you get an email at the threshold and usage carries on. Alerts can fire at 75%, 90% and 100%.

Three details matter for agents. The narrowest scope for an organization budget is a single repository, so the stop is shared with every person working there: in GitHub's own example, once the organization's Actions budget is spent, members are blocked from using all GitHub-hosted runners until the next billing cycle or until the budget is raised. A budget counts metered usage only from the date it was created, so the first cycle can overshoot. And overlapping budgets each apply, so a product budget and a repository budget can block sooner than you expect.

> Budgets and alerts, and Setting up budgets, docs.github.com, read 2026-11-12.

## What none of the three layers catches

The loop itself. All three GitHub layers work per job, per branch or per account; none of them knows that forty runs over a night were one agent working one task. The retry cap belongs in the agent's instructions, and the dollar cap belongs on the task, which is the gap the next two sections fill.

| Layer | GitHub Actions | Latchkey |
| --- | --- | --- |
| One job | `timeout-minutes` (default 360) | `--timeout` (30 s to 3 h, default 30 min) and `--budget` in dollars |
| Superseded work | `concurrency` with `cancel-in-progress` | `concurrency` in a Latchkey CI workflow file; `latchkey cancel` for jobs |
| One piece of work | None | Batch budget: `latchkey budget set <batch> <dollars>` |
| Everything | Budget with Stop usage, per account or repository | Workspace spend limit, per calendar month |

## Latchkey: a budget on each piece of work

A batch is a name you pass on every job of one task, with `--batch` or `LATCHKEY_BATCH`. Its spend is every job in it at list rates, in whole minutes, with no monthly reset. Once that spend reaches the budget, `latchkey run` in the batch is refused before anything is created, with exit code 193, and the batch's queued jobs are held. Jobs already running finish within their own timeout and budget, so a batch can end a little past its number.

The permission model is the useful part. An API key can set a batch's first budget, up to the workspace spend limit, or lower it, and never raise it; a raise is refused and the budget stays. Only a workspace owner or admin can raise one, on the Billing page. So the agent can be told to budget its own work without being able to talk itself into more.

For a single command, `--budget` stops that job once it has cost the amount at its size's rate. A job stopped there ends with exit code 3, billed at most that budget.

```Terminal
batch="agent-claude-fix-login"
latchkey budget set "$batch" 5 --max-concurrent 4
latchkey run --batch "$batch" --timeout 900 --budget 0.25 'npm ci && npm test'
case $? in
  193) echo "batch budget reached: report it, do not retry" ;;
  3)   echo "this job hit its own budget" ;;
esac
latchkey batch show "$batch"
```

> Exit codes and budget rules from `latchkey help run` and `latchkey help budget` in @latchkeydev/cli 0.16.0, run 2026-11-12.

## The workspace spend limit underneath

An owner or admin sets the spend limit in US dollars on the Billing page. It counts the calendar month, and at the limit Latchkey refuses new jobs, holds queued ones and stops running ones, GitHub Actions jobs on Latchkey runners included. A job or batch budget can never be set above it. There is none by default, so set one before you hand a key to an agent.

Budgets attach to work and to the workspace, not to keys: there is no per-key monthly budget. To see what one agent spent, name its batches after it, which is what Latchkey's own MCP setup page recommends, and read `latchkey budget show <batch>` or the batch budgets list on the Billing page.

## What the agent should do at a limit

Tell it in plain words, because the natural move for an agent that hits a wall is to try again. A refusal at the workspace spend limit carries the code `JOB_CREATION_BLOCKED_SPEND_CAP_REACHED` and is a limit to report to a human, never one to retry. A batch at its budget, `JOB_BATCH_BUDGET_REACHED`, takes a report or a new batch name for genuinely new work, and the CLI's reference rules out the obvious dodge: a new name is not a way around a budget your human set. The credential side of the same problem, one key per agent, is on [one scoped API key per coding agent](/learn/agent-ci/scoped-api-key-per-coding-agent).

## No measured overnight loop

The $2.16 and $0.09 in the introduction are arithmetic on published numbers, not a bill anyone received. No recorded run backs this page, and it has no entry in `content/repro-evidence.mjs` or `content/heal-evidence.mjs`. GitHub's side comes from the workflow syntax, pricing and budgets pages on docs.github.com; Latchkey's from `latchkey help run` and `latchkey help budget` in CLI 0.16.0 and its billing documentation, all read 2026-11-12.

## FAQ

### How do I set a spending limit for GitHub Actions?

Create a budget for the Actions product under Billing, Budgets and alerts, scoped to your account, organization or a single repository, and select "Stop usage when budget limit is reached". Without that option the budget only sends alerts.

### What happens when you reach your GitHub Actions budget?

With Stop usage selected, further metered usage is blocked: GitHub's example shows members blocked from all GitHub-hosted runners until the next billing cycle or until the budget is increased. Without it you are emailed and usage continues.

### What is the default timeout for a GitHub Actions job?

360 minutes. `jobs.<job_id>.timeout-minutes` defaults to 360, and if you set a value above the runner's execution time limit the job is canceled at that limit instead.

### Can I set a spending limit per repository in GitHub Actions?

Yes. Organization budgets can be scoped to the whole organization or to a single repository, and enterprise budgets can also target a cost center. A repository is the narrowest scope for Actions spend.

## References

- [Budgets and alerts, docs.github.com (read 2026-11-12)](https://docs.github.com/en/billing/concepts/budgets-and-alerts)
- [Setting up budgets to control spending on metered products](https://docs.github.com/en/billing/how-tos/set-up-budgets)
- [Actions runner pricing](https://docs.github.com/en/billing/reference/actions-runner-pricing)
- [Latchkey: managing billing, spend limit and batch budgets](https://latchkey.dev/documentation/managing-billing)
- [Latchkey CLI documentation: limits and budgets](https://latchkey.dev/documentation/latchkey-cli)

---

Latchkey runs CI/CD that repairs its own failures. Agent entry points: https://latchkey.dev/agent.txt, https://latchkey.dev/openapi.json, https://latchkey.dev/llms.txt
