---
title: Scale sets
description: The registration between a runner class and one Sylphx Runners installation, what it reports, and how to steer or delete it.
type: how-to
product: runners
summary: Register a class on your installation, then read and steer it.
updated: 2026-10-01
order: 0
---

<Callout tone="note" title="Today this runs on the management API">
Self-hosted runners are registered with `POST /v1/runners` on the management API; see the [Runners quickstart](/docs/runners/quickstart). The `scale_sets` calls below are not served at api.sylphx.com.
</Callout>

A ScaleSet registers one runner class on one Sylphx Runners installation. It is
the whole of the setup: once the registration is Ready, jobs that ask for that
class are answered without anyone creating a runner by hand.

## The spec

Four fields decide what is registered, and two of them are required:

<PropertyTable
	properties={[
		{
			name: 'connection',
			type: 'string',
			description: 'The Sylphx Runners installation Connection. Required.',
			required: true,
		},
		{
			name: 'runner_class',
			type: 'string',
			description: 'The class, sylphx-<os>-<size>, for example sylphx-linux-standard. It is the label a workflow asks for. Required.',
			required: true,
		},
		{
			name: 'runner_group',
			type: 'string',
			description: 'The forge runner group to register in; the installation’s default group by default.',
		},
		{
			name: 'max_runners',
			type: 'int32',
			description: 'Concurrent runners at most; 0 means the plan’s limit.',
		},
	]}
/>

`runner_class` is the half a workflow sees: it is the label a workflow asks
for, so two scale sets on one installation are told apart by the class they
register and the group they register it in. `max_runners` is the cap on how
many of that class run at once, and zero leaves the cap to the plan.

## The session between the installation and Runners

The registration is not a queue. Runners holds the message session with the
installation with one fenced holder, and the forge matches jobs to its
runners; there is no list of pending jobs on our side, so nothing can be
delivered twice by being on two lists at once.

Two status fields are about that session rather than about runners:

<PropertyTable
	properties={[
		{
			name: 'registration',
			type: 'string',
			description: 'The forge’s id of the registered scale set.',
		},
		{
			name: 'session_region',
			type: 'string',
			description: 'The region whose holder owns the message session.',
		},
	]}
/>

`registration` is the proof the scale set exists at the installation: it is the
forge's own id for it, so a registration that never landed has none.
`session_region` names the region holding the session, and it moves when the
holder does — which is what keeps a lost holder from taking the session with
it.

## Reading its health

<PropertyTable
	properties={[
		{
			name: 'conditions',
			type: 'Condition[]',
			description: 'Ready, Reconciling, Stalled.',
		},
		{
			name: 'observed_generation',
			type: 'int64',
			description: 'The generation this status was computed from.',
		},
		{
			name: 'busy_runners',
			type: 'int32',
			description: 'Runners running a job.',
		},
		{
			name: 'idle_runners',
			type: 'int32',
			description: 'Runners waiting for a job.',
		},
	]}
/>

The two counters are the live answer to whether the registration is doing
anything: busy is work in flight, idle is machines waiting for a job. A
condition that is false carries a reason and a severity, so a scale set that
is reconciling reads differently from one that is stalled.

## Steering it

`update` writes the spec fields a registration is steered with — the runner
group, the concurrency cap and the caller-writable metadata:

```bash
sylphx runners scale-sets update orgs/acme/scale_sets/scale-set
```

```bash
curl -X PATCH "https://api.sylphx.com/v1/orgs/acme/scale_sets/scale-set" \
  -H "Authorization: Bearer $SYLPHX_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"name":"orgs/acme/scale_sets/scale-set","spec":{"connection":"…","runner_class":"…"}}'
```

`update_mask` names the fields to write, and an unset mask writes every
populated field. `allow_missing` turns the call into a declarative upsert: the
Resource is created when it does not exist. `validate_only` runs every check
and writes nothing.

<Callout tone="note" title="A mask keeps an update narrow">
`update_mask` is what stops an update from writing fields you did not mean to
write: unset, it writes every populated field, `runner_class` and `connection`
included. The CLI’s `update` offers the runner group, the concurrency cap and
the caller-writable metadata — the fields a live registration is steered with.
</Callout>

## Deleting it

`delete` is `destructive`, so the CLI asks before it runs and takes `--yes`:

```bash
sylphx runners scale-sets delete orgs/acme/scale_sets/scale-set --yes
```

Without `force`, a scale set that still has children fails with
`FAILED_PRECONDITION`; `force` also deletes every child Resource. `etag`
deletes only if the current etag matches, and `allow_missing` succeeds when
the scale set is already gone.

Reading a scale set needs `runners:read`; `create`, `update` and `delete` need
`runners:write`. The whole collection, method by method, is in
[the scale_sets reference](/docs/api/scale_sets).

<RelatedDocs
	links={[
		{
			href: '/docs/api/scale_sets/update',
			label: 'update',
			description: 'Every field, the scope, and the examples.',
		},
		{
			href: '/docs/api/scale_sets/delete',
			label: 'delete',
			description: 'The etag, the two flags, and the errors.',
		},
		{
			href: '/docs/runners/runner-lifecycle',
			label: 'Runner lifecycle',
			description: 'The one machine a single job is given.',
		},
		{
			href: '/docs/cli/scale_sets',
			label: 'The CLI commands',
			description: 'The same calls as sylphx runners scale-sets.',
		},
	]}
/>
