# Changelog > What's new in RelayRunner and the CLI. Show: **All** · [Platform](/docs/changelog/platform) · [CLI](/docs/changelog/cli) Follow along with the [RSS feed](/changelog.xml). ## CLI v0.10.1 CLI release · November 14, 2026 ### Fixes and improvements - When the gateway refuses a request, `pause`, `share`, `whoami` and `workspaces` now show its reason and error code, such as "Only a machine with an owner can do this (MACHINE_NOT_OWNED)", instead of only the HTTP status. ## CI traces in your observability tool Platform update · [November 14, 2026](/docs/changelog/opentelemetry-traces) - **Telemetry.** Send each workflow run to your observability tool as an OpenTelemetry trace, with a span for every job and step and the time each job waited for a runner. Spans also say where each job ran, whether the Router retried it, and the CPU and memory it used on your machines. Set it up in **Settings → Telemetry**. See [Telemetry](/docs/dashboard/telemetry). ## CLI v0.10.0 CLI release · November 13, 2026 ### New - Report how much CPU and memory each job uses. The dashboard's History page shows it for jobs on your machine. ## CLI v0.9.2 CLI release · November 13, 2026 ### Fixes and improvements - `relayrunner connect` now tells the dashboard how many CPUs and how much memory each job VM gets, so the Runners and Performance pages can show it. ## Retries, queueing and provider usage Platform update · [November 13, 2026](/docs/changelog/retries-queueing-and-provider-usage) - **Automatic retries.** The Router can re-run a job that failed because of its runner, not its own steps: the runner never started it, its machine went offline, or GitHub lost touch with it. The retry routes again, so it can land on a different runner. Turn it on with **Retry jobs when their runner fails** on the Router page, or per repo with [`retry`](/docs/reference/repo-config#retry). See [Automatic retries](/docs/concepts/routing#automatic-retries). - **Jobs wait for a busy machine.** When every machine that fits a job is busy and nothing later in your runner order can take it, the job now waits for a free slot instead of failing. See [When every machine is busy](/docs/concepts/routing#when-every-machine-is-busy). - **Skip providers that stop picking up jobs.** When a hosted provider leaves jobs waiting with no runner, the Router sends new jobs further down your runner order until it recovers. See [When a provider stops picking up jobs](/docs/concepts/routing#when-a-provider-stops-picking-up-jobs). - **Real usage from your providers.** Connect Depot, Namespace or WarpBuild with a read-only token in **Settings → Providers**, and the dashboard pulls your real usage from them every hour. ## CLI v0.9.1 CLI release · November 12, 2026 ### Fixes and improvements - `relayrunner connect` now checks that its Linux VMs can look up hostnames before it builds the job image. When they can't, often because of VPN or DNS filtering software that's still running when its app is closed, it says so and how to check, instead of failing to build the image. ## CLI v0.9.0 CLI release · November 12, 2026 ### New - `relayrunner firewall on` adds a firewall on your Mac for isolated jobs. Job VMs can reach the internet, not your Mac or your local network, and no job can remove it, even with Docker. It asks for your password, stays on after a restart, and `relayrunner firewall off` removes it. - On a Mac with Apple silicon and Apple's container tool, `connect` now runs each job in its own Linux VM, away from your files. `--macos` also takes macOS jobs on the Mac itself, and `--no-isolated` runs every job there. Without the container tool, jobs run on the Mac and `connect` says how to set it up. New `--cpus` and `--memory` flags set the size of each job's VM, like a hosted runner's size. With a size and no `--concurrency`, as many jobs run at once as fit. - `relayrunner connect --isolated --macos` takes both kinds of job on one Mac. Linux jobs run in a VM, and macOS jobs such as Xcode builds run on the Mac itself. - Isolated jobs now find the everyday tools a job written for `ubuntu-latest` expects: Node with npm, pip, compilers and `make`, `wget`, `zip`, Git LFS, Docker Compose and the `gh` CLI. An image given with `--image` is used as it is. ### Fixes and improvements - License the CLI under Apache 2.0 and ship the LICENSE file in the npm package. - `connect` says where jobs run when it starts, and how to take macOS jobs too. With `--macos` or `--no-isolated` it warns that those jobs can read your files. - `init` no longer warns about Docker or Linux packages on a Mac that runs jobs in a VM. ## CLI v0.8.0 CLI release · November 12, 2026 ### New - Docker works in isolated jobs. `relayrunner connect --isolated` now starts Docker inside each job's VM, so jobs with `services:`, `container:` or `docker build` run. `--local-network` uses the same job image as the default, so it builds that image once too. - `relayrunner connect --isolated` (experimental) runs each job in its own Linux VM on a Mac with Apple silicon, using Apple's `container` tool. The VM starts from GitHub's runner image with none of your files mounted, can reach the internet but not your Mac or local network, and is deleted when the job ends. `--local-network` lets jobs reach your network, and `--image` starts from another runner image. `connect` removes the images it got for older runner versions, and keeps a log of what it did in `~/.relayrunner/isolated.log`. - `relayrunner workspaces` shows which of your workspaces this machine runs jobs in and which it doesn't, with its sharing. `relayrunner workspaces add ` and `remove ` change that, by workspace name, slug or id, or from a list when you leave the name out. Adding a workspace while the machine runs anyone's jobs warns that anyone who can push to that workspace's private repos can run jobs on it. After you approve a new machine, `connect` prints the workspaces it runs jobs in and how to change them, and `connect` and `whoami` tell a machine that isn't in any of your workspaces apart from an account with no workspace yet. ### Fixes and improvements - `relayrunner connect` suggests `--isolated` on Macs that can use it. - After its connection drops, `relayrunner connect` waits a few random seconds before reconnecting, so machines don't all reconnect at the same moment after a RelayRunner update. ## CLI v0.7.0 CLI release · November 11, 2026 ### New - `relayrunner connect` tells you when npm has a newer `relayrunner`, with the command that updates it (`npx relayrunner@latest connect`, or `npm install --global relayrunner@latest` for a global install). It asks npm at most once a day, in the background, so it never slows down connecting, and a `connect` left running checks again each day. It only checks when someone is at the terminal and not in CI. `connect` also tells the gateway which CLI version the machine runs, and `relayrunner --version` prints it. - `relayrunner connect` runs several jobs at once: 2 on a computer with 8 or more CPU cores, otherwise 1. `--concurrency ` changes it. Each slot runs its jobs in its own copy of the runner (`~/.relayrunner/runner//slot-/`), and with more than one slot each line of a job's output starts with its slot. ## CLI v0.6.0 CLI release · November 11, 2026 ### New - A machine is now in every workspace you're a member of, so signing in no longer means picking one. `relayrunner connect` and `login` print the workspaces the machine is in, and `whoami` lists them. The sharing question shows when any of your workspaces has someone else in it, and `anyone` means any job in your workspaces. If you aren't in a workspace yet, signing in still works: the CLI says so and links to the GitHub install, and the machine takes jobs once you're in one. `~/.relayrunner/machine.json` no longer saves a workspace, and files from earlier versions still load. - `relayrunner init` keeps a job's Depot, Namespace, WarpBuild, Ubicloud or RunsOn label as its fallback, the same way it keeps a Blacksmith label. ## CLI v0.5.0 CLI release · November 11, 2026 ### New - `relayrunner connect` now names its terminal tab after the machine and what it's doing, such as "RelayRunner · M3P0 · Waiting for jobs", "Running a job" or "Paused", and puts the old title back when it exits. Press `p` to pause or resume the machine without disconnecting, the same as pausing it on the Runners page. ### Fixes and improvements - On a Mac without a hostname of its own, the suggested machine name is now the Computer Name from System Settings (such as "Zach's MacBook Pro") instead of a generic one like `Mac.local`. A hostname you set still comes first. The download message also says that `actions/runner` is GitHub's official runner. ## CLI v0.4.0 CLI release · November 11, 2026 ### New - `relayrunner connect` gets a new machine token from the gateway each time it comes online and the old one stops working, so a copied `machine.json` can't sign in as the machine for long. If another copy does sign in while this one is online, the person who approved the machine gets an email. Before it runs a job, `connect` checks the job's runner config: only GitHub's files, only GitHub servers over https, and a runner named for that job. Anything else is refused and reported as a failed job. The terminal now shows which repo each job is from. `--server`, `RELAYRUNNER_URL` and saved sign-ins must use https, except for `localhost` while developing. The sharing prompt now says that "Anyone in the workspace" only covers jobs from repos you can open. --- # CLI changelog > What's new in each release of the relayrunner CLI. Show: [All](/docs/changelog) · [Platform](/docs/changelog/platform) · **CLI** Follow along with the [RSS feed](/changelog.xml). ## CLI v0.10.1 CLI release · November 14, 2026 ### Fixes and improvements - When the gateway refuses a request, `pause`, `share`, `whoami` and `workspaces` now show its reason and error code, such as "Only a machine with an owner can do this (MACHINE_NOT_OWNED)", instead of only the HTTP status. ## CLI v0.10.0 CLI release · November 13, 2026 ### New - Report how much CPU and memory each job uses. The dashboard's History page shows it for jobs on your machine. ## CLI v0.9.2 CLI release · November 13, 2026 ### Fixes and improvements - `relayrunner connect` now tells the dashboard how many CPUs and how much memory each job VM gets, so the Runners and Performance pages can show it. ## CLI v0.9.1 CLI release · November 12, 2026 ### Fixes and improvements - `relayrunner connect` now checks that its Linux VMs can look up hostnames before it builds the job image. When they can't, often because of VPN or DNS filtering software that's still running when its app is closed, it says so and how to check, instead of failing to build the image. ## CLI v0.9.0 CLI release · November 12, 2026 ### New - `relayrunner firewall on` adds a firewall on your Mac for isolated jobs. Job VMs can reach the internet, not your Mac or your local network, and no job can remove it, even with Docker. It asks for your password, stays on after a restart, and `relayrunner firewall off` removes it. - On a Mac with Apple silicon and Apple's container tool, `connect` now runs each job in its own Linux VM, away from your files. `--macos` also takes macOS jobs on the Mac itself, and `--no-isolated` runs every job there. Without the container tool, jobs run on the Mac and `connect` says how to set it up. New `--cpus` and `--memory` flags set the size of each job's VM, like a hosted runner's size. With a size and no `--concurrency`, as many jobs run at once as fit. - `relayrunner connect --isolated --macos` takes both kinds of job on one Mac. Linux jobs run in a VM, and macOS jobs such as Xcode builds run on the Mac itself. - Isolated jobs now find the everyday tools a job written for `ubuntu-latest` expects: Node with npm, pip, compilers and `make`, `wget`, `zip`, Git LFS, Docker Compose and the `gh` CLI. An image given with `--image` is used as it is. ### Fixes and improvements - License the CLI under Apache 2.0 and ship the LICENSE file in the npm package. - `connect` says where jobs run when it starts, and how to take macOS jobs too. With `--macos` or `--no-isolated` it warns that those jobs can read your files. - `init` no longer warns about Docker or Linux packages on a Mac that runs jobs in a VM. ## CLI v0.8.0 CLI release · November 12, 2026 ### New - Docker works in isolated jobs. `relayrunner connect --isolated` now starts Docker inside each job's VM, so jobs with `services:`, `container:` or `docker build` run. `--local-network` uses the same job image as the default, so it builds that image once too. - `relayrunner connect --isolated` (experimental) runs each job in its own Linux VM on a Mac with Apple silicon, using Apple's `container` tool. The VM starts from GitHub's runner image with none of your files mounted, can reach the internet but not your Mac or local network, and is deleted when the job ends. `--local-network` lets jobs reach your network, and `--image` starts from another runner image. `connect` removes the images it got for older runner versions, and keeps a log of what it did in `~/.relayrunner/isolated.log`. - `relayrunner workspaces` shows which of your workspaces this machine runs jobs in and which it doesn't, with its sharing. `relayrunner workspaces add ` and `remove ` change that, by workspace name, slug or id, or from a list when you leave the name out. Adding a workspace while the machine runs anyone's jobs warns that anyone who can push to that workspace's private repos can run jobs on it. After you approve a new machine, `connect` prints the workspaces it runs jobs in and how to change them, and `connect` and `whoami` tell a machine that isn't in any of your workspaces apart from an account with no workspace yet. ### Fixes and improvements - `relayrunner connect` suggests `--isolated` on Macs that can use it. - After its connection drops, `relayrunner connect` waits a few random seconds before reconnecting, so machines don't all reconnect at the same moment after a RelayRunner update. ## CLI v0.7.0 CLI release · November 11, 2026 ### New - `relayrunner connect` tells you when npm has a newer `relayrunner`, with the command that updates it (`npx relayrunner@latest connect`, or `npm install --global relayrunner@latest` for a global install). It asks npm at most once a day, in the background, so it never slows down connecting, and a `connect` left running checks again each day. It only checks when someone is at the terminal and not in CI. `connect` also tells the gateway which CLI version the machine runs, and `relayrunner --version` prints it. - `relayrunner connect` runs several jobs at once: 2 on a computer with 8 or more CPU cores, otherwise 1. `--concurrency ` changes it. Each slot runs its jobs in its own copy of the runner (`~/.relayrunner/runner//slot-/`), and with more than one slot each line of a job's output starts with its slot. ## CLI v0.6.0 CLI release · November 11, 2026 ### New - A machine is now in every workspace you're a member of, so signing in no longer means picking one. `relayrunner connect` and `login` print the workspaces the machine is in, and `whoami` lists them. The sharing question shows when any of your workspaces has someone else in it, and `anyone` means any job in your workspaces. If you aren't in a workspace yet, signing in still works: the CLI says so and links to the GitHub install, and the machine takes jobs once you're in one. `~/.relayrunner/machine.json` no longer saves a workspace, and files from earlier versions still load. - `relayrunner init` keeps a job's Depot, Namespace, WarpBuild, Ubicloud or RunsOn label as its fallback, the same way it keeps a Blacksmith label. ## CLI v0.5.0 CLI release · November 11, 2026 ### New - `relayrunner connect` now names its terminal tab after the machine and what it's doing, such as "RelayRunner · M3P0 · Waiting for jobs", "Running a job" or "Paused", and puts the old title back when it exits. Press `p` to pause or resume the machine without disconnecting, the same as pausing it on the Runners page. ### Fixes and improvements - On a Mac without a hostname of its own, the suggested machine name is now the Computer Name from System Settings (such as "Zach's MacBook Pro") instead of a generic one like `Mac.local`. A hostname you set still comes first. The download message also says that `actions/runner` is GitHub's official runner. ## CLI v0.4.0 CLI release · November 11, 2026 ### New - `relayrunner connect` gets a new machine token from the gateway each time it comes online and the old one stops working, so a copied `machine.json` can't sign in as the machine for long. If another copy does sign in while this one is online, the person who approved the machine gets an email. Before it runs a job, `connect` checks the job's runner config: only GitHub's files, only GitHub servers over https, and a runner named for that job. Anything else is refused and reported as a failed job. The terminal now shows which repo each job is from. `--server`, `RELAYRUNNER_URL` and saved sign-ins must use https, except for `localhost` while developing. The sharing prompt now says that "Anyone in the workspace" only covers jobs from repos you can open. --- # CI traces in your observability tool > Send every workflow run to Honeycomb, Grafana, Axiom or any OpenTelemetry tool as a trace. Platform update · November 14, 2026 - **Telemetry.** Send each workflow run to your observability tool as an OpenTelemetry trace, with a span for every job and step and the time each job waited for a runner. Spans also say where each job ran, whether the Router retried it, and the CPU and memory it used on your machines. Set it up in **Settings → Telemetry**. See [Telemetry](/docs/dashboard/telemetry). [All updates](/docs/changelog) --- # Platform changelog > What's new in the dashboard, routing and docs. Show: [All](/docs/changelog) · **Platform** · [CLI](/docs/changelog/cli) Follow along with the [RSS feed](/changelog.xml). ## CI traces in your observability tool Platform update · [November 14, 2026](/docs/changelog/opentelemetry-traces) - **Telemetry.** Send each workflow run to your observability tool as an OpenTelemetry trace, with a span for every job and step and the time each job waited for a runner. Spans also say where each job ran, whether the Router retried it, and the CPU and memory it used on your machines. Set it up in **Settings → Telemetry**. See [Telemetry](/docs/dashboard/telemetry). ## Retries, queueing and provider usage Platform update · [November 13, 2026](/docs/changelog/retries-queueing-and-provider-usage) - **Automatic retries.** The Router can re-run a job that failed because of its runner, not its own steps: the runner never started it, its machine went offline, or GitHub lost touch with it. The retry routes again, so it can land on a different runner. Turn it on with **Retry jobs when their runner fails** on the Router page, or per repo with [`retry`](/docs/reference/repo-config#retry). See [Automatic retries](/docs/concepts/routing#automatic-retries). - **Jobs wait for a busy machine.** When every machine that fits a job is busy and nothing later in your runner order can take it, the job now waits for a free slot instead of failing. See [When every machine is busy](/docs/concepts/routing#when-every-machine-is-busy). - **Skip providers that stop picking up jobs.** When a hosted provider leaves jobs waiting with no runner, the Router sends new jobs further down your runner order until it recovers. See [When a provider stops picking up jobs](/docs/concepts/routing#when-a-provider-stops-picking-up-jobs). - **Real usage from your providers.** Connect Depot, Namespace or WarpBuild with a read-only token in **Settings → Providers**, and the dashboard pulls your real usage from them every hour. --- # Retries, queueing and provider usage > The Router retries jobs whose runner failed them, waits for busy machines and skips stuck providers, and the dashboard syncs real usage from your providers. Platform update · November 13, 2026 - **Automatic retries.** The Router can re-run a job that failed because of its runner, not its own steps: the runner never started it, its machine went offline, or GitHub lost touch with it. The retry routes again, so it can land on a different runner. Turn it on with **Retry jobs when their runner fails** on the Router page, or per repo with [`retry`](/docs/reference/repo-config#retry). See [Automatic retries](/docs/concepts/routing#automatic-retries). - **Jobs wait for a busy machine.** When every machine that fits a job is busy and nothing later in your runner order can take it, the job now waits for a free slot instead of failing. See [When every machine is busy](/docs/concepts/routing#when-every-machine-is-busy). - **Skip providers that stop picking up jobs.** When a hosted provider leaves jobs waiting with no runner, the Router sends new jobs further down your runner order until it recovers. See [When a provider stops picking up jobs](/docs/concepts/routing#when-a-provider-stops-picking-up-jobs). - **Real usage from your providers.** Connect Depot, Namespace or WarpBuild with a read-only token in **Settings → Providers**, and the dashboard pulls your real usage from them every hour. [All updates](/docs/changelog) --- # Optimized checks > Simple checks that run with no runner and no billed minutes. Simple checks like a PR title check or lint finish in seconds, but GitHub bills each job as a whole minute. An optimized check runs without a runner, so it uses no minutes. ## As a job Put the check in its own job on `runs-on: relayrunner`: ```yaml on: pull_request: types: [opened, edited, synchronize, reopened] jobs: pr-title: runs-on: relayrunner steps: - uses: relayrunner/actions/checks/pr-title@v1 ``` Keep optimized checks in a job of their own. A job that mixes in other steps fails with the reason. Each check is also an ordinary action, so the same job works on `ubuntu-latest` or your own runner if you point it there. ## The checks | Check | What it does | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `checks/pr-title` | Fails unless the pull request title is a [Conventional Commit](https://www.conventionalcommits.org/), such as `feat(api): add a check`. `types` narrows the allowed types. | | `checks/lint` | Runs linters on the commit and reports each problem on its file and line. `tools` picks the [linters](/docs/reference/repo-config#checkslint) and `paths` limits which files count. | > **Lint is in preview:** Lint runs in RelayRunner's own sandbox with no internet access. The job reads your code with its own `GITHUB_TOKEN`, which GitHub revokes when the job ends, so it needs no extra App. It's only on for organizations we've enabled so far. The job fails with the reason otherwise. ## From repo config The PR title check and lint can also run on every pull request with no workflow at all. Lint from repo config needs the separate RelayRunner Checks GitHub App, which can read the repo's code. Add `relayrunner.yml` to the repo's root or `.github/` folder: ```yaml checks: pr-title: true lint: [oxlint, typos] ``` This works for public repos too. See [Repo config](/docs/reference/repo-config). ## See what they saved The Usage page counts each optimized check as a job that would have billed at least a minute on `ubuntu-latest`. See [Usage and savings](/docs/dashboard/usage). --- # Principles > The rules routing follows. RelayRunner sits between your workflows and the places they can run. These are the rules it follows. ## Your workflows don't change Opting a job in is one change: a relay job and a `runs-on`. After that, everything else happens in the dashboard or the CLI: changing the runner order, adding a machine, pausing one, switching providers. None of it needs a commit. ## Hosted runners stay the fallback A job that names a hosted label still runs when no machine is free. Your machines add capacity, and CI never waits on them. Close the laptop and jobs run on the next runner in your order. ## No lock-in to one provider A job lists the runners it may use, and your runner order decides between them. Moving from one provider to another, or adding your own hardware, only means reordering the list. Each provider bills you directly under your own account. ## Public repositories never run on your machines Anyone can run code on a machine through a pull request to a public repo, so a public repo's jobs never run on one. Public repos can still route between hosted providers. ## Jobs wait when machines are busy, and fail with a reason otherwise When every machine that fits is busy and no other step can take the job, the job waits for a free slot, the same as on any self-hosted runner. When nothing can take it, the relay fails right away with the reason, such as "No runner available: no self-hosted machine is online and idle". ## One job per runner Every job on a machine gets a fresh runner that takes that one job and is then removed. Nothing is left behind between jobs, and there's no long-lived runner credential to leak. ## Built for people and agents The CLI has readable output for people and `--json` for agents, setup is safe to retry, and the docs and dashboard data are available to agents too. --- # Routing > How a GitHub Actions job opts in to routing, and how the relay picks its runner. A job opts in to routing with a relay job. The relay names the runners the job may use, and gets one of them back. ## The relay job ```yaml jobs: relay-test: uses: relayrunner/actions/.github/workflows/relay.yml@v1 with: machine: macOS, ARM64 github: macos-latest test: needs: relay-test runs-on: ${{ needs.relay-test.outputs.runner }} steps: ... ``` The relay takes one input per provider: - **`machine`**: labels a self-hosted machine must have, all of them. `self-hosted` accepts any machine. - **`github`**: a GitHub-hosted label, such as `ubuntu-latest`. - **A hosted provider's input**, such as `depot` or `blacksmith`: one of that provider's labels, such as `depot-ubuntu-24.04`. See [Hosted providers](/docs/runners/hosted). Leave out a provider and the job never runs there. See [relay.yml inputs](/docs/reference/relay-inputs) for the details. ## What happens on a push 1. The relay job is queued on GitHub with `runs-on: relayrunner`. 2. GitHub tells RelayRunner, which answers the relay itself. It runs no steps and bills no minutes. This takes about a second. 3. The relay goes down your workspace's [runner order](/docs/concepts/runner-order) from the top. It skips any provider the job didn't name, and picks the first one that can take the job now. 4. The relay finishes with a `runner` output: a one-time label for a machine, or the job's own label for a hosted provider. 5. Your job runs on that label. A machine gets a fresh one-job runner for it. If your machines are busy and nothing else fits, the job waits for one. See [When every machine is busy](#when-every-machine-is-busy). If nothing fits at all, the relay fails with the reason and the job doesn't start. See [When nothing fits](#when-nothing-fits). ## Which machine The self-hosted step takes a machine that is online, idle, not paused, has every `machine` label, and is shared with whoever started the job. Your own machines come first for your jobs. A machine takes jobs while it has a free slot (see [Jobs at once](/docs/runners/self-hosted#jobs-at-once)). ## Rules that always apply - **Public repos never run on machines.** The self-hosted step is skipped for them. - **A provider missing from the runner order is never used**, even if the relay names it. - **One relay routes one job.** If two jobs or a matrix share one relay's output, only the first runs. Give each job its own relay. ## When a provider stops picking up jobs If a hosted provider's runner type leaves 3 jobs in a row waiting 10 minutes with no runner, the Router skips it and sends jobs to the next step instead. This covers outages, network trouble, and an account that hit its spend cap. A job that runs and fails doesn't count. After 10 minutes, the Router sends one job back to try it. If a runner picks it up, the runner type is back in use. If not, the Router waits twice as long before the next try, up to an hour. The Router page shows each runner type it's skipping, with a **Try again now** button for owners and admins. When no other step can take a job, the Router still sends it to the skipped runner type rather than failing the relay. This is on by default. To keep sending jobs to a provider however long they wait, turn off **Skip runners that stop picking up jobs** under **Settings** on the Router page. ## When every machine is busy If every machine that fits is busy and no later step in your runner order can take the job, the job waits for a free slot instead of failing. The relay passes, and the job shows as queued on GitHub until a machine has room. Jobs get free slots in the order they started waiting. A waiting job runs as soon as a slot frees, on whichever fitting machine frees first. Cancelling the run takes it out of line, and GitHub cancels a job that waits more than 24 hours. If the machine a job was sent to goes offline before the job reaches it, the job goes back in line the same way. It runs on the next fitting machine with room, or on the same machine once it reconnects. To send jobs elsewhere instead of waiting, add a step after your machines, such as `github` or a hosted provider. ## When nothing fits The relay fails straight away with one line that names why each provider couldn't take the job, such as: > No runner available: no self-hosted machine is online and idle; `github` isn't set on this relay. The job that needs the relay doesn't start. Re-run it once a machine is back, or add a fallback so it doesn't happen again: give the relay a `github` label and keep GitHub-hosted in the runner order. ## When a machine goes away mid-job Routing happens before a job starts. If you stop `relayrunner connect` or the machine sleeps while a job is running, that job fails, the same as on any self-hosted runner. Turn on [automatic retries](#automatic-retries) to have the Router re-run it, or see [Re-running a job](#re-running-a-job). To take a machine out of rotation without cutting off a running job, [pause it](/docs/runners/self-hosted#pause) first. ## Re-running a job **Re-run all jobs** runs the relay again, so the job routes from the top of your runner order. **Re-run failed jobs** keeps the relay's earlier answer, so where the job goes depends on the runner it had: | The job ran on | A re-run goes to | | -------------------------- | -------------------------------------------------------------------------------------------- | | A connected machine | The same machine if it's free, else another machine that fits the job, else it waits for one | | A runner added from GitHub | The same runner | | GitHub-hosted | GitHub-hosted, with the same label | | A hosted provider | The same provider, with the same label | To move a failed job to a different provider, use **Re-run all jobs**. ## Automatic retries The Router can re-run a job that failed because of its runner rather than its own steps. A job counts when: - Its runner never started its steps. - The connected machine running it went offline. - GitHub lost touch with its runner. A job whose own steps failed is never retried. GitHub only re-runs jobs once the whole run has finished, so the retry starts then. The Router re-runs the relay with it, so the retry routes again. Turn this on with **Retry jobs when their runner fails** under **Settings** on the Router page. Owners and admins can choose: - **Send the retry to**: any runner, the same runner first, or a different runner first. When nothing else can take a retry meant for a different runner, it goes back to the one it failed on. - **Retries per run**: 1 or 2. A repo can turn retries on or off for itself with [`retry`](/docs/reference/repo-config#retry) in its `relayrunner.yml`. Each retry shows on the Activity page with the reason. Runs with more than one relay aren't retried yet. ## When RelayRunner can't answer If the GitHub App isn't installed on the repo's owner, or RelayRunner can't be reached, the relay job stays queued until GitHub gives up on it. It doesn't fall back on its own, because only RelayRunner can answer a relay. See [Troubleshooting](/docs/reference/troubleshooting). ## See why a job ran where it did The dashboard's **Activity** page shows each routed job, the runner it got, and every step that was skipped with the reason. See [Activity and history](/docs/dashboard/activity). --- # Runner order > The list a workspace tries runners in, and how to change it. The runner order is your workspace's list of providers, top to bottom. Each job tries the steps in order and uses the first one its relay names that can take it now. Set it on the dashboard's **Router** page. Owners and admins can change it. ## Steps | Step | Takes the job when | | --------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Self-hosted** | A connected machine or added runner is online, idle, has the relay's `machine` labels and is shared with whoever started the job. Never for public repos. | | **GitHub-hosted** | The relay names a `github` label. With a minutes limit, only while the account is under it. | | **A hosted provider** | The relay names one of its labels, such as `depot-*` in the `depot` input. With a spend limit, only while the account is under it. | A step that can't take the job is skipped with a reason, and the next one is tried. ## Common orders **Machines first** (the default for a new workspace). Use your own hardware when it's free, and GitHub otherwise. 1. Self-hosted 2. GitHub-hosted **Included minutes first.** Spend the minutes your plan includes, then move to your machines before overage, then fall back to GitHub anyway so nothing stalls. See [Spend controls](/docs/guides/spend-controls). 1. GitHub-hosted, while under 80% 2. Self-hosted 3. GitHub-hosted **Machines, then a faster provider.** Burst to a [hosted provider](/docs/runners/hosted) such as Depot when your machines are busy, and keep GitHub as the last resort for jobs that don't name one. 1. Self-hosted 2. Depot 3. GitHub-hosted GitHub can appear twice: once with a minutes limit and once without. ## The minutes limit A GitHub-hosted step can have a limit, such as "while under 80%". It then takes a job only while the job's GitHub account has used less than that share of its plan's included minutes this month. Each account is checked on its own. If GitHub won't share an account's minutes, the limit never blocks its jobs. The Router page shows when that's the case. An org that shares its enterprise's minutes is judged against the enterprise's, once the App is [installed on the enterprise](/docs/reference/github-app#enterprises). ## Spend limits A Blacksmith, Depot, WarpBuild or Ubicloud step can have a spend limit, such as "while under $200 a month". It then takes a job only while the job's GitHub account has spent less than that on the provider this calendar month (UTC). Spend is worked out from each finished job's minutes and the provider's list price, so it can differ a little from your bill, and jobs still running count once they finish. Put another step after it, such as GitHub-hosted or your machines, so jobs past the limit still have somewhere to go. ## Changes take effect at once The order applies to the next relay. Nothing in your workflows changes. --- # Add a provider > Teach RelayRunner to recognize another runner service, with a pull request. The dashboard recognizes runner services by their `runs-on` labels, so it can show where every job ran and what it cost. The list lives in code and changes by pull request. If a service you use shows up as **Other**, you can add it. ## What to change 1. **The provider's facts** in [`packages/providers/src/index.ts`](https://github.com/relayrunner/relayrunner/blob/main/packages/providers/src/index.ts): add an id to `PROVIDER_IDS` and an entry to `PROVIDERS` with its name, a one-line summary, how its labels look, an example `runs-on`, and links to its docs and dashboard. The dashboard and these docs both read from here. 2. **Its list prices**, if it publishes them, in [`packages/providers/src/prices.ts`](https://github.com/relayrunner/relayrunner/blob/main/packages/providers/src/prices.ts), with a comment naming the price page. Prices are estimates for the Providers page, so leave out plan allowances and discounts. 3. **How to match its labels** in `LABEL_MATCHERS` in the same file, with its id in `HOSTED_PROVIDER_IDS`. Most services use a prefix, such as `depot-`. Add a case to [`packages/providers/src/index.test.ts`](https://github.com/relayrunner/relayrunner/blob/main/packages/providers/src/index.test.ts). 4. **Its logo**: an SVG in `apps/site/public/providers/`, and the icon and brand color the dashboard uses in `apps/web/src/components/providers.tsx` and `apps/web/src/styles.css`. Run `pnpm check` from the repo root before you open the pull request. The repo's [README](https://github.com/relayrunner/relayrunner#development) covers setup. ## Recognized and routed A provider in `HOSTED_PROVIDER_IDS` is also a step in the runner order. To route to it, add an input of the same name to the relay workflow in [relayrunner/actions](https://github.com/relayrunner/actions) too. --- # Feedback > How to report a problem or ask for something. We read everything. Pick whichever is easiest. - **GitHub issues**: [relayrunner/relayrunner](https://github.com/relayrunner/relayrunner/issues) for bugs and feature requests. Include the Activity page's **Show why** text for a routing problem. - **Email**: [feedback@relayrunner.app](mailto:feedback@relayrunner.app). - **X**: [@relayrunnerCI](https://x.com/relayrunnerCI). - **From an agent**: an agent connected to the [MCP server](/docs/reference/mcp) can call `send_feedback` to report a bug, a confusing result or a missing tool. - **Security issues**: [security@relayrunner.app](mailto:security@relayrunner.app), not a public issue. Pull requests are welcome too. See [Add a provider](/docs/contribute/add-provider) for a good first one. --- # Activity and history > Where each job ran, why it ran there, and how long it waited. Two dashboard pages show what happened in your CI: **Activity** and **History**. Both cover every repo the workspace's GitHub installation can see. For how long things usually take, see [Performance](/docs/dashboard/performance). ## Activity **Activity** lists routed jobs, optimized checks, machine events and member changes, newest first. For each routed job it shows the runner it got, and **Show why** lists every step of the runner order that passed on it, with the reason, such as no free machine or a minutes limit reached. A relay that failed shows the same message the job failed with on GitHub. Filter by time, repo, result or machine. ## History **History** has two tabs. Each run and job shows who started it and its short commit, so you can tell pushes apart. - **Workflows** lists every workflow run, newest first. Routed runs are marked, and each run's time is compared with its workflow's usual time. - **Jobs** lists every job a runner finished, newest first, with where it ran, how long it waited and ran, and how it ended. Search by job or workflow name. Click a run to open it in a side panel: each job, where it ran, and how long it waited for a runner. Click a job to see its steps and how long each took, then step back up to its run. Each panel links to GitHub and to the job's usual times in Performance, and its address can be shared. A chart above each list shows how long every run or job took over the chosen range, one dot each. Switch it to bars to see how many finished per hour (or per 6 or 12 hours on longer ranges), split by result. The chart follows the range and filters above it. Jobs on your connected machines also report how much CPU and memory they used. On the Jobs tab, the chart can show either one (the p95 of the job's use), and the table gets CPU and Memory columns. CPU is a share of the job's cores: its VM's, or the machine's when the job runs on the machine itself. ## From an agent The same data is available to agents through the [MCP server](/docs/reference/mcp), such as "where did the last run of `test` go, and why?". --- # Performance > How long your workflows and jobs usually take, and which runner suits each job. The **Performance** page, under Analytics, shows how long your workflows and jobs usually take over the past 24 hours, 7, 14 or 30 days. Times come from successful runs only, since a failed run stops early. The ⓘ beside each column explains how it's worked out. ## Workflows For each workflow: its runs, the share that succeeded, and the median and p90 time from a run's start to its last job finishing. That time includes any wait for a runner, so it's what you actually wait for. **Waiting** is how much of that time no job was running, mostly jobs waiting for a runner. **Trend** compares the range with the one just before it, such as the past 7 days with the 7 days before; red means the workflow got slower. The 30-day range has no trend, since job history goes back 31 days. **Slowest job** is the job that usually takes longest. Open it to compare its times on each runner. ## Jobs How long each job usually takes on each runner it ran on, fastest first. Use it to see which runner suits which job: a test suite that takes 2 minutes on your Mac and 9 on `ubuntu-latest` is a good one to route. A time from fewer than 20 runs reads "only 7 runs": hover it to see how much to trust it. Open a job for a chart of every run. ## Runners Each runner's job count, median wait, median and p90 run time, and success rate. **Started** is how reliable the runner is, separate from your tests: the share of jobs sent to it that it picked up. A job counts as missed when it waited 10 minutes and no runner took it. A runner that missed every job still gets a row. The comparison column lines up the jobs a runner shares with `ubuntu-latest` (or your busiest runner), so a runner that only takes quick jobs doesn't look faster than it is. ## From an agent Agents can read the same times through the [MCP server](/docs/reference/mcp), such as "which runner is fastest for `test`?". --- # Remove RelayRunner > Switching jobs back, disconnecting machines and uninstalling the GitHub App. You can stop using RelayRunner at any time. Here's how to undo each part. ## 1. Switch your jobs back For each routed job, delete its `relay-` job and put the label from the relay's `github` (or `blacksmith`) input back in `runs-on`: ```yaml jobs: test: runs-on: ubuntu-latest steps: ... ``` Do this before uninstalling the App. A relay job with nobody to answer it stays queued until GitHub cancels it after 24 hours. To pause routing without editing workflows, stop every machine and keep GitHub-hosted in the runner order. Jobs then run on their old labels. ## 2. Disconnect your machines Stop `relayrunner connect` on each machine and remove it from the **Runners** page. To delete the CLI's files, remove `~/.relayrunner`. ## 3. Remove added runners Remove any [existing runners](/docs/runners/self-hosted#runners-your-org-already-has) you added. This takes the RelayRunner label off them. The runners stay registered in your org. ## 4. Uninstall the GitHub Apps On GitHub, go to your organization's or account's **Settings → GitHub Apps** and uninstall **RelayRunner**, and **RelayRunner Checks** if you installed it. ## 5. Delete your data Email [privacy@relayrunner.app](mailto:privacy@relayrunner.app) to have your account and workspace data deleted. See [Data and privacy](/docs/reference/privacy). --- # Signals > What makes your CI slow, costly or busier than it needs to be, and which workflows it affects. The **Signals** page, under Analytics, points out patterns in your CI that cost you time or money. Each signal has the same title every time, with the repos and workflows it affects listed underneath, most minutes first. Signals are grouped by the question they answer: - **Speed**: why is CI slow? - **Cost**: why does CI cost this much? - **Complexity**: is CI doing more than it needs to? Below the signals, the [Breakdown](#breakdown) shows who and what uses your CI minutes. Every area shows on the page, even when none of its signals came up. Signals that didn't come up are listed under their area, after **No sign of**, so you can see everything the page checks for. The ⓘ beside any signal explains it and links to its section here. Click a row under any signal to see the runs behind it, newest first. Each run opens in [History](/docs/dashboard/activity#history) with its jobs, and on signals about one job, that job. The page updates every hour. A signal shows up once it covers at least 30 minutes and 1% of all your job minutes in the range, so small one-offs stay off the page. Minutes and spend sit in separate columns. Spend uses the same list prices as the [Usage](/docs/dashboard/usage) page, so public repos and self-hosted runners count as free. ## Speed signals Waiting for a runner and waiting on one job cost time rather than money, so those two signals leave the spend column empty. ### Waiting for a runner These jobs waited more than a minute after GitHub queued them before a runner took them, grouped by their `runs-on` labels. Jobs wait when every runner with those labels is busy, or when the account reaches GitHub's limit on jobs running at once. Add a step to your [runner order](/docs/concepts/runner-order) so jobs go to the next runner that's free instead of waiting. Click a runner's name to open its panel, with its jobs per day and recent jobs. When every waiting job ran on the same machine, the name opens that machine. ### One job sets the pace This job ran alongside the others in its run and was still going after they all finished, so the run waited on it alone. The minutes are that wait. Jobs that wait for another job with `needs` aren't counted, since they start after it by design. Split the job, or compare its times on each runner on the [Performance](/docs/dashboard/performance) page. ### Flaky jobs These jobs failed in one of their steps and then passed when the run was tried again, so the failure likely had nothing to do with the change. The minutes and spend are the failed attempts. Fixing the test or step that fails now and then saves the re-run and the wait for it. ### Runner failures These jobs failed before any of their steps started, grouped by their `runs-on` labels. The runner was the problem, not your code. Turn on [automatic retries](/docs/concepts/routing#automatic-retries) to send them to another runner. ## Cost signals ### No concurrency limit These runs kept going after someone pushed again to the same branch. The minutes count from when the newer run started. Your default branch is left out, since most teams want every commit there to finish. Add a concurrency group with `cancel-in-progress` to the workflow, and GitHub cancels the older run when a newer one starts: ```yaml concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true ``` ### Same commit runs twice The workflow ran for the same commit from both `push` and `pull_request`. The push runs are the extra ones, so their minutes and spend are what the page shows. Limit `push` to your default branch and the pull request run still covers every change. ### Short jobs rounded up GitHub bills each job by the whole minute. These jobs finished in under a minute on a billed runner, so part of every minute went unused. The page shows that unused part. Lint and other quick checks can run as [optimized checks](/docs/concepts/optimized-checks) instead. ## Complexity signals ### Docs changes run CI The workflow ran for a pull request that only changed docs: Markdown and other prose files, anything in a `docs` folder, or a license. For runs without a pull request, it goes by the title instead: commits titled as docs changes, such as `docs: fix a typo` in [Conventional Commits](https://www.conventionalcommits.org/). Workflows with "doc" in their name are left out, since building docs is their job. Add `paths-ignore` to the workflow's `push` and `pull_request` triggers so changes that only touch docs skip it: ```yaml on: pull_request: paths-ignore: - "docs/**" - "**/*.md" ``` ### Full matrix on every branch The workflow started 10 or more jobs for a push or pull request on a branch other than your default, usually a wide matrix. All of those runs' minutes count. Running a smaller matrix before merge, and the full one on your default branch, still catches most problems early. ## Breakdown The Breakdown splits all your job minutes and spend one way at a time: - **Person**: who started the run. - **Repository**. - **Workflow**, with its repo. - **Branch**. - **Trigger**: push, pull request, schedule, manual or re-run. - **Changed folder**: the folders a pull request changed, with their repo, such as `src` or `docs`. Inside folders like `apps` and `packages`, each project is its own folder, such as `apps/web`. Only pull request runs count here, and a run counts under every folder it changed, so the rows overlap. Each view lists the 10 with the most minutes, then everything else in one row. Runs count once each, so a run with a 20-job matrix is still one run. To know which folders a pull request changed, the dashboard reads the names of its files from GitHub once an hour. It keeps only the folders, never the files' contents. ### Filter the page Click a row in the Breakdown to narrow the whole page to it: the signals, their rows and the other Breakdown views then only count that person's, repo's, workflow's, branch's, trigger's or folder's runs. Click a row in another view to narrow it further, and use the × beside a filter to remove it. A filtered page also shows that filter's minutes per day, with the same number of days before as a dashed line, so you can see whether it's growing. Filtered pages cover up to the last 30 days. --- # Telemetry > Send every workflow run to your observability tool as an OpenTelemetry trace. Each workflow run can go to your observability tool as a trace: the run, its jobs and their steps, with where each job ran and how long it waited for a runner. Traces use the [OpenTelemetry](https://opentelemetry.io) format and its [CI/CD conventions](https://opentelemetry.io/docs/specs/semconv/cicd/cicd-spans/), so Honeycomb, Grafana, Axiom or your own OpenTelemetry Collector can read them without setup. Every job in your connected repos is included, whichever runner took it. ## Set up 1. Open **Settings → Telemetry**. Owners and admins can change it. 2. Pick where to send traces, or choose **Other** and paste any endpoint that accepts OTLP over HTTP with JSON. Traces go to the endpoint's `/v1/traces`. 3. Add the headers your tool wants, usually an API key. Values are stored encrypted and never shown again. 4. Save, then click **Send test trace**. A trace named `RUN RelayRunner test` shows up in your tool. From then on, each job goes out when it finishes and each run when it completes. ## Spans | Span name | Description | When sent | | ---------------------- | ---------------------------------------------------------------------------------- | ---------------------- | | `RUN {workflow}` | The run attempt, from when it started to when it completed. The root of the trace. | When the run completes | | `{job name}` | One job, from when GitHub queued it to when it finished. | When the job finishes | | `Waiting for a runner` | The time between GitHub queuing the job and a runner taking it. | With its job | | `{step name}` | One step of the job. Skipped steps are left out. | With its job | Failed runs, jobs and steps have an error status and an `error.type`. Ids come from GitHub's run and job ids, so a job sent early and its run sent later land in the same trace. ## Attributes The run span: - `cicd.pipeline.name`, `cicd.pipeline.run.id`, `cicd.pipeline.run.url.full`, `cicd.pipeline.result` - `vcs.ref.head.name`, `vcs.ref.head.revision`, `vcs.change.id` (the pull request number) - `relayrunner.run.attempt`, `relayrunner.run.event` (push, pull_request and so on), `relayrunner.run.title`, `relayrunner.run.actor` - `relayrunner.routed`: whether the run went through the [Router](/docs/concepts/routing) - `relayrunner.route.provider` and `relayrunner.route.skipped`: where the Router sent it, and what it passed over and why - `relayrunner.retry`: whether this attempt was an [automatic retry](/docs/concepts/routing#automatic-retries) Each job span: - `cicd.pipeline.task.name`, `cicd.pipeline.task.run.id`, `cicd.pipeline.task.run.url.full`, `cicd.pipeline.task.run.result`, `cicd.worker.name` - `relayrunner.provider`: where it ran, such as `machine`, `github` or `depot` - `relayrunner.runner`: the machine's name or the runner type - `relayrunner.labels`: the job's `runs-on` labels - `relayrunner.queue.wait_ms` and `relayrunner.billed_minutes` - `relayrunner.cpu.p95`, `relayrunner.memory.p95_mb`, `relayrunner.cpus`, `relayrunner.memory_mb`: on machines that report usage The resource carries `service.name` (`github-actions` unless you change it), `vcs.repository.name`, `vcs.repository.url.full` and `vcs.owner.name`. ## When sending fails If your endpoint is down or rate limiting, sending retries for about an hour. If it refuses the key, the error shows in **Settings → Telemetry**. After three days of nothing but errors, sending pauses until you save the settings again. ## What gets sent Repository and workflow names, branches, commit SHAs, pull request titles, GitHub usernames, job and step names, timings and where each job ran. Never logs, secrets or environment variables. --- # Usage and savings > Minutes, cost estimates, and what optimized checks saved. The **Usage** page covers every job in your connected repos, whichever runner took it. ## Actions minutes For each connected GitHub account: minutes used this month against the plan's included minutes, and how fast they're going. It's the same number a [minutes limit](/docs/concepts/runner-order#the-minutes-limit) uses, and the page says when an account's minutes can't be read. ## Short jobs GitHub bills each job by the minute, rounded up, so a job that finishes in 5 seconds pays for a whole minute. The page shows, per repo, how many jobs finished in under a minute and what that round-up cost. These are good candidates for optimized checks or a machine. Estimates use GitHub's list prices for private repos. Standard runners in public repos and self-hosted runners are free. ## Saved by optimized checks Each [optimized check](/docs/concepts/optimized-checks) counts as a job on `ubuntu-latest` that would have billed at least a minute. ## Providers The **Providers** page shows how much of your CI ran on each provider, with cost estimates at list prices, and which providers you haven't used yet. See [Providers](/docs/runners/providers). --- # Workspaces and members > One workspace per GitHub install, its roles, and where members come from. A workspace is one install of the RelayRunner GitHub App. Install it on an organization or personal account and that account becomes a workspace, named after it, with its own runner order, activity and members. Your machines aren't tied to one: each takes your jobs in every workspace you're in. ## GitHub installation Installing the App makes the workspace, and the person who installed it is its owner. **Settings → GitHub** links to the install on GitHub, where you choose which repos RelayRunner sees. Uninstalling the App deletes the workspace. If the App was installed by someone who isn't an org owner, GitHub sends the owners a request. The workspace appears once an owner accepts it. Organizations and personal accounts both work. A few things only work for organizations, such as runner groups and reading your Actions budget. ## Roles | | Member | Admin | Owner | | ------------------------------------------- | ------ | ----- | ----- | | Connect, pause and share their own machines | ✓ | ✓ | ✓ | | Pause any machine | ✓ | ✓ | ✓ | | See activity, runs and usage | ✓ | ✓ | ✓ | | Remove any machine | | ✓ | ✓ | | Change the runner order | | ✓ | ✓ | | Add existing runners | | ✓ | ✓ | | Make someone an admin | | ✓ | ✓ | | Make someone an owner | | | ✓ | ## Members Membership comes from GitHub. Anyone who can see the workspace's installation on GitHub joins as a member when they sign in to RelayRunner, and leaves when GitHub stops showing it to them, for example when they leave the org. There are no invites: to add someone, give them access to the org on GitHub. Owners and admins change roles under **Settings → Members**. Leaving a workspace doesn't remove your machines. They stop taking that workspace's jobs and keep working in your other workspaces. ## More than one workspace Each install is its own workspace, so installing the App on another org adds one. Use **Add an organization** in the workspace menu at the top of the sidebar, and switch between workspaces there too. A machine you connect is in all of them. ## Emails A few service emails come from `noreply@send.relayrunner.app`. There's no marketing email. | Email | Sent to | When | | --------------------------- | -------------------------- | ----------------------------------------------------------------------------------------------------------------- | | Welcome | You | You sign up. | | New machine connected | The person who approved it | You connect a new machine. | | Machine signed in elsewhere | The person who approved it | A machine's token signs in from another network while the machine is online. Remove the machine if it wasn't you. | Everything else, such as routing decisions and failed relays, is on the **Activity** page. A failed relay also fails its job on GitHub, so it shows up wherever you already get CI failures. --- # Set up with an agent > Letting a coding agent set up RelayRunner and route your jobs for you. ## Install the skills Give your agent RelayRunner's skills, so it knows the CLI, how to route jobs and how to read your CI: ```sh npx skills add relayrunner/relayrunner ``` It works with Claude Code, Codex, Cursor and most other coding agents, and asks which ones to install for. You get three skills: | Skill | What your agent can do with it | | ------------------------- | -------------------------------------------------------------------------------------------------- | | `relayrunner` | Sign a machine in, put it online, size and isolate jobs on a Mac, set up `relayrunner.yml`. | | `relayrunner-route-jobs` | Route a repo's jobs with `relayrunner init`, and explain the jobs it skips. | | `relayrunner-ci-insights` | Answer questions about your runners, runs and usage through the [MCP server](/docs/reference/mcp). | Run `npx skills update` now and then to get the latest. ## Set up with one prompt Without the skills, your agent can still do the whole setup: sign the machine in, pick which jobs to route, edit the workflows and show you the diff. Paste this into your agent, inside the repo's checkout: ```text Set up relayrunner in this checkout using https://raw.githubusercontent.com/relayrunner/relayrunner/main/docs/agent-install.md ``` The [agent guide](https://github.com/relayrunner/relayrunner/blob/main/docs/agent-install.md) tells the agent to: 1. Check the machine is supported and install the CLI. 2. Sign the machine in with `relayrunner login`. You approve it in the browser. 3. Run `relayrunner init --plan --json` and go over which jobs can be routed, and why some are skipped. 4. Route the jobs you pick with `relayrunner init --apply`, then show you `git diff`. It doesn't commit or push unless you ask. 5. Put the machine online with `relayrunner connect`, ideally in your own terminal so it keeps running after the agent's session ends. Every CLI command the agent uses has a `--json` flag, and every step is safe to retry. ## Let the agent see your CI Agents can also read your workspace through the RelayRunner MCP server: runners, recent runs, where each job ran, and usage. It's read-only. See [MCP server](/docs/reference/mcp) to connect one. ## These docs, for agents Every docs page is also available as Markdown. Add `.md` to a page's URL, or ask for it with `Accept: text/markdown`. [llms.txt](/llms.txt) lists every page, and [llms-full.txt](/llms-full.txt) has them all in one file. --- # How it works with GitHub Actions > What changes in your GitHub Actions workflows, what stays the same, and which jobs you can route. Every routed job still runs on GitHub Actions. The only thing that changes is which runner picks it up. ## What changes Each job you route gets a relay job in front of it, and its `runs-on` reads the relay's answer. Here's a complete workflow with `test` routed and `lint` left as it was: ```yaml title=".github/workflows/ci.yml" {9-14,17-18} name: CI on: push: branches: [main] pull_request: jobs: # Picks a runner for the test job: your machines first, then ubuntu-latest. relay-test: uses: relayrunner/actions/.github/workflows/relay.yml@v1 with: machine: self-hosted github: ubuntu-latest test: needs: relay-test runs-on: ${{ needs.relay-test.outputs.runner }} steps: - uses: actions/checkout@v4 - uses: actions/setup-node@v4 with: node-version: 22 - run: npm ci - run: npm test lint: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - run: npm ci - run: npm run lint ``` `npx relayrunner init` makes this change for you. See [Move your workflows over](/docs/get-started/move-over). ## What stays the same - Your workflow files, the events that start them, and your steps. - Check names, so required checks and branch protection keep working. - Logs, artifacts and re-runs, which all stay in GitHub. - Secrets, which go from GitHub straight to the runner. ## Minutes The relay job finishes in about a second without a runner, so it uses no minutes. Jobs on your own machines don't use GitHub minutes either. Jobs that go to GitHub-hosted runners use your included minutes as usual. See [Spend controls](/docs/guides/spend-controls). ## What you can't route yet - **Matrix jobs.** One relay routes one job. - **Reusable workflows.** They choose their own runner. - **Windows jobs**, and GitHub Enterprise Server. - **Public repos on your machines.** Public repos can still route between hosted providers. ## Next - [Quickstart](/docs/get-started/quickstart): connect a machine and route your first job. - [How routing works](/docs/concepts/routing): how the relay picks a runner. --- # Install the CLI > Getting the relayrunner CLI onto a machine, and signing it in. The `relayrunner` CLI connects a machine to your workspaces and routes a repo's jobs. It runs on macOS and Linux with Node.js 22 or newer. Windows isn't supported yet. ## Install Run it with `npx`, with nothing to install: ```sh npx relayrunner connect ``` Or install it globally: ```sh npm install --global relayrunner relayrunner --help ``` ## Sign in There are two ways to sign a machine in. Both open the dashboard, where you sign in with GitHub and name the machine. There's no workspace to pick: the machine is in all of yours. - **`relayrunner connect`** signs in and puts the machine online in one go. Use this on a machine you'll run jobs on now. - **`relayrunner login`** signs in and exits, without putting the machine online. Coding agents use this, since they can't keep `connect` running after their session ends. Check who the machine is signed in as with `relayrunner whoami`. ## Keep it running `connect` is the on/off switch. Leave it open in a terminal tab while you want the machine to take jobs. It reconnects on its own if the network drops. To stop taking new jobs without going offline, run `relayrunner pause`, or press `p` in the `connect` terminal. `relayrunner resume` undoes it. ## Update `npx relayrunner` always fetches the latest version. For a global install, run `npm install --global relayrunner@latest`. `connect` installs GitHub's latest runner each time it starts. GitHub stops sending jobs to a runner about 30 days after a newer one comes out, so restart `connect` now and then if you leave it open for weeks. The Runners page says when a machine's runner needs an update. See the [CLI reference](/docs/reference/cli) for every command and flag. --- # Move your workflows over > What relayrunner init changes in your GitHub Actions workflows, and which jobs it leaves alone. Routing a job through RelayRunner is one change: a relay job in front of it, and a `runs-on` that points at the relay's answer. `relayrunner init` makes that change for you. ## Run init Inside a checkout of the repo: ```sh npx relayrunner init ``` It sorts every job into three groups and lets you pick from the first: - **Ready.** The job uses a plain GitHub-hosted label (`ubuntu-*`, `macos-*`) or a hosted provider's label, such as `blacksmith-*` or `depot-*`. Warnings flag what to check first, such as a job that needs Docker or installs Linux packages when your machine runs jobs on itself and not in a VM. - **Already routed.** The job already runs where a relay sends it. - **Skipped, with the reason.** See below. To see the list without changing anything, run `relayrunner init --plan`. To route jobs without prompts, run `relayrunner init --apply test,lint`, or `--apply all` for every ready job. ## What it writes For a job like this: ```yaml jobs: test: runs-on: ubuntu-latest steps: ... ``` `init` writes: ```yaml jobs: relay-test: uses: relayrunner/actions/.github/workflows/relay.yml@v1 with: machine: self-hosted github: ubuntu-latest test: needs: relay-test runs-on: ${{ needs.relay-test.outputs.runner }} steps: ... ``` The old label moves to the relay's `github` input (or the provider's own input, such as `depot`, for a hosted provider's label), so the job still runs there when no machine is free. A `macos-*` job gets `machine: macOS`, so it only goes to Macs. `init` keeps your comments and formatting, and never commits. Push the change. The check keeps its name in GitHub, so required checks and branch protection keep working. ## Jobs it skips - **Deploy jobs.** Jobs that use secrets or a deploy environment. Running one on a machine gives that machine those secrets, so it's your call. You can route one by hand with the same change. - **Matrix jobs.** One relay answers one job, so it can't serve every leg of a matrix. - **Reusable workflows.** They choose their own runner. - **Windows jobs**, and jobs already on a self-hosted runner. ## Public repos `init` refuses public repositories, since their jobs [never run on your machines](/docs/concepts/principles#public-repositories-never-run-on-your-machines). ## Undo Delete the relay job and put the old label back in `runs-on`. Or leave the change in place: with no machine online and GitHub in your runner order, jobs run on their old label anyway. --- # Route GitHub Actions jobs to any runner > RelayRunner routes each GitHub Actions job to the first runner in your list that can take it, from your own machines to hosted runners. RelayRunner is a router for GitHub Actions. You tell it which runners a job may use, and it sends each job to the first one in your list that can take it right now: your laptop when it's online and free, a self-hosted runner your org already has, a hosted provider such as Blacksmith or Depot, or GitHub's hosted runners. Your workflows stay the same. Checks keep their names, logs stay in GitHub, and you change where jobs go from the dashboard, not with a commit. ## Why use it - **Your minutes ran out.** Coding agents push all day and use up a plan's included minutes early in the month. Send jobs to a machine you already own first, and keep GitHub as the fallback. - **Your bill spiked.** Use your included minutes, then switch to your own machines before you pay overage. - **macOS builds cost 10x.** Run them on the Mac on your desk, and fall back to hosted Macs when it's asleep. - **One spare machine isn't enough.** A home server or old laptop is free, but jobs queue behind each other. It only gets a job when it's idle, and the rest go to hosted runners. ## How it fits together - **Workspace.** Your team in RelayRunner. Each one is a GitHub organization or personal account with the RelayRunner GitHub App installed, and its members come from GitHub. - **Runners.** Where jobs can run: machines you connect with the CLI, self-hosted runners you already have, hosted providers and GitHub-hosted runners. See [Providers](/docs/runners/providers). - **Relay job.** A small job you add in front of each job you want routed. It names the runners that job may use and finishes in about a second, with no runner and no billed minutes. See [Routing](/docs/concepts/routing). - **Runner order.** The order the workspace tries runners in, set on the dashboard's Router page. See [Runner order](/docs/concepts/runner-order). ```yaml jobs: relay-test: uses: relayrunner/actions/.github/workflows/relay.yml@v1 with: machine: self-hosted # any machine in your workspace github: ubuntu-latest # when no machine is free test: needs: relay-test runs-on: ${{ needs.relay-test.outputs.runner }} steps: - uses: actions/checkout@v4 - run: npm test ``` Each job on a machine gets a fresh one-job runner, so nothing is left behind between jobs. Public repositories never run on your machines. ## Next - [Quickstart](/docs/get-started/quickstart): a green job on your own machine in a few minutes. - [Principles](/docs/concepts/principles): the rules routing follows. --- # Quickstart > Go from sign-up to a green GitHub Actions job on your own machine. You need a private GitHub repository, and a Mac or Linux computer with Node.js 22 or newer. ## 1. Connect your machine In a terminal on the computer you want CI on, run: ```sh npx relayrunner connect ``` The first run shows a code and opens the dashboard. Continue with GitHub to sign up and install the [RelayRunner GitHub App](https://github.com/apps/relayrunner) on your organization or personal account in one trip. The install is your workspace. Then check the code matches and approve the machine. Leave `connect` running. While it runs, the machine is online and takes jobs. Ctrl-C takes it offline. ## 2. Route a job In another terminal, inside a checkout of your repository, run: ```sh npx relayrunner init ``` `init` lists the repo's jobs and lets you pick which to route. For each one it adds a relay job in front and points the job's `runs-on` at the relay's answer. The job's old label, such as `ubuntu-latest`, becomes the fallback. It only edits `.github/workflows/` and never commits. See [Move your workflows over](/docs/get-started/move-over) for what it changes and what it skips. > **Tip:** If you run `connect` inside the repository, it offers to do this step for you. ## 3. Push and watch Commit the change and push. In the workflow run on GitHub you'll see `relay- / RelayRunner` finish in about a second, then your job start. The terminal running `connect` shows the job as it runs. Open **Activity** in the [dashboard](https://app.relayrunner.app) to see where the job ran and why. Stop `connect`, push again, and the same job runs on its old label instead. ## Next - [Runner order](/docs/concepts/runner-order): choose what's tried first. - [Run CI on your laptop while you work](/docs/guides/laptop-ci): the full laptop setup. - [Set up with an agent](/docs/get-started/agents): let a coding agent do all of this. --- # Keep up with coding agents > When agents push all day, run CI on your own machines first and burst to hosted runners when they're full. Coding agents open pull requests around the clock, and each push runs CI. A month's included minutes can go in a few days, a single self-hosted machine turns into a queue, and paying for fast hosted runners for every push adds up quickly. This guide uses every machine you have first, and sends only the overflow to a hosted provider. ## 1. Connect what you have Run `relayrunner connect` on every spare computer: the Mac mini, the old laptop, the home server. Each one runs a job or two at once (more with `--concurrency`), so more machines means more jobs at once. Share them with **anyone** in the workspace so jobs from agents and teammates can use them: ```sh relayrunner share anyone ``` Jobs started by a bot only run on machines shared with anyone. ## 2. Burst to hosted runners Set the runner order so the overflow goes to a provider instead of waiting: 1. **Self-hosted** 2. **Blacksmith** (optional, if you want faster hosted runners) 3. **GitHub-hosted** When every machine is busy, the next job goes to the next step straight away, instead of queueing behind the others. ## 3. Cap the hosted spend Put a minutes limit on GitHub so overflow stops billing at a point you choose. See [Spend controls](/docs/guides/spend-controls). With no unlimited step after it, a job that finds every machine busy fails fast with the reason, and the agent can retry. ## 4. Take the cheap jobs off runners PR title checks and lint don't need a machine at all. Make them [optimized checks](/docs/concepts/optimized-checks) so they never wait for a runner or bill a minute. ## Watch it The **Performance** page shows how long each job takes on each runner and how long it waited for one. If waits grow, connect another machine or move a step up. --- # Run CI on your laptop while you work > For a solo developer who wants CI on their own computer while they're working, and back on GitHub when the lid closes. Your laptop is idle most of the time you're coding, and it's usually faster than a 2-core hosted runner. This guide runs your CI there while you work, and on GitHub the rest of the time, with nothing to switch. ## 1. Connect the laptop ```sh npx relayrunner connect ``` Sign up and install the GitHub App when it asks. Leave `connect` running in a terminal tab. ## 2. Route your jobs In your repo's checkout: ```sh npx relayrunner init ``` Pick the jobs to route. Each keeps its old label, such as `ubuntu-latest`, as the fallback. Commit and push. ## 3. Check the runner order On the dashboard's **Router** page, a new workspace starts with: 1. Self-hosted 2. GitHub-hosted That's what you want: the laptop when it's free, GitHub otherwise. ## What happens day to day - **While you work**, jobs you push run on the laptop. The `connect` terminal shows each one. - **When the laptop is busy** with as many jobs as it runs at once, the next one goes to GitHub, so a long test run never blocks a quick lint. - **When the lid closes** or you stop `connect`, jobs go to GitHub, with nothing to change. - **A job running at that moment** stops with the laptop. Use **Re-run all jobs** to send it to GitHub, or **Re-run failed jobs** to run it on the laptop again once it's back. With [automatic retries](/docs/concepts/routing#automatic-retries) on, the Router re-runs it for you. To keep the laptop free for a while, without going offline, press `p` in the `connect` terminal or run `relayrunner pause`. ## Keep it to your own jobs A new machine only runs jobs you started, so a teammate's push or a bot never lands on your laptop. Leave it that way for a personal machine. See [Your machines](/docs/runners/self-hosted#who-can-run-jobs-on-it). ## Linux jobs on a Mac On a Mac with Apple silicon, each job runs in its own Linux VM, so jobs written for `ubuntu-latest` run as they are: `apt`, `services:` and `container:` all work, and the job can't read your files. See [Isolated jobs on a Mac](/docs/runners/self-hosted#isolated-jobs-on-a-mac). On an Intel Mac, jobs run on macOS. Most Node, Python, Go or Rust jobs run fine there. Jobs that install Linux packages with `apt`, or use `services:` or `container:`, need Linux or Docker. `init` warns about these. Give them `machine: Linux` instead, or leave them on GitHub. --- # macOS and iOS builds on your Mac > Run macOS jobs on a Mac you own, and fall back to hosted Macs when it's busy or asleep. macOS minutes on GitHub cost about ten times Linux minutes, so iOS and Mac builds are often most of a CI bill. A Mac mini or the MacBook you already use can run the same jobs for free. ## 1. Connect the Mac On the Mac: ```sh npx relayrunner connect --macos ``` `--macos` matters on a Mac with Apple silicon, which otherwise runs its jobs in Linux VMs. With it, macOS jobs run on the Mac itself, where Xcode is, and Linux jobs still run in VMs. Leave it running. A Mac mini that stays on makes a good dedicated runner; set it to not sleep in System Settings. ## 2. Route the macOS jobs ```sh npx relayrunner init ``` `init` gives each `macos-*` job a relay like this: ```yaml relay-build: uses: relayrunner/actions/.github/workflows/relay.yml@v1 with: machine: macOS github: macos-latest ``` `machine: macOS` means only Macs take it, never a Linux machine. Use `macOS, ARM64` to require Apple silicon. ## 3. Set the order Keep **Self-hosted** above **GitHub-hosted** on the Router page. The Mac takes the job when it's free, and a hosted Mac takes it otherwise. ## Things to check - **Xcode.** The job uses whatever Xcode is installed on the Mac. Select a version in the job, such as with `xcode-select`, rather than relying on the default. - **Signing.** Keychains and certificates on the Mac are visible to the job. Route signing and release jobs only to a Mac you trust, or keep them on GitHub. - **Simulators** and caches persist on the Mac between jobs, which makes later runs faster. Clean them up now and then. - **Several Macs.** Connect each one. Each job goes to an idle one. ## A Mac you don't own A leased or cloud Mac works the same way: run `relayrunner connect` on it. --- # Spend controls > Use your included GitHub minutes first, then switch to your own machines before you pay overage. A busy month, or a few agents pushing all day, can use up a plan's included minutes well before the month ends. After that, GitHub either bills overage or stops starting jobs. This guide keeps CI running without either. ## 1. Order the runners On the **Router** page, set: 1. **GitHub-hosted, while under 80%** 2. **Self-hosted** 3. **GitHub-hosted** Jobs use GitHub while the account is under 80% of its included minutes. Past that, they go to your machines. If no machine is free, the last step still sends them to GitHub, so nothing stalls. Drop step 3 if you'd rather a job fail than ever bill overage. A job that finds no free machine then fails fast with the reason. ## 2. Pick the limit The limit is a share of the plan's included minutes for the month. 80% leaves room for jobs you haven't routed. Each GitHub account is checked on its own. One org going over its limit doesn't move another org's jobs. ## 3. Have somewhere to go The limit only helps if there's a next step. Connect at least one machine, share it with the workspace if teammates push too, and route the jobs that use the most minutes. The **Usage** page shows which repos those are. ## Cap other providers too Blacksmith, Depot, WarpBuild and Ubicloud steps can have a monthly dollar limit the same way. See [Spend limits](/docs/concepts/runner-order#spend-limits). ## Watch it - **Router**: each account's minutes used this month, next to the limit. - **Activity**: jobs that passed GitHub by, marked with the account's usage at the time. - **Usage**: minutes and cost per repo, and how much went on jobs that finished in seconds but billed a whole minute. ## Cut minutes you don't need - **Short jobs** bill a whole minute each. An optimized check bills nothing. Move PR title checks and lint to [optimized checks](/docs/concepts/optimized-checks). - **macOS minutes** cost about ten times Linux. Run macOS jobs on a Mac first. See [macOS and iOS builds on your Mac](/docs/guides/mac-builds). --- # Share your team's machines > Pool the team's spare computers so anyone's jobs can use them. Most teams have computers sitting idle: a Mac mini in the office, a workstation that's off-hours half the day, last year's laptop. Pool them, and anyone's jobs can run on whichever one is free. ## 1. Bring the team in The workspace is your org's GitHub App install, and its members come from GitHub. Anyone who can see the install on GitHub joins when they sign in to RelayRunner, so there's nobody to invite. Members can connect machines, pause them and see activity. Owners and admins also manage the runner order and roles under **Settings → Members**. ## 2. Connect the shared machines On each shared computer: ```sh npx relayrunner connect --share anyone ``` **Anyone** lets the machine take any job in the workspace, including teammates' and bots'. A machine you keep for yourself stays on **only me**. ## 3. What "anyone" covers A machine shared with anyone only takes jobs from repos its owner can open on GitHub. Being in the same workspace isn't enough. That way, connecting a machine never gives it code or secrets its owner couldn't already see. So the person who connects a shared machine should have access to every repo you want it to serve. A team or service account works well for this. ## 4. How jobs are spread Each job goes to an idle machine. The machines of the person who started the job come first, then whichever one was picked least recently, so work spreads across the pool. ## Personal laptops in the pool A teammate can connect their own laptop too, on **only me**. Their pushes use it first, and everything else uses the shared machines. If they want to help the team while they're away, they can switch it to **anyone** and back. ## When someone leaves Remove them from the org on GitHub. They leave the workspace, and their machines stop taking its jobs. --- # RelayRunner API > Read your workspace's runners, runs, routing and usage over HTTP. The RelayRunner API is the read-only HTTP API behind the [MCP server](/docs/reference/mcp). > **Connecting an AI agent?:** Use the [MCP server](/docs/reference/mcp) instead. Claude, Cursor, VS Code and other MCP clients sign in on their own and get these routes as tools. Call the API directly only if you're writing your own OAuth client. ```text https://api.relayrunner.app/api ``` [API routes](/docs/reference/api/routes) lists every route with its parameters and responses, and you can try them there. The [OpenAPI spec](/openapi.json) has the same in JSON. ## Sign in The API uses OAuth 2.1 with PKCE and the `workspace:read` scope. Clients can register themselves, and the authorization server publishes its endpoints at `https://app.relayrunner.app/.well-known/oauth-authorization-server/api/auth`. While signing in, you pick which workspaces the token may read. Send the access token as `Authorization: Bearer `. Tokens last 15 minutes; use the refresh token for a new one. When a token covers more than one workspace, name the one you're asking about in the `x-relayrunner-workspace` header. ## Routes | Route | What it returns | | -------------------------------------------------------- | ---------------------------------------------------------------------- | | `GET /me` | You and the workspaces the token may read | | `GET /workspaces/current` | One workspace, your role and its GitHub accounts | | `GET /workspaces/{id}/runner-order` | The providers jobs are tried on, in order | | `GET /workspaces/{id}/router-setup` | Recent workflows and how many of their runs were routed | | `GET /machines` | Runners, their status, labels, pause state and current job | | `GET /runs` | Workflow runs from the last 14 days | | `GET /runs/{repositoryId}/{runId}/{runAttempt}/jobs` | One run's jobs and where each ran | | `GET /runs/{repositoryId}/{runId}/{runAttempt}/failures` | Why a run's jobs failed, with the end of each log | | `GET /activity` | The activity log, including where each job was routed and why | | `GET /usage`, `/usage/checks`, `/usage/actions` | Short jobs and round-up cost, optimized check savings, Actions minutes | | `GET /job-times` | Typical job durations per runner | | `GET /signals` | What's making CI slow, costly or busier than it needs to be | | `POST /feedback` | Reports a bug, confusing result or missing feature to the team | A failed request answers with a code and a message, such as `{"error": {"code": "INVALID_REQUEST", "message": "The request isn't valid."}}`. [Errors](/docs/reference/errors) lists every code. ## Rate limit Each person's connection can make 300 requests a minute, counting requests from the MCP server too. Past that, the API answers `429` with the `RATE_LIMITED` code and a `Retry-After` header. --- # CLI > Every relayrunner command and flag. The `relayrunner` CLI runs on macOS and Linux with Node.js 22 or newer. Run it with `npx relayrunner `, or install it with `npm install --global relayrunner`. See [Install](/docs/get-started/install). ## `connect` ```sh relayrunner connect ``` Puts this machine online and runs the jobs it's sent until you stop it. On a Mac with Apple silicon and Apple's container tool, each job runs in its own Linux VM; elsewhere jobs run on the machine itself. It runs up to 2 jobs at once on a computer with 8 or more CPU cores, otherwise 1; `--concurrency` changes that. The first run signs the machine in. Ctrl-C takes it offline, and a job that's running stops with it. Press `p` to pause or resume. While it stays open, it checks for a newer GitHub runner every few hours and switches to it between jobs. Run inside a repo with no routed jobs, it offers to route them, the same as `init`. | Flag | What it does | | ------------------------- | ---------------------------------------------------------------------------- | | `--name ` | The machine's name. Defaults to the computer's name. | | `--share only-me\|anyone` | Who can run jobs on this machine. | | `--concurrency ` | How many jobs to run at once. Defaults to 2 with 8+ CPU cores, else 1. | | `--macos` | Also take macOS jobs, which run on the Mac itself. Linux jobs stay in VMs. | | `--cpus ` | CPUs for each job's VM. Defaults to an even share of the Mac's cores. | | `--memory ` | Memory for each job's VM. Defaults to an even share of half the Mac's. | | `--image ` | Your own runner image for the VMs, used with the tools it has. | | `--local-network` | Let jobs in VMs reach your Mac and your local network. | | `--no-isolated` | Run jobs on the Mac itself, where they can read your files. | | `--isolated` | Stop with an error when jobs can't run in VMs, where the default falls back. | The VM flags apply to Macs with Apple silicon. See [Isolated jobs on a Mac](/docs/runners/self-hosted#isolated-jobs-on-a-mac). ## `login` ```sh relayrunner login ``` Signs this machine in and exits, without putting it online. For agents and scripts. Takes `--name` and `--share` like `connect`. ## `whoami` ```sh relayrunner whoami [--json] ``` Shows the machine, the workspaces it's in, who enrolled it, its sharing setting and whether it's online. ## `init` ```sh relayrunner init [--plan] [--apply ] [--json] ``` Routes a repo's jobs through RelayRunner. Run it inside a checkout. See [Move your workflows over](/docs/get-started/move-over). | Flag | What it does | | ---------------- | -------------------------------------------------------------------------------------- | | `--plan` | List the jobs and what would happen. Changes nothing. | | `--apply ` | Route these jobs without prompting: ids (`test`), `file:id` (`ci.yml:test`), or `all`. | | `--json` | Print the result as JSON. | ## `pause` and `resume` ```sh relayrunner pause relayrunner resume ``` Stops new jobs reaching this machine while it stays connected, and lets them through again. A running job finishes. ## `share` ```sh relayrunner share # show the current setting relayrunner share only-me # only jobs you started relayrunner share anyone # any job in the workspace ``` ## `workspaces` ```sh relayrunner workspaces # which workspaces this machine runs jobs in relayrunner workspaces add acme # run jobs from Acme here too relayrunner workspaces remove acme # stop running Acme's jobs here ``` You pick the machine's workspaces when you approve it; this changes them later. `add` and `remove` take a workspace's name, slug or id, or show a list to pick from when you leave it out. Adding a workspace while sharing is `anyone` warns that anyone who can push to its private repos can run jobs on the machine. See the [changelog](/docs/changelog) for what's new. ## `firewall` ```sh relayrunner firewall # show whether it's on relayrunner firewall on # asks for your password relayrunner firewall off ``` Turns on a firewall on your Mac for [isolated jobs](/docs/runners/self-hosted#isolated-jobs-on-a-mac). Job VMs can reach the internet, not your Mac or your local network, and no job can remove it. It stays on after a restart. --- # Errors > The error codes the API returns, and what each one means. When a request fails, the API answers with an HTTP error status and a JSON body like this one: ```json { "error": { "code": "NOT_FOUND", "message": "Not found.", "details": {} } } ``` - `code` names what went wrong. Codes never change once they ship, so branch on the code. - `message` is a sentence you can show a person or pass to an agent. Its wording can change. - `details` appears on some codes with more context, such as the problems in a runner order. When a code means you can try again later, such as `RATE_LIMITED`, the response also has a `Retry-After` header with the seconds to wait. The [MCP server](/docs/reference/mcp) passes the message and the code to your agent. ## Codes | Code | Status | Message | | --- | --- | --- | | `AGENT_NOT_ALLOWED` | 403 | Agent tokens can't call this route. | | `CANNOT_DELETE_SELF` | 400 | You can't delete your own account here. | | `CANNOT_DEMOTE_SELF` | 400 | You can't remove your own operator role. | | `DEVICE_CODE_CLAIMED` | 403 | Another account already approved or denied this request. Run `relayrunner connect` again for a new code. | | `DEVICE_CODE_EXPIRED` | 400 | That code has expired. Run `relayrunner connect` again for a new one. | | `DEVICE_CODE_INVALID` | 400 | That code doesn't match a sign-in request. Check it against your terminal. | | `DEVICE_CODE_USED` | 409 | That code has already been used. | | `FORBIDDEN` | 403 | You don't have access to that. | | `GITHUB_APP_NOT_CONFIGURED` | 500 | The GitHub App isn't set up. | | `GITHUB_RATE_LIMITED` | 503 | GitHub's API limit was reached. Try again shortly. | | `GITHUB_UNAVAILABLE` | 502 | GitHub didn't answer. Try again shortly. | | `INTERNAL_ERROR` | 500 | Something went wrong on our side. | | `INVALID_REQUEST` | 400 | The request isn't valid. | | `INVALID_TOKEN` | 401 | The token is invalid, expired or revoked. | | `LEGAL_TERMS_OUTDATED` | 409 | The terms changed since you opened this page. Reload it to read the new version. | | `MACHINE_NOT_OWNED` | 403 | Only a machine with an owner can do this. | | `METHOD_NOT_ALLOWED` | 405 | That method isn't allowed here. | | `NO_ACTIVE_WORKSPACE` | 409 | Pick a workspace first. | | `NOT_A_MEMBER` | 403 | You aren't a member of that workspace. | | `NOT_FOUND` | 404 | Not found. | | `PAYLOAD_TOO_LARGE` | 413 | The request body is too large. | | `PRICE_EXISTS` | 409 | That rate already has a price from that day. Remove it first to correct it. | | `PRICE_INVALID` | 400 | Check the price and date. | | `PROVIDER_EXISTS` | 409 | There's already a provider with that id. | | `PROVIDER_ID_INVALID` | 400 | Use lowercase letters, numbers and dashes for the id. | | `PROVIDER_INVALID` | 400 | Fill in a name, summary and labels, and use https:// links. | | `PROVIDER_NOT_CONNECTED` | 404 | That provider isn't connected. | | `PROVIDER_PREFIX_TAKEN` | 409 | Another provider, or GitHub's own runners, already use labels like that. | | `PROVIDER_PREFIXES_INVALID` | 400 | Add at least one label prefix, three or more characters each. | | `PROVIDER_TOKEN_REJECTED` | 400 | The provider didn't accept that token. Check it and its scope. | | `PROVIDER_UNKNOWN` | 400 | There's no provider with that id. | | `PROVIDER_UNREACHABLE` | 502 | The provider's API didn't answer. Try again shortly. | | `RATE_LIMITED` | 429 | Too many requests. Try again shortly. | | `RUNNER_ORDER_INVALID` | 400 | The runner order isn't valid. | | `TELEMETRY_ENDPOINT_INVALID` | 400 | Use an https URL on the internet, like https://api.honeycomb.io. | | `TELEMETRY_HEADER_MISSING` | 400 | Enter a value for each header. | | `TELEMETRY_NOT_CONFIGURED` | 404 | Telemetry isn't set up for this workspace. | | `TOKEN_STORAGE_UNAVAILABLE` | 503 | Saving provider tokens isn't set up on this server yet. | | `UNAUTHENTICATED` | 401 | Sign in first. | | `USER_HAS_SHARED_WORKSPACES` | 409 | The user is in workspaces with other members. Remove them from those first. | | `WEBHOOK_ENQUEUE_FAILED` | 503 | The delivery couldn't be queued. Redeliver it from the App's settings. | | `WEBHOOK_SECRET_NOT_CONFIGURED` | 500 | The webhook secret for this App isn't set up on this server. | | `WEBHOOK_SIGNATURE_INVALID` | 401 | The webhook signature is missing or doesn't match the App's secret. | | `WEBSOCKET_REQUIRED` | 426 | Connect with a WebSocket upgrade. | | `WORKSPACE_REQUIRED` | 409 | This token reads more than one workspace. Name one with the x-relayrunner-workspace header. | --- # FAQ > Common questions about RelayRunner. ## Is RelayRunner free? Yes. It's open source, and the core routing will stay that way. We plan to add a paid tier later for advanced management, aimed at larger teams. Nothing you set up now will need to change. ## Do I pay RelayRunner for runner minutes? No. Your machines are yours, and hosted providers bill you directly under your own accounts. A routed job only gets a label. ## Do relay jobs use minutes? No. A relay job finishes in about a second, without a runner. Optimized checks don't use minutes either. ## Does it work with my existing GitHub Actions workflows? Yes. Each routed job gets a relay job in front of it and a `runs-on` that reads its answer. Steps, check names and logs stay the same. See [GitHub Actions](/docs/get-started/github-actions). ## What happens when my laptop is closed? Jobs go to the next step in your runner order, usually GitHub-hosted, with nothing to change. A job that was running when it closed stops. Re-run it and it routes again (see [Re-running a job](/docs/concepts/routing#re-running-a-job)), or turn on [automatic retries](/docs/concepts/routing#automatic-retries) to have the Router re-run it for you. ## Do I need to keep a terminal open? Yes, `relayrunner connect` is the on/off switch. While it runs, the machine takes jobs. ## Can a job read the files on my computer? On a Mac with Apple silicon, no. Each job runs in its own Linux VM that sees none of your files. On Linux and Intel Macs a job runs as you, like on any self-hosted runner, so route jobs only from repos you trust. See [Security](/docs/reference/security). ## Does it work with public repositories? Public repos can use relays to choose between hosted providers, and optimized checks. Their jobs never run on your machines. ## Does it work on Windows? Not yet. Machines can be macOS or Linux, on x64 or ARM64. ## Can I use it with GitHub Enterprise Server? Not yet. It works with organizations and personal accounts on github.com. ## Can a job run on several machines at once, like a matrix? Not through one relay. Each relay routes one job, so matrix jobs aren't routed. Give separate jobs their own relays. ## Can my teammates' jobs run on my laptop? Only if you share it with **anyone**. A new machine only runs jobs you started. ## Does RelayRunner see my code or secrets? No. Jobs on your machines use GitHub's own runner, which talks to GitHub directly. See [Data and privacy](/docs/reference/privacy). ## Can I run RelayRunner myself? The code is on [GitHub](https://github.com/relayrunner/relayrunner). The hosted service at app.relayrunner.app is the supported way to use it today. ## What providers does it support? It routes to your own machines, existing self-hosted runners, GitHub-hosted runners, and hosted providers such as Blacksmith, Depot and Namespace. See [Providers](/docs/runners/providers). --- # GitHub App > What the RelayRunner GitHub App can do, and why it asks for each permission. RelayRunner reaches GitHub through one GitHub App, [RelayRunner](https://github.com/apps/relayrunner). It signs you in, receives workflow events, and registers the one-job runners that answer relays and run jobs on your machines. Install it on each organization or personal account you want to route. ## Repository permissions | Permission | Access | Why | | -------------- | ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------ | | Administration | Read & write | Registers one-job runners on the job's own repo, so only that repo's jobs can reach them. Lists and labels repo runners you add. | | Actions | Read & write | Workflow job and run events, and checking whether a relay job is still queued. Write re-runs jobs for automatic retries and is reserved for cancelling jobs. | | Checks | Read & write | The PR title check, a check run that explains a relay that can't be answered, and reading why a job's runner failed. | | Contents | Read & write, `relayrunner.yml` only | Reads [repo config](/docs/reference/repo-config). Write is reserved for saving it from the dashboard. It can't touch any other file. | | Pull requests | Read | Pull request events for the PR title check. | | Metadata | Read | Required by GitHub for every App. | ## Organization permissions | Permission | Access | Why | | ------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Self-hosted runners | Read & write | Registers one-job runners in the org when a repo runner isn't allowed, and lists and labels [existing runners](/docs/runners/self-hosted#runners-your-org-already-has) you add. | | Administration | Read | Reads your plan, Actions usage and budget for the [minutes limit](/docs/concepts/runner-order#the-minutes-limit). It can't change any setting. | ## Enterprise permissions | Permission | Access | Why | | ------------------ | ------ | ------------------------------------------------------------------------------------------------------------------------------------------------ | | Enterprise billing | Read | Your enterprise's shared Actions minutes, for the minutes limit. Only used if an enterprise owner [installs it on the enterprise](#enterprises). | ## Account permissions | Permission | Access | Why | | --------------- | ------ | ---------------------------------------------------------- | | Email addresses | Read | Your email at sign-in. | | Plan | Read | A personal account's Actions usage, for the minutes limit. | ## Enterprises An org in a GitHub enterprise shares the enterprise's Actions minutes, and GitHub only reports those to the enterprise. To use a minutes limit there, an enterprise owner installs the App on the enterprise too: open [the install page](https://github.com/apps/relayrunner/installations/new) and pick the enterprise. Settings > GitHub links there when it's needed. That install only reads the enterprise's billing. It sees no repos, and your org installs keep doing the routing. Each org in the enterprise is then judged against the shared minutes. ## What it never gets The App can't read your code, beyond `relayrunner.yml`. It doesn't receive job logs or repository secrets. Jobs that run on your machines use GitHub's own runner, which talks to GitHub directly. ## Runner groups If your org's runner group settings block RelayRunner's runners, it posts a failed `RelayRunner` check on the commit that says what to change. ## RelayRunner Checks Lint uses a second, optional App, **RelayRunner Checks**, with Contents read, Checks write and Pull requests read. It lints each commit in a sandbox with no internet access, then discards it. Install it only if you want lint. ## RelayRunner Spend An optional App, [RelayRunner Spend](https://github.com/apps/relayrunner-spend), reads billing on its own. Its only permission is Organization administration (read). You don't need it unless the dashboard asks for it. ## When permissions change When the App asks for a new permission, GitHub asks each account to accept it under **Settings → GitHub Apps**. Features that need it start working once it's accepted. --- # Runner labels > Every runs-on label RelayRunner uses or answers, and what it means. ## Labels you write | Label | Where | Meaning | | ---------------------------------- | ------------------------------------------------------------------------------ | --------------------------------------------------------------------- | | `relayrunner` | A relay job's `runs-on` (set by the relay workflow), or an optimized check job | The job finishes without a runner. | | `self-hosted` | The relay's `machine` input | Any connected machine or added runner. | | `macOS`, `Linux` | The relay's `machine` input | A machine with that OS. | | `ARM64`, `X64` | The relay's `machine` input | A machine with that architecture. | | `ubuntu-latest`, `macos-latest`, … | The relay's `github` input | A GitHub-hosted runner. | | `blacksmith-*`, `depot-*`, … | A hosted provider's input, such as `depot` | That provider's runner. See [Hosted providers](/docs/runners/hosted). | ## Labels the relay returns After the relay answers, the job's `runs-on` is either a one-time `rr-…` label for one of your machines, or the provider label from the relay, such as `ubuntu-latest`. You never write these yourself. ## Recognized labels The Providers, Performance and Usage pages recognize the labels of many runner services, routed or not, to show where jobs ran. See [Providers](/docs/runners/providers#recognized-providers). --- # MCP server > Let AI agents read your workspace's runners, runs and usage. The MCP server lets your agent answer questions about your CI without you opening the dashboard. It's read-only, apart from sending feedback. ```text https://agents.relayrunner.app/mcp ``` ## Connect an agent ### Claude Code Add the server, then run `/mcp` in Claude Code to sign in. ```sh claude mcp add --transport http relayrunner https://agents.relayrunner.app/mcp ``` ### Claude In Claude, open **Settings → Connectors** and choose **Add custom connector**. Name it RelayRunner, paste the URL above, then choose **Connect**. ### Cursor Add this to `~/.cursor/mcp.json`, then choose **Connect** next to RelayRunner in Cursor's MCP settings. ```json { "mcpServers": { "relayrunner": { "url": "https://agents.relayrunner.app/mcp" } } } ``` ### VS Code Add the server, then start it from the **MCP Servers** list to sign in. ```sh code --add-mcp '{"name":"relayrunner","type":"http","url":"https://agents.relayrunner.app/mcp"}' ``` ### Other agents Add the URL as a remote MCP server (streamable HTTP). The agent signs in with OAuth, so there's no key to paste. ### Signing in The first time the agent uses it, your browser opens. Sign in with GitHub, pick which workspaces the agent may read, and approve it. The same steps are in the dashboard under **Settings → Agents**, where you can also see and disconnect agents. Disconnecting, or leaving a workspace, cuts the agent off at once. For better answers, install the `relayrunner-ci-insights` skill with `npx skills add relayrunner/relayrunner`. It tells your agent which tool answers which question. See [Set up with an agent](/docs/get-started/agents#install-the-skills). ## Skills The server also serves the RelayRunner skills, so agents that support skills over MCP can load them without installing anything. Each one is a resource at `skill:///SKILL.md`: | Skill | What it covers | | ------------------------- | --------------------------------------------------------------------- | | `relayrunner` | Setting up machines with the CLI and running them day to day | | `relayrunner-route-jobs` | Routing a repo's jobs, and why a job was skipped | | `relayrunner-ci-insights` | Which tool answers which question, such as why a job ran where it did | ## Things to ask - "Are my runners online, and what are they running right now?" - "Why did my last push run on GitHub instead of my Mac?" - "Which jobs cost the most this month?" - "How long does the test job usually take on each runner?" - "What's making our CI slow?" - "Which workflows aren't routed yet?" ## Tools | Tool | What it reads | | ------------------ | ----------------------------------------------------------------------- | | `list_workspaces` | The workspaces this connection can read | | `get_workspace` | One workspace, your role and its GitHub installation | | `list_runners` | Runners, their status, labels, pause state and current job | | `get_runner_order` | The providers jobs are tried on, in order | | `list_workflows` | Recent workflows and which ones are routed | | `list_runs` | Recent workflow runs, by repo, status or result | | `get_run_jobs` | One run's jobs and where each ran | | `get_run_failures` | Why a run's jobs failed, with the end of each log (read from GitHub) | | `list_activity` | The activity log, including where each job was routed and why | | `get_usage` | Short jobs and round-up cost, optimized check savings, Actions minutes | | `get_job_times` | Typical job durations per runner | | `get_signals` | What's making CI slow, costly or busier than it needs to be | | `send_feedback` | Reports a bug, confusing result or missing tool to the RelayRunner team | Every tool takes an optional `workspace`, needed only when the agent can read more than one. --- # Data and privacy > What RelayRunner sees and stores about your jobs, repos and machines. RelayRunner needs to know about your jobs to route them, but not what's in them. Here's what it sees. The [Privacy Policy](/privacy) has the full detail. ## What we store - **Your account**: your GitHub user id, username, name, email and avatar. - **Workspaces**: their members and roles. - **Repositories**: names, ids and whether each is public or private, for the GitHub accounts you connect. - **Runs and jobs**: repo, workflow and job names, who triggered them, status, timings, and where each job was routed and why. - **Machines**: name, OS and hardware, and when each was last online. - **Actions usage**: minutes and amounts per repo from GitHub billing, where the App has permission. ## What it never gets - **Your code.** The App can only read `relayrunner.yml`. The one exception is lint: it reads a commit with the lint job's own token, or through the separate Checks App if you install it, lints it in a sandbox with no internet access and discards it. - **Job logs and secrets.** Jobs on your machines use GitHub's runner, which talks to GitHub directly. - **Provider accounts.** A routed job only gets a label. The provider, such as GitHub or Depot, runs it under your agreement with them. ## Who sees it Members of a workspace see its machines, runners, activity and usage. The maintainers who run the service can see accounts and workspaces when they need to, to support you or keep it running. Nothing is sold, and there's no third-party analytics. ## Where it lives The service runs on Cloudflare. Data is kept while you use the service. ## Delete your data Email [privacy@relayrunner.app](mailto:privacy@relayrunner.app) to get a copy of your data or have it deleted. See [Remove RelayRunner](/docs/dashboard/remove) for switching off each part. --- # relay.yml inputs > Every input and output of the relay, a reusable GitHub Actions workflow. The relay is a reusable workflow in [relayrunner/actions](https://github.com/relayrunner/actions). Add one in front of each job you want routed. ```yaml jobs: relay-test: uses: relayrunner/actions/.github/workflows/relay.yml@v1 with: machine: self-hosted github: ubuntu-latest test: needs: relay-test runs-on: ${{ needs.relay-test.outputs.runner }} ``` ## Inputs Give one input per provider the job may use. A provider you leave out is never used for this job, even if it's in your runner order. | Input | Default | What it is | | ------------ | ------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `machine` | none | Comma-separated labels a self-hosted machine must have, all of them. `self-hosted` accepts any machine. Narrow it with `macOS`, `Linux`, `ARM64` or `X64`, such as `macOS, ARM64`. Ignored for public repos. | | `github` | none | One GitHub-hosted label, such as `ubuntu-latest` or `macos-latest`. | | `blacksmith` | none | One Blacksmith label, such as `blacksmith-4vcpu-ubuntu-2404`. | | `depot` | none | One Depot label, such as `depot-ubuntu-24.04`. | | `namespace` | none | One Namespace label, such as `namespace-profile-default`. | | `warpbuild` | none | One WarpBuild label, such as `warp-ubuntu-latest-x64-2x`. | | `ubicloud` | none | One Ubicloud label, such as `ubicloud-standard-2`. | | `runs-on` | none | One RunsOn label, such as `runs-on=${{ github.run_id }}/runner=2cpu-linux-x64`. | ## Output | Output | What it is | | -------- | --------------------------------------------- | | `runner` | The label to put in the next job's `runs-on`. | ## Requirements - The [RelayRunner GitHub App](https://github.com/apps/relayrunner) is installed on the repo's owner and connected to a workspace. - If your organization limits which actions and reusable workflows can run, allow `relayrunner/actions/*`. ## Versions `@v1` gets fixes and new features without breaking changes. See [releases](https://github.com/relayrunner/actions/releases). ## Good to know - **One relay per job.** Jobs that share one relay's output won't all run. Give each job its own relay. - **It shows as `relay- / RelayRunner`** in the workflow run, and finishes in about a second. - **No minutes.** The relay runs on `runs-on: relayrunner`, which needs no runner. Don't give any runner of your own the `relayrunner` label, or it may take relays instead. --- # Repo config > The relayrunner.yml file, which sets a repo's checks and retries. `relayrunner.yml` turns on [optimized checks](/docs/concepts/optimized-checks) for every pull request in a repo, with no workflow. Put it at `.github/relayrunner.yml` or the repo root. Checks read it from the default branch, so a pull request can't change the checks it gets. ```yaml checks: pr-title: true lint: [oxlint, typos] ``` ## `checks.pr-title` `true` posts a **PR title** check on each pull request that passes when the title is a Conventional Commit. To narrow the allowed types: ```yaml checks: pr-title: types: [feat, fix, docs, chore] ``` By default, all the usual Conventional Commit types are allowed. ## `checks.lint` A list of linters to run on each pull request: any of `oxlint`, `oxfmt`, `ruff`, `actionlint`, `zizmor`, `shellcheck` and `typos`. To count only some files: ```yaml checks: lint: tools: [oxlint] paths: ["apps/**"] ``` Each tool gets a `Lint / ` check run. Lint needs the RelayRunner Checks GitHub App and is in preview. ## `retry` Turns [automatic retries](/docs/concepts/routing#automatic-retries) on or off for the repo, whatever the workspace sets. `true` uses the workspace's choices, `false` turns retries off here, and you can set either choice: ```yaml retry: runner: other # any, same or other max: 2 # retries per run, 1 or 2 ``` ## Mistakes The file is checked strictly. An unknown key or a bad value fails the check with the reason, instead of being ignored. --- # Security > What RelayRunner can and can't do with your code and machines. ## What runs on your machine A routed job runs in `actions/runner`, GitHub's official runner. The runner talks to GitHub directly for the job's code, secrets and logs. None of it passes through RelayRunner. ## Isolated jobs On a Mac with Apple silicon, each job runs in its own Linux VM. In the VM, a job: - Sees none of your files, and not the machine's token. - Starts clean. Nothing an earlier job left behind is there, and the VM is deleted when the job ends. - Reaches the internet, but not your Mac or your local network. `--local-network` turns this off. A job that sets out to remove this block can, through Docker. Run `relayrunner firewall on` once to add a firewall on the Mac that no job can remove. This needs Apple's container tool. Until it's installed, jobs run on the Mac itself, and `connect` says so when it starts. See [Isolated jobs on a Mac](/docs/runners/self-hosted#isolated-jobs-on-a-mac). ## Jobs on the machine itself On Linux and Intel Macs, with `--no-isolated`, and for macOS jobs with `--macos`, a job runs on the machine as the user who started `relayrunner connect`. It isn't sandboxed. Like any self-hosted runner, it can read and write what that user can, including files in your home folder. So: - Route jobs only from repos you trust to run on that machine. - For a shared or always-on machine, run `connect` as a separate user. - Keep deploy jobs, which carry production secrets, off machines unless you mean to. `init` skips them for this reason. ## One job per runner Each job gets a fresh runner registered for that job alone, and removed when it ends. There's no long-lived runner registration to steal. The terminal shows which repo each job comes from. ## Public repositories Machines never take a public repo's jobs. Anyone can open a pull request to a public repo, and that would let them run code on your machine. Public repos can still use relays to choose between hosted providers. ## Who can send jobs to a machine - **Only me** (the default): only jobs you started. - **Anyone**: any job in the workspace, from a repo the machine's owner can open on GitHub. Sharing never gives a machine code its owner couldn't already see. ## Machine tokens Each machine signs in with its own token, which changes every time `connect` comes online. If someone else signs in with it, the person who approved the machine gets an email. Removing a machine in the dashboard revokes its token straight away. ## The GitHub App The App can't read your code, apart from `relayrunner.yml`, and never receives job logs or secrets. See [GitHub App](/docs/reference/github-app) for every permission and why. ## Report a vulnerability Email [security@relayrunner.app](mailto:security@relayrunner.app). --- # Troubleshooting > Common problems and how to fix them. Start with the **Activity** page in the dashboard. Most routing questions are answered by **Show why** on the job. ## The relay job stays queued `relay- / RelayRunner` never starts, and nothing shows on the Activity page. - **The GitHub App isn't installed** on the repo's owner, or doesn't include this repo. Check **Settings → GitHub** in the repo owner's workspace. If the install was a request to an org owner, it needs approving on GitHub first. - **Your org limits actions.** Allow `relayrunner/actions/*` in the org's Actions settings. - **A runner group blocks it.** A failed `RelayRunner` check on the commit says what to change. - **Another runner has the `relayrunner` label.** Remove it, or it may take relays instead of RelayRunner. A relay nobody answers stays queued until GitHub cancels it after 24 hours. Cancel the run and push again once it's fixed. ## The relay failed with "No runner available" The message says why each provider couldn't take the job. The usual causes: - **"no self-hosted machine is online and idle"**: start `relayrunner connect`, or add a `github` fallback to the relay. - **"is paused"**: resume the machine. - **"only runs … jobs"**: the machine is shared with **only me**, and someone else started the job. See [sharing](/docs/runners/self-hosted#who-can-run-jobs-on-it). - **"public repos never run on self-hosted machines"**: give the relay a `github` label. - **"isn't in your runner order"**: add the provider on the Router page. - **A minutes limit**: the account is over its limit. Add a step after it. - **"GitHub's API limit … was reached"**: your org used up its GitHub API calls for the hour, so the Router couldn't check your machines. Routing goes back to normal when GitHub resets the limit. Add a `github` fallback so jobs still run in the meantime. ## The job ran somewhere I didn't expect Open the job on the Activity page and choose **Show why**. Each skipped step has its reason. Common surprises: - The machine was busy with another job, so the next step took it. - GitHub's API limit was reached, so the Router couldn't check your machines and the next step took the job. - A GitHub step with a minutes limit came first and the account was still under it. - The relay's `machine` labels didn't match, such as `macOS` when the machine is Linux. ## Only the first job ran Two jobs, or a matrix, share one relay's output. Give each job its own relay. ## The job failed on my machine but passes on GitHub The machine is missing something the hosted image has: a tool, a Linux package, Docker for `services:` or `container:`. Install it, narrow the relay's `machine` labels, or leave that job on GitHub. On a Mac with Apple silicon, [jobs run in a Linux VM](/docs/runners/self-hosted#isolated-jobs-on-a-mac) with Docker and the everyday tools, like `ubuntu-latest`. ## Isolated jobs on a Mac - **"Jobs run on this Mac itself"** when `connect` starts: Apple's container tool isn't set up, so jobs aren't isolated. Install it from [its releases page](https://github.com/apple/container/releases), run `container system start`, then start `connect` again. - **"needs Apple's container tool"** or **"isn't running"**: a flag you passed needs VMs. Install the tool from [its releases page](https://github.com/apple/container/releases) and run `container system start`. - **"needs macOS 26 or later"** or **"needs a Mac with Apple silicon"**: this computer can't run jobs in VMs. Run `relayrunner connect` without the VM flags. - **A macOS job never reaches the Mac**: jobs in VMs run on Linux. Add `--macos` to run macOS jobs on the Mac itself. - **A job can't reach a server on your network**: jobs in VMs are kept off your Mac and local network. Add `--local-network`. If you turned on the firewall on the Mac, also run `relayrunner firewall off`. - **A job runs out of memory, or is slow**: give each VM more with `--cpus` and `--memory`, or lower `--concurrency`. - **A job needs a tool the VM doesn't have**: install it in the job, use its `setup-*` action, or pass your own image with `--image`. - **`ping` fails in a job**: that's expected in a VM. Use `curl` to test a connection. For anything else, `~/.relayrunner/isolated.log` records what happened. It holds no job output or credentials, so you can attach it to an issue. ## connect says the machine was removed Someone removed it from the Runners page. Run `relayrunner connect` again to sign in as a new machine. ## Still stuck Ask in [GitHub issues](https://github.com/relayrunner/relayrunner/issues), or send feedback. See [Feedback](/docs/contribute/feedback). --- # GitHub-hosted > Using GitHub's own runners as a provider, and keeping an eye on minutes. **GitHub**: GitHub's own hosted runners, billed per minute in private repos. - Labels: `ubuntu-*, windows-*, macos-*, or a larger runner's own name` - Example: `runs-on: ubuntu-latest` - Links: [Docs](https://docs.github.com/en/actions/reference/runners/github-hosted-runners) GitHub-hosted runners are the usual fallback: always available, and billed against your plan. ## Use it Name a GitHub-hosted label in the relay's `github` input, usually the job's old `runs-on`: ```yaml relay-test: uses: relayrunner/actions/.github/workflows/relay.yml@v1 with: machine: self-hosted github: ubuntu-latest ``` Keep **GitHub-hosted** in your [runner order](/docs/concepts/runner-order), usually last. A step with no limit always takes a job that names a `github` label, so it's a safe last resort. ## Minutes GitHub bills private repos by the minute, rounded up per job, and each plan includes some minutes a month. Standard runners in public repos are free. macOS minutes cost about ten times Linux minutes. A GitHub-hosted step can carry a **minutes limit**, such as "while under 80%". It then only takes jobs while the job's GitHub account has used less than that share of its included minutes. Put another step after it to catch the rest. See [Spend controls](/docs/guides/spend-controls). The **Usage** page shows each account's minutes next to what your jobs would have cost. See [the minutes limit](/docs/concepts/runner-order#the-minutes-limit) for how it's read. --- # Hosted providers > Routing jobs to Blacksmith, Depot, Namespace, WarpBuild, Ubicloud and RunsOn. Hosted providers run GitHub Actions jobs on their own machines, under your account with them. Route jobs to any of these when your own machines are busy, or ahead of GitHub-hosted runners. ## Set it up 1. Install the provider's GitHub App on the organization and set up your account with them. A routed job only gets one of the provider's labels. The provider's App runs it and bills you. 2. Add the provider to your workspace's [runner order](/docs/concepts/runner-order) on the Router page. 3. Name one of its labels in the relay input of the same name: ```yaml relay-test: uses: relayrunner/actions/.github/workflows/relay.yml@v1 with: machine: self-hosted depot: depot-ubuntu-24.04 github: ubuntu-latest ``` Each job has its own relay, so each job picks its own size, such as `blacksmith-2vcpu-ubuntu-2404` for a lint job and `blacksmith-4vcpu-ubuntu-2404` for tests. `relayrunner init` keeps a job's existing provider label as its fallback, so a job already on a hosted provider stays there whenever no machine is free. ## Where it sits in the order A provider's step always takes a job that names one of its labels. There's no way to see a provider's capacity, so steps below it only run for jobs that don't name one. A common order is your machines, then a hosted provider, then GitHub-hosted. Unlike machines, hosted providers also run public repos' jobs. ## The providers | Provider | Relay input | | ---------- | ------------ | | Blacksmith | `blacksmith` | | Depot | `depot` | | Namespace | `namespace` | | WarpBuild | `warpbuild` | | Ubicloud | `ubicloud` | | RunsOn | `runs-on` | **Blacksmith**: Hosted runners for GitHub Actions. - Labels: `blacksmith-*` - Example: `runs-on: blacksmith-4vcpu-ubuntu-2404` - List price: $0.008/min (as of November 2026) - Links: [Docs](https://docs.blacksmith.sh), [Dashboard](https://app.blacksmith.sh) **Depot**: Hosted runners for GitHub Actions. - Labels: `depot-*` - Example: `runs-on: depot-ubuntu-24.04` - List price: $0.006/min (as of November 2026) - Links: [Docs](https://depot.dev/docs/github-actions/overview), [Dashboard](https://depot.dev/orgs) **Namespace**: Hosted runners for GitHub Actions, sized by runner profile. - Labels: `namespace-profile-*, nscloud-*` - Example: `runs-on: namespace-profile-default` - Links: [Docs](https://namespace.so/docs), [Dashboard](https://cloud.namespace.so) **WarpBuild**: Hosted runners for GitHub Actions. - Labels: `warp-*` - Example: `runs-on: warp-ubuntu-latest-x64-2x` - List price: $0.004/min (as of November 2026) - Links: [Docs](https://docs.warpbuild.com), [Dashboard](https://app.warpbuild.com) **Ubicloud**: Hosted runners for GitHub Actions. - Labels: `ubicloud, ubicloud-*` - Example: `runs-on: ubicloud-standard-2` - List price: $0.00125/min (as of November 2026) - Links: [Dashboard](https://console.ubicloud.com) **RunsOn**: Runners started in your own AWS account. - Labels: `runs-on=/runner=` - Example: `runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64` - Links: [Docs](https://runs-on.com/docs/) RunsOn's label carries the run's id, so write it as an expression: `runs-on: runs-on=${{ github.run_id }}/runner=2cpu-linux-x64`. ## Good to know - Provider-only actions in a job, such as Blacksmith's cache actions, won't work on your machines or GitHub. `init` warns about Blacksmith's. --- # Providers > Where you can route jobs, and the runner services the dashboard recognizes. A provider is somewhere a job can run. You can **route** jobs to some providers. The dashboard **recognizes** many more, so it can show where every job ran and what it cost. ## Where jobs can be routed A relay can send a job to any of these. Each has a step in your [runner order](/docs/concepts/runner-order). - [Self-hosted](/docs/runners/self-hosted): machines you connect with the CLI, and [runners your org already has](/docs/runners/self-hosted#runners-your-org-already-has). - [GitHub-hosted](/docs/runners/github): GitHub's own runners, billed against your plan's minutes. - [Hosted providers](/docs/runners/hosted): Blacksmith, Depot, Namespace, WarpBuild, Ubicloud and RunsOn, each through its own GitHub App. ## Your accounts, your bills You pay each hosted provider yourself. A routed job gets a label, and the provider runs it under your own account and bills you directly. You keep your plans, credits and discounts, and you can drop RelayRunner without changing who you pay. ## Recognized providers The dashboard reads where a job ran from its `runs-on` labels, including jobs you didn't route. It recognizes these providers. Jobs with any other labels show up as Other. The dashboard's **Providers** page shows how much of your CI ran on each, with cost estimates at list prices. | Provider | Labels | List price | | --- | --- | --- | | Blacksmith: Hosted runners for GitHub Actions. | `blacksmith-*` | $0.008/min | | Depot: Hosted runners for GitHub Actions. | `depot-*` | $0.006/min | | Namespace: Hosted runners for GitHub Actions, sized by runner profile. | `namespace-profile-*, nscloud-*` | — | | WarpBuild: Hosted runners for GitHub Actions. | `warp-*` | $0.004/min | | Ubicloud: Hosted runners for GitHub Actions. | `ubicloud, ubicloud-*` | $0.00125/min | | RunsOn: Runners started in your own AWS account. | `runs-on=/runner=` | — | | GitHub: GitHub's own hosted runners, billed per minute in private repos. | `ubuntu-*, windows-*, macos-*, or a larger runner's own name` | — | | Cloudflare: RelayRunner's own runner, a Cloudflare Container started for each job. | `relayrunner-container` | — | List prices for each provider's example runner, as of November 2026. To add another, see [Add a provider](/docs/contribute/add-provider). --- # Your machines > Turn computers you own into GitHub Actions runners with the CLI. Any Mac or Linux computer can take jobs: your laptop, a Mac mini under the desk, an old gaming PC, a home server. Connect it with the CLI and it joins the self-hosted step of every workspace you're in. ## Connect a machine ```sh npx relayrunner connect ``` The first run signs the machine in. After that, the machine is online while `connect` runs, and the terminal shows each job as it runs. Ctrl-C takes it offline. Jobs run in GitHub's official runner, set up fresh for each job. The machine needs no runner setup of its own. Where a job runs depends on the machine: - **On a Mac with Apple silicon**, each job runs in its own Linux VM, away from your files. See [Isolated jobs on a Mac](#isolated-jobs-on-a-mac). - **On Linux or an Intel Mac**, jobs run on the machine itself, as the user who started `connect`. ## Labels Every machine has the label `self-hosted`, plus the OS and architecture its jobs run on: `macOS` or `Linux`, and `ARM64` or `X64`. A Mac that runs jobs in Linux VMs has the label `Linux`, and `macOS` too when you connect it with `--macos`. A relay's `machine` input lists the labels a machine must have, all of them: | `machine:` | Runs on | | ------------- | --------------------------------------------- | | `self-hosted` | Any machine | | `Linux` | Linux machines, and Macs that run jobs in VMs | | `macOS` | Macs that take macOS jobs | | `Linux, X64` | Intel or AMD Linux machines | ## Who can run jobs on it A new machine only runs jobs you started (**only me**). To let teammates' and bots' jobs run on it too, share it with **anyone** in the workspace: ```sh relayrunner share anyone # or only-me ``` You can also change it from the sharing icon on the machine's card on the **Runners** page. Only the machine's owner can change it. "Anyone" only covers jobs from repos the machine's owner can open on GitHub, so sharing a machine never gives it code you couldn't already see. See [Share your team's machines](/docs/guides/team-machines). ## Pause Pausing keeps a machine connected but stops new jobs reaching it. A job it's already running finishes. ```sh relayrunner pause relayrunner resume ``` Press `p` in the `connect` terminal, or use the machine's ⋯ menu on the Runners page. Anyone in the workspace can pause a machine. ## Jobs at once A machine runs up to 2 jobs at once when it has 8 or more CPU cores, otherwise 1. Change it with `relayrunner connect --concurrency `. Each job gets its own VM or its own copy of the runner, so jobs never share a workspace. When every slot is busy, the next job goes to another machine or falls through to the next step in your runner order. With no next step, it waits for a free slot. ## Isolated jobs on a Mac On a Mac with Apple silicon, each job runs in its own Linux VM. The VM sees none of your files, can reach the internet but not your Mac or your local network, and is deleted when the job ends. You need macOS 26 and Apple's [container](https://github.com/apple/container/releases) tool, started once with `container system start`. Without it, `connect` still works: jobs run on the Mac itself, where they can read your files, and `connect` tells you so each time it starts. The first run takes a couple of minutes to prepare the image jobs start from. After that a VM starts in a few seconds. ### What's in the VM The VM has the everyday tools a job written for `ubuntu-latest` expects: Git and Git LFS, Docker with Compose and Buildx, Node with npm, Python with pip, compilers and `make`, `curl`, `wget`, `jq`, `zip` and the `gh` CLI. Jobs with `services:` or `container:` run too. For a specific version of Node or Python, or another language such as Go, Java, Ruby or Rust, use its `setup-*` action as you would on GitHub. The VM is ARM, so a job that needs x64 binaries should stay on `ubuntu-latest`. To start from your own runner image, pass `--image `. Your image is used with the tools it has. ### macOS jobs In a VM, jobs run on Linux, so the Mac's labels are `Linux` and `ARM64` and it takes jobs written for `ubuntu-latest`. To take macOS jobs such as Xcode builds too, add `--macos`: ```sh npx relayrunner connect --macos ``` Linux jobs still run in a VM. macOS jobs run on the Mac itself, as you, so route them only from repos you trust. A job that names neither OS goes by its GitHub fallback, so one written for `ubuntu-latest` runs in the VM. ### The size of each job Each VM gets an even share of the Mac's CPU cores, and of half its memory, so the Mac stays usable while jobs run. A Mac with 14 cores and 36 GB running 2 jobs at once gives each job 7 CPUs and 9 GB. To give every job a fixed size, like a hosted runner's, set it: ```sh npx relayrunner connect --cpus 4 --memory 16 ``` That runs as many 4 CPU, 16 GB jobs at once as fit. `--concurrency` still sets the number yourself. A core on an Apple silicon Mac is faster than a hosted runner's vCPU, so the same size usually finishes sooner here. ### Your network A job can't reach your Mac or your local network. If it needs something there, such as a package registry or a test database, add `--local-network`. The job still can't read your files. This block runs inside the VM, where a job that uses Docker could remove it. For a block no job can remove, run `relayrunner firewall on` once. It asks for your password, stays on after a restart, and `relayrunner firewall off` removes it. While it's on, `--local-network` has no effect. ### Run jobs on the Mac itself `--no-isolated` runs every job on the Mac, as you, with the tools you have installed. Jobs can read your files, so route them only from repos you trust. ```sh npx relayrunner connect --no-isolated ``` ### When something goes wrong `~/.relayrunner/isolated.log` has the details. It records the VM commands and their results, never a job's output or credentials, so it's safe to share. `connect` removes the images it downloaded for older runner versions, so they don't fill your disk. ## Remove a machine Remove it from the Runners page. You can remove your own machines; owners and admins can remove any. When someone leaves a workspace, their machines stay theirs and stop taking its jobs. ## Runners your org already has If you already run self-hosted runners, you can route jobs to them as they are, with no change on the runner's machine. On the **Runners** page, choose **Add runners from GitHub** and pick the ones to add. The list shows runners registered in your organization and runners registered on your private repos. Owners and admins can add them. A runner registered on a repo only takes that repo's jobs. The list checks up to 100 private repos. If you have more, type a repo's name in the dialog to look there. Added runners share the self-hosted step with your connected machines, and are picked the same way: online, idle, and with every label in the relay's `machine` input. Removing one from RelayRunner leaves it registered on GitHub. ## Public repos Machines never run a public repo's jobs. See [Principles](/docs/concepts/principles).