runs-on routes a job to a runner that has all the labels listed. A runner gives itself three read-only labels, self-hosted, its operating system and its architecture, and --labels adds custom ones. Between registration and run.sh, the API showed the runner offline:
gh api repos/{owner}/{repo}/actions/runners --jq '.runners[] |
"\(.name) \(.status) busy=\(.busy) [\([.labels[] | "\(.name):\(.type)"] | join(", "))]"'l1-booknest-runner offline busy=false [self-hosted:read-only, Linux:read-only, X64:read-only, l1-booknest-demo:custom]
The demo job says runs-on: [self-hosted, l1-booknest-demo], so it can land only on this runner. runs-on: self-hosted alone would match any of your runners, which is rarely what you mean; name a label that describes a capability (gpu, staging-network).
Runner groups are a second routing layer for organizations and enterprises only (not run here). Every organization has a default group; on the Team plan and above owners create more, each listing the repositories allowed to use it, and by default only private repositories may use a group's runners. With runs-on: {group: staging-runners, labels: [linux]} a job must match both. A personal account such as BookNest's has repository-level runners, with labels as the only routing.