Skip to content

Runners

A runner is whatever executes a job. Two kinds are yours to use: the hosted pool, which is the default and needs no setup, and self-hosted runners — the neo CLI acting as an agent on your own machine, either standing by for work (neo runner start) or for one push at a time (neo push --runner).

Hosted runners are neosource’s own pool, and they are what runs your jobs. You do not configure or provision anything: push a workflow and jobs are scheduled onto the pool.

Every job runs in the same canonical environment, with tools added by packages:. See The job environment — that page is the one that determines what your job can actually do.

This is the most common misunderstanding coming from GitHub Actions.

runs-on is a scheduling label. It does not choose an environment. There is no runs-on value that gives your job a different base image or a different operating system.

jobs:
a:
runs-on: ubuntu-latest # a label
packages: [nodejs_20] # this is what determines the environment
b:
runs-on: nix # requests the strict Nix form
packages: [nodejs_20] # same tools either way

runs-on: nix is now a scheduling alias like any other label. packages: and flake: are job-level keys honoured on every runs-on; the only rule is that a job may not declare both.

A job that declares no packages: and no flake: runs in the canonical container image. A job that declares either runs under Nix. That decision comes entirely from the job’s own keys, never from its runs-on label.

You can attach your own hardware to the pool. Registration, job claim by long-poll, lease heartbeat and completion all work, driven by an agent that is embedded in the neo CLI — there is no second binary to install:

Terminal window
neo runner start

Zero-arg inside a pushed checkout with a stored login. start registers first if this machine has no persisted credentials yet, so it is the only command most people need; neo runner register exists for scripts that want registration as its own step. Credentials persist to ~/.config/neosource/runner.json. Get the CLI from Install the CLI.

Two flags decide what the machine will accept:

  • --labels (default self-hosted,linux,x64) is what the runner advertises for runs-on matching. --serve-default-labels also advertises the hosted labels (ubuntu-latest, nix) so unmodified workflows can land here.
  • Registration is actor-bound by default: it claims only jobs from runs you triggered — your pushes, your PRs, your dispatches. --workspace-wide claims any job in the workspace and requires workspace admin, which the server enforces.

Two operational rules worth knowing:

  • A runner registration is swept after 24 hours without claiming a job, so a machine left off for a day comes back to a dead credential. neo runner start handles this for you, whether it was running the whole time or has just been restarted: when the scheduler stops recognising the credential it drops the claim loop, registers again, and resumes. You do not have to re-run neo runner register by hand. The one limit is a bounded retry budget — five revocations in quick succession and the agent gives up and exits non-zero rather than churning against a scheduler that keeps rejecting it.
  • The hosted pool stays the fallback. A self-hosted machine that dies mid-run does not strand the job — the lease expires and the job is requeued.

Running your own push’s CI on your own machine while the server still records the run — real run records, real logs, visible in the UI, just executed locally:

Terminal window
neo push --runner

An ephemeral actor-bound session runner registers before the push, claims that push’s jobs, streams their logs into your terminal, and exits when the runs finish. The hosted pool is the fallback if this machine dies mid-run.

Runs, jobs, and logs are all readable through the API and the Actions view in the web UI. The endpoint list is in How CI works here.