How to build a coding agent CI pipeline
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.
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 testRoute 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.
./config.sh --url https://github.com/octo-org --token example-token --ephemeralRoute 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. 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.
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.
Key takeaways
- Commits pushed with the job's
GITHUB_TOKENstart no workflow run, except through dispatch, or as an approval-requiredpull_requestrun on GitHub.com. workflow_dispatchis the only trigger an agent can pull directly, and the workflow must already be on the default branch.- If you self-host for an agent, make the runner ephemeral: a just-in-time runner performs at most one job before it is removed.
- Concurrency is the ceiling you meet first: on standard GitHub-hosted runners, 20 jobs on Free, 40 on Pro, 60 on Team, 500 on Enterprise.
Frequently asked questions
Can my agent dispatch a workflow on a branch that is not the default branch?
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?
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.