# How to build a coding agent CI pipeline

> A coding agent CI pipeline needs its own trigger, its own runner and its own budget. Four routes to one, with the limit that decides between them.

Source: https://latchkey.dev/learn/agent-ci/give-your-coding-agent-a-ci-runner  
Updated: 2026-09-20

A coding agent CI pipeline is the loop your agent uses to find out whether its own edit works, and it should not be your team's pipeline: on GitHub, a commit pushed with the job's own token starts no workflow run at all. This page gives the agent four routes to a runner, and names the limit that decides between them.

The loop is push, wait, read, edit, push again. Run it on the shared pipeline and it spends three currencies at once: your team's job concurrency, your Actions minutes, and the attention of everyone watching the repository flicker red and green for edits nobody proposed yet.

It is also, on GitHub, frequently silent. An agent that commits with the token its workflow was handed starts nothing, so the loop it believes it is running does not exist. Start there, because it decides which of the four routes below is even available to you.

## The trigger gap, and what it does to the loop

GitHub states the rule in one sentence: when you use the repository's `GITHUB_TOKEN` to perform tasks, events triggered by the `GITHUB_TOKEN` will not create a new workflow run. The documented exceptions are `workflow_dispatch` and `repository_dispatch`, which always create runs.

There is now one more, and it is the one agents meet. On GitHub.com, when a workflow using `GITHUB_TOKEN` creates or updates a pull request, the resulting `pull_request` events with the `opened`, `synchronize` or `reopened` activity types do create runs, but in an approval-required state: the pull request shows a banner and someone with write access has to select Approve workflows to run. Other activity types, such as `labeled` or `closed`, still create nothing. GitHub records this as rolling out on GitHub.com and not yet shipped to Enterprise Server.

So the agent's loop has a hole in the middle. It pushes, then waits for a run that either does not exist or is parked behind a human who is in a meeting. The four routes below each close that hole differently.

## Route 1: let the agent start the run itself

`workflow_dispatch` is the exception the rule leaves open, and it is what turns a pipeline into something an agent can call. Two conditions apply: the workflow file has to exist on the default branch for the Run workflow button to appear, and GitHub documents that once a workflow has run at least once you can dispatch it against any branch or tag through the API or the CLI.

This is the only route that proves the real pipeline, with the same secrets, permissions, runner image and matrix. It is also the slowest, and it spends a run of your pipeline every time the agent is curious. Give the dispatch its own concurrency group so the agent's third attempt cancels its second.

```.github/workflows/agent-check.yml
on:
  workflow_dispatch:
    inputs:
      suite:
        description: Which suite to run
        default: unit
concurrency:
  group: agent-${{ github.ref }}
  cancel-in-progress: true
jobs:
  test:
    runs-on: ubuntu-latest
    timeout-minutes: 20
    steps:
      - uses: actions/checkout@v7
      - run: npm ci && npm test
```

> `timeout-minutes` defaults to 360 on a job, so a job with no timeout of its own runs for six hours before anything stops it. On a GitHub-hosted runner that default and the platform ceiling are the same number.

## Route 2: a runner of the agent's own, and why it must be ephemeral

The tempting move is a self-hosted runner reserved for the agent. Read GitHub's own hardening guidance before you do: self-hosted runners for GitHub do not have guarantees around running in ephemeral clean virtual machines, and can be persistently compromised by untrusted code in a workflow. The same page warns that destroying the runner after each job is weaker than it sounds, because there is no way to guarantee that a self-hosted runner only runs one job.

An agent's output is code nobody has reviewed yet. That is a milder threat than a stranger's pull request, but it is the same shape: a machine you keep is a machine that keeps whatever the last job left behind, including a dependency the agent installed to make a test pass.

The supported answer is just-in-time runners, which perform at most one job before being automatically removed from the repository, organization or enterprise. GitHub recommends autoscaling with ephemeral runners over persistent ones, because with ephemeral runners it only assigns one job to a runner. Registering one is a single flag.

```Terminal, quoted from the GitHub self-hosted runners reference
./config.sh --url https://github.com/octo-org --token example-token --ephemeral
```

## Route 3: a managed runner label

If the agent should run the same jobs as everyone else but not fight for the same capacity, a managed runner is one line of YAML: change `runs-on` and the job lands on someone else's fleet. Latchkey publishes four sizes, `latchkey-small` through `latchkey-xlarge`, at $0.0025 to $0.0200 per minute per its runners documentation, read on 2026-09-20.

