Select a Runner
Every job in the three shipped workflows (devflow.yml, devflow-implement.yml, devflow-retrospective.yml) runs on ubuntu-latest by default. One GitHub Actions variable, DEVFLOW_RUNNER, changes all of them at once.
Set it under Settings → Secrets and variables → Actions → Variables.
Keep the
DEVFLOW_ prefix exactly as written. There is no PRFLOW_RUNNER alias.
Send the Light Jobs to a Cheaper Runner
A second optional variable,DEVFLOW_LIGHT_RUNNER, moves the light (mostly one-core) jobs onto a cheaper runner while the heavy jobs stay on DEVFLOW_RUNNER. It takes the same value shapes — a bare label or a JSON label array.
The light jobs are, in devflow.yml: config, review_dedupe, gate, review_finalize, and the command job when the triggering comment is a standalone /prflow:review. In devflow-implement.yml: config and gate. Everything else keeps DEVFLOW_RUNNER — a /prflow:review-and-fix or /prflow:pr-description command job, the implement claude job (which runs the test suite, and since issue #402 prefers its own optional DEVFLOW_IMPLEMENT_RUNNER, falling back to DEVFLOW_RUNNER), and every devflow-retrospective.yml job.
When DEVFLOW_LIGHT_RUNNER is unset or empty, each light job falls back to the DEVFLOW_RUNNER chain — DEVFLOW_RUNNER’s value, or ubuntu-latest when that is also unset. Set nothing new and no job moves.
Give Standalone Review Its Own Runner
A third optional variable,DEVFLOW_REVIEW_RUNNER, applies to one job: the command job on a standalone /prflow:review. It is preferred over DEVFLOW_LIGHT_RUNNER there and takes the same value shapes. Use it when that job is worth its own machine but the one-core helpers are not — a standalone review is model-API-bound, so it wants a long-lived, uninterrupted box rather than a fast one, and the helpers around it finish in about a minute.
Unset or empty, the expression falls through to the DEVFLOW_LIGHT_RUNNER chain and then the DEVFLOW_RUNNER chain, so setting nothing moves nothing.
Launch a Runner per Job
In a bare-label value,{0} becomes the workflow run id. Write a literal { as {{.
If you start a fresh runner for each job instead of keeping one running, give each heavy job its own label. Put {1} in DEVFLOW_IMPLEMENT_RUNNER, DEVFLOW_REVIEW_RUNNER or DEVFLOW_RUNNER. It becomes the target issue or pull-request number. For example, ci-{0}-issue-{1} resolves to ci-4512345678-issue-42. This works for a bare label and for a JSON array such as ["self-hosted","ci-{0}-issue-{1}"]. When a JSON array contains {1}, {0} and {{ are expanded in it too.
- Only the implement
claudejob and thecommandjob expand{1}. The light jobs and everydevflow-retrospective.ymljob run before a target number exists, or without one. A{1}in the variable they read fails their workflow evaluation. PointDEVFLOW_LIGHT_RUNNERat a label without{1}, such as a GitHub-hosted runner. Keep{1}out ofDEVFLOW_RUNNERif you run the retrospective. - Put
{0}and{1}in the label. A label that repeats across runs lets one queued job start on another job’s runner. - If no number resolves, PRFlow skips a variable that contains
{1}and uses the next one in the chain. It never produces a label with a blank or literal placeholder. - Launch and remove the runners yourself. Subscribe to the
workflow_jobwebhook. Onqueued, read the job’slabelsand register a runner with exactly those labels. Oncompleted, remove that runner. Registering runners takes a token with repository administration write access, or organization self-hosted-runner write access for organization runners.
{1} resolves exactly as before.
Provision a Self-Hosted Runner
Install these before you point PRFlow at the runner:git.- GitHub CLI (
gh). jq.- Python 3.11 or newer, available as
python3. - A POSIX bash on
PATH. openssl,curlandnohup. The long-run credential refresher needs all three.- Docker, when
setup.servicesor your own checks need it.
ok, and the version should be 3.11 or higher. Fix each MISSING line before you continue.
The shipped workflows upload their execution transcript with
actions/upload-artifact@v7, which requires Actions Runner 2.327.1 or newer (Node 24). A GitHub-hosted runner meets this floor automatically; a self-hosted runner older than 2.327.1 must be upgraded, or the upload step fails.Windows Runners
Windows Runners
The workflows force It refuses to create a shim when no compatible interpreter is present, so a clean exit means the runner is ready.PRFlow cannot make its job credential files owner-only on Windows, because Windows ignores POSIX file modes. Those files live in the runner’s temporary directory under its work directory. Isolation is your host setup:The first command grants the runner account Modify, which it needs to run jobs. The second removes every inherited entry. The third displays the result, which should list only those two entries.Runner services that share one account also share that account’s
bash for their run: steps, so install Git Bash or an equivalent POSIX bash. If Python is available only as python or py -3, run the shipped shim provisioner once on the runner, from a checkout that has the plugin tree:- Run each runner service under its own account. Register it with
--windowslogonaccount, because the defaultNETWORK SERVICEaccount is shared by every service that uses it. - Restrict each runner’s work directory to that account and administrators. With the service stopped, run:
.claude/settings.json, and each can read the others’ job credential files.Executables in Nonstandard Locations
Executables in Nonstandard Locations
Set
DEVFLOW_GH, DEVFLOW_JQ or DEVFLOW_BASH only when the working executable is somewhere the normal search path does not reach. A correct PATH is simpler and less likely to drift.Interruptible (Spot) Capacity
Self-hosted heavy runners on interruptible capacity (for example EC2 Spot) trade cost for the chance that the provider reclaims the instance mid-run. If that happens to a long/prflow:implement job, the runner disappears before the job finishes and the run ends without completing. A recovery job then reconciles the lost run automatically: it resumes a failed run with a bounded request, terminalizes it to 💥 Failed when it cannot resume (except at a spent attempt cap when a resume for that run was already posted or could not be checked), and names the real cause of the loss in its comment. For a Linux EC2 Spot runner you can additionally opt in to confirmed reclaim detection by setting prflow_implement.spot_interruption_watcher.enabled to true, so a reclaim is named as such rather than as a generic runner loss. See Cloud Recovery for what survives an interruption, the recovery job, the opt-in Spot watcher, and how to pick the work back up.
Use Claude Code on Windows
The action’s bundled Claude Code installer is Unix-only. On a self-hosted Windows runner, install Claude Code yourself and point.prflow/config.json at it:
This is a trigger-time setting. It takes effect only after the configuration change is merged into your default branch, not while it sits in a pull request.
Avoid Two Configuration Traps
Leavesetup.git_dir_pin and setup.git_work_tree_pin at false unless you have validated their constraints:
git_dir_pinis not honored by implementation runs and can misdirect repository-root configuration reads. When it is enabled and an implement run ignores it, the run summary now carries a::warning::naming the suppression, so the divergence from your config is visible rather than silent.git_work_tree_pinbreaks remote marketplace cloning. It suits a local-only marketplace list and nothing else.