GitHub Is the Agent Control Plane You Already Have
Every team building autonomous agents eventually rebuilds the same five things: a task queue, a review gate, a merge protocol, a job runner with logs, and a board a human will actually look at. It takes months, it's the least interesting code in the building, and the result is worse than what you were already paying nothing for.
GitHub is all five. It is an underrated agent control plane, and if your agents run on hardware you own, the whole thing costs approximately zero.
The mapping
| you need | GitHub primitive |
|---|---|
| task queue | Issues, with labels |
| work board | Projects |
| review gate | Pull requests + required checks |
| conflict resolution | git — three-way merge, already solved |
| compute | Actions on self-hosted runners |
| audit trail | run logs + commit history |
| artifact hosting | Releases, Pages, Packages |
The part people miss is the last column's real payoff: every agent action lands somewhere reviewable as a side effect of doing the work. An agent that opens an issue, pushes a branch, opens a PR and lets checks run has produced a complete, timestamped, searchable record without you building a single dashboard. Compare that with an agent writing to a log file on a box you have to SSH into.
And conflict resolution is the sleeper. Multiple agents editing one codebase is a genuinely hard distributed-systems problem — and it's the problem git was built for. You do not need a bespoke locking scheme. You need branches and a merge.
The loop
issue labelled agent:todo
-> workflow triggers (or an agent polls `gh issue list`)
-> agent works on a branch
-> opens a PR, checks run on YOUR runner
-> human approves, or an auto-merge rule does
-> merge closes the issue; logs persist
Dispatch from anything that can shell out to gh:
# hand an agent a task
gh issue create --title "migrate billing retries" \
--label agent:todo --body-file spec.md
# what's queued
gh issue list --label agent:todo --json number,title --limit 50
# trigger a job explicitly
gh workflow run agent-task.yml -f issue=1234
# read the outcome
gh run list --workflow agent-task.yml --limit 5
gh run view <id> --log-failed
workflow_dispatch is the piece to internalise: it hands an agent a triggerable
job with typed inputs and a permanent log, without exposing a service. No
inbound port, no auth you wrote, no uptime you own.
Self-hosted runners are the actual lifehack
Register one machine and it stops being "a computer" and becomes CI/CD, a job scheduler, a secret store and a log viewer. Any workflow targeting your label now runs on your hardware — your GPUs, your disks, your network — scheduled and displayed by GitHub. You get their control plane on your compute.
That inverts the usual cost curve. Long builds, GPU jobs, big model pulls, hour-long test suites: all free, because the minutes are yours. A private repo plus self-hosted runners costs nothing in Actions minutes at all.
Five things will bite you, and they're the reason most people's setup half-works:
Labels are the contract. A job asking for ubuntu-latest will never match a
self-hosted runner, however many you register. Adding capacity without changing the
runs-on label fixes exactly nothing. Read the job's labels, not the workflow
file you assume it uses.
A hosted job that can't be billed fails with zero steps and no log.
gh run view --log-failed answers log not found. The tell is an empty
runner_name and steps: []. It reads as a broken workflow rather than "nothing
picked this up", which is how it goes unnoticed for days — and how a deploy silently
stops shipping.
The runner's toolchain is not free. A self-hosted box has no preinstalled tool cache, and the "free up disk space" steps people copy from hosted-runner blogs will happily delete things you needed. Provision the machine deliberately.
One runner takes one job. A parallel matrix on a single runner is a queue with extra steps.
A ghost registration looks exactly like capacity. An offline-but-registered runner appears in the API and satisfies no job. Verify with a job that runs, not with a listing.
Public vs private, decided properly
The question isn't "is my code secret." It's what each repo costs and exposes:
- Actions minutes are free on public repos — and free on self-hosted runners in either. Self-hosting makes the minutes question disappear.
- Pages publishes from either; a private Pages site generally wants a paid plan. Public site from a private repo is the normal shape.
- Secrets never belong in a public repo, and that includes anything a step echoes into a log. If it's printed, treat it as published.
- Forks of public repos don't receive your secrets on
pull_request. That's a feature. A workflow needing secrets must not run on untrusted forks.
For a solo dev the layout that works: private repo for source and agent workflows, public repo or public Pages branch for what the world sees. Publish outward. Never invert it.
Hygiene when agents write the workflows
Pin what an agent may do. Set permissions: explicitly per job. A broad default
token plus an autonomous author is how one bad turn becomes a force push. Start
read-only; add only what a run demonstrably needs.
A workflow that never triggers produces no red run. Path filters, a branch list naming a deleted branch, a required check reporting skipped — all of these look like success and block merges silently. Assert the trigger, not just the steps.
Never swallow a failure into a warning. An agent loop that reports success while producing nothing is worse than one that crashes, because nobody investigates a green run. This is the single most expensive mistake in the whole pattern.
Build once, deploy many. Rebuilding the same image on every push is where self-hosted runner time actually goes.
Commit granularity is your audit trail. One giant "agent did stuff" commit records nothing. Small commits with real messages are the difference between a log and evidence.
Where it isn't the answer
Worth stating plainly, because the first surprise is what kills trust in a pattern:
- Not real-time. Actions is a batch scheduler. Sub-second control loops don't belong here.
- Not a database. Issues are a queue, not hot queryable state.
- Rate limits are real for a polling agent. Prefer webhooks or an explicit
dispatch over a tight
ghpoll.
None of that touches the core case: unattended work, on your hardware, with a reviewable record.
We wrote it down
All of this is in the open skill pack, so you can hand it to your own agent rather than reimplementing from a blog post:
github-agentic-c2— the wiring diagram above: the mapping, the loop, the public/private split, the workflow hygiene rulesgithub-runner-fleet— stand up, verify and repair the runners; the failure modes above in operational detailgithub-actions-image-pipeline— build-once/deploy-many and why the cache evicts your base imagerepo-to-website/website-as-code/ship-an-app-free— the publishing half, from a repo to a real URL on free tiers
The summary is short enough to act on today: put your agents' work behind issues and PRs, run the jobs on a machine you already own, and let GitHub keep the receipts. You will have a better control plane by Friday than most teams build in a quarter.