Be clear about what this does and does not fix. It removes the capacity contention and, on Latchkey, adds a runner that repairs environment failures mid-job. It does not create the workflow run the trigger gap swallowed, and it does not give the agent a way to start a check outside the pipeline. Route 1 or Route 4 still has to supply that.

## Route 4: no workflow run at all

The agent usually does not want to know whether your workflow file parses. It wants to know whether the suite passes on a clean Linux machine with a fresh dependency install, which is the thing that differs from its own working tree. A one-off remote job answers that without a commit, a branch or a run.

`latchkey run` packs the working tree, ships it to a fresh runner, executes one bash line and exits with the job's own code; the full flag list is on [run tests on a fresh runner from your terminal](/learn/agent-ci/run-tests-on-a-fresh-runner-from-your-terminal). An agent already connected over MCP has the same fleet through the `run_job` tool, with one difference its own description is blunt about: that runner starts with no workspace and no repository checkout, an empty directory, so anything needing your code goes through the CLI instead.

```Terminal
latchkey run 'npm ci && npm test'
verdict=$?   # the CLI's own exit status is the answer
latchkey run --size medium --timeout 900 'npm ci && npm run build'
```

## The ceiling you will actually hit

It is not minutes. It is concurrency. GitHub's published limits give standard GitHub-hosted runners 20 total concurrent jobs on Free, 40 on Pro, 60 on Team and 500 on Enterprise, with a separate macOS sub-limit of 5 concurrent jobs on Free, Pro and Team and 50 on Enterprise. An agent that opens four pull requests in ten minutes, each starting a five-job matrix, is bidding against your team for that number.

The hard ceilings sit further out and are worth knowing before you let an agent set a timeout: a job on a GitHub-hosted runner may run for 6 hours, and a job on a self-hosted runner for 5 days. The 24-hour queue limit in the same table is a self-hosted row; there is no queue-time row for GitHub-hosted runners. The token has its own clock, because it is an installation access token: on a self-hosted runner it can only be refreshed for up to 24 hours, so a job longer than that needs different credentials.

## What this page does not have

No recorded run stands behind this page. `content/repro-evidence.mjs` has no entry for this slug and `content/heal-evidence.mjs` has none either, so there is no repair block and no reproduction log, and none is implied anywhere above.

The reason is that the subject is an architecture choice rather than a failure. There is no error text to reproduce and no single command whose output would settle it; what would settle it is your own concurrency graph over a week. Every number above is a published limit with the page it came from named in the references, and the Latchkey rates come from its runners documentation rather than from a measurement taken here.

## FAQ

### Can my agent dispatch a workflow on a branch that is not the default branch?

Yes, once the workflow exists on the default branch. GitHub documents that the Run workflow button appears only when the file is on the default branch, and that after a workflow has run at least once you can dispatch it against any branch or tag through the API or the CLI. So the `workflow_dispatch` trigger has to be merged before the agent can aim it at a feature branch.

### How long can one job run before GitHub cancels it?

Six hours on a GitHub-hosted runner and five days on a self-hosted one, per GitHub's published limits. The job-level `timeout-minutes` default is 360, so on a hosted runner the default and the platform ceiling coincide, while on a self-hosted runner the default stops a job long before the platform would. The 24-hour queue limit in the same table applies to self-hosted runners only.

### Should a coding agent be allowed to use our self-hosted runners?

Only if those runners are ephemeral. GitHub is direct about the risk: self-hosted runners have no guarantee of running in clean virtual machines and can be persistently compromised by untrusted code in a workflow, and destroying the machine after each job does not fix it because nothing guarantees a runner only ran one job. Just-in-time runners are the supported shape, since each performs at most one job before being removed.

### How many jobs can run at the same time on my plan?

On standard GitHub-hosted runners, 20 concurrent jobs on Free, 40 on Pro, 60 on Team and 500 on Enterprise. macOS is counted separately and capped much lower, at 5 concurrent jobs on Free, Pro and Team and 50 on Enterprise. GitHub Support can raise these on request, which is worth knowing before you conclude that an agent has to share your existing headroom.

## References

- [GitHub Actions limits (read 2026-09-20)](https://docs.github.com/en/actions/reference/limits)
- [Secure use reference: hardening for self-hosted runners](https://docs.github.com/en/actions/reference/security/secure-use)
- [Self-hosted runners reference: ephemeral runners for autoscaling](https://docs.github.com/en/actions/reference/runners/self-hosted-runners)
- [GITHUB_TOKEN: when it triggers workflow runs](https://docs.github.com/en/actions/concepts/security/github_token)

---

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
