---
title: "Registration"
description: "Provisioning a worker credential and attaching it to backups."
url: "https://saved.sh/docs/workers/registration"
---

Provisioning creates two things: a **worker ID** that backups point at, and a **key** that a
process uses to claim work. The ID is permanent and public. The key is shown once.

Nothing about this step touches a machine. You can provision a worker months before you
install anything, and the same worker survives the host being rebuilt.

## Provision [#provision]

```bash
sctl worker provision prod-worker-1
```

```
Provisioned worker "prod-worker-1" (0f2c9a1e-...).

  Key (copy now, not shown again):
  sk_live_...
```

The dashboard does the same thing under **Workers → New worker**, and generates a
`config.yaml` ready to paste.

<Callout type="error">
  The key is shown once and is not recoverable. It is what lets a machine claim work in your
  workspace, so treat it like a database password. If you lose it, or it leaks,
  [rotate](#rotation-versus-deletion) rather than delete.
</Callout>

Names must be 1 to 100 characters. They are for you, not for us: the ID is what everything
else refers to.

```bash
sctl worker list
```

```
NAME            ID                                    CREATED               UPDATED
prod-worker-1   0f2c9a1e-...                          2026-08-08T09:14:02Z  2026-08-08T09:14:02Z
```

## What the key authorizes [#what-the-key-authorizes]

The worker key is an org-scoped machine credential, and it carries exactly two permissions:

| Permission          | What it allows                                             |
| ------------------- | ---------------------------------------------------------- |
| `artifacts:upload`  | Ask for a presigned upload URL for one of its own runs     |
| `artifacts:confirm` | Report the checksum, size and filename of what it uploaded |

That is the whole surface. A worker key **cannot** read your backups, list your artifacts,
download anything, create or delete definitions, read the audit log, touch billing, or see
another workspace. It also cannot manage workers, including itself.

On the orchestration side the same key is bound to one namespace, your workspace, and to one
task queue, its own worker ID. It may poll that queue and report results. It cannot start
work, administer anything, or see another worker's queue, let alone another workspace's.

<Callout>
  This is why the worker is safe to run on a machine you would not trust with an API key. The
  worst a stolen worker key can do is upload garbage to that worker's own runs and read
  whatever the config file already gave it. That second half is the real risk, so protect the
  file, not just the key.
</Callout>

## Assign backups to a worker [#assign-backups-to-a-worker]

A `local` backup **must** name a worker. That is what decides which machine the run lands on,
and it is not optional, because no other machine has the credentials.

```yaml title="saved.yaml"
workers:
  - name: prod-worker-1

backups:
  - name: prod-db
    kind: local
    source_type: postgres
    worker: prod-worker-1
    schedule: "0 2 * * *"
```

```bash
sctl apply -f saved.yaml
```

The `workers:` block declares that the name should exist. `apply` will refuse a backup whose
`worker:` is neither declared in the file nor already provisioned in the workspace, rather
than creating a schedule that fires into nothing.

Two rules follow from routing being per-worker:

* **Runs are never redistributed.** If the assigned worker is offline, its runs queue until
  it comes back. Another worker will not pick them up, because it does not have the
  credentials.
* **Changing a backup's worker recreates its schedule.** Expect the change to take effect
  from the next fire, not retroactively.

The backup ID also has to appear in that worker's own `config.yaml`, under `backups:`, or the
run fails with `ConfigDrift`. See [Configuration](/docs/workers/configuration#per-backup-sources).

## Presence [#presence]

Worker presence is read live from the polling connections, not from a heartbeat we store.

```bash
sctl worker list
```

The dashboard shows an instance count and a **last seen** time per worker. Read them exactly
as written:

* **Connected is trustworthy.** If it reports two instances, two processes are polling right
  now.
* **Disconnected lags.** Poller records expire on a TTL, so a worker that died thirty seconds
  ago can still look present for a few minutes. Nothing here is a definite red light.

An instance count above one is worth investigating. It usually means a second copy of the
same config is running somewhere, which is
[a real problem](/docs/workers/lifecycle#one-process-per-credential).

## Rotation versus deletion [#rotation-versus-deletion]

These are not variations on the same action.

|                        | `sctl worker rotate`                              | `sctl worker delete`   |
| ---------------------- | ------------------------------------------------- | ---------------------- |
| Worker ID              | Unchanged                                         | Gone                   |
| Old key                | Revoked immediately                               | Revoked immediately    |
| New key                | Printed once                                      | There is none          |
| Backups assigned to it | Keep working once the new key is deployed         | Have nowhere to run    |
| Running processes      | Disconnect at the next poll and must be restarted | Disconnect permanently |

```bash
sctl worker rotate 0f2c9a1e-...
```

Rotation is the answer to almost everything: a leaked key, a decommissioned host, an
employee who had access to the config file, or an ordinary hygiene schedule. Deletion is for
a worker you are genuinely finished with.

<Callout type="warn">
  Rotation revokes the old key the moment the new one is issued. Any process still holding the
  old key stops being able to poll. Plan for a short gap: rotate, write the new key into
  `config.yaml`, restart the worker. A run that was already in flight will be retried once the
  worker is back, because the run belongs to the queue, not to the connection.
</Callout>

## Limits [#limits]

| Plan  | Workers per workspace |
| ----- | --------------------- |
| Trial | 1                     |
| Paid  | 10                    |

Provisioning past the limit fails with a quota error rather than silently succeeding. See
[Limits](/docs/reference/limits).

## Next [#next]

<Cards>
  <Card href="/docs/workers/configuration" title="Configuration" description="Put the key in a config file, along with the sources this worker serves." />

  <Card href="/docs/workers/install" title="Install" description="Get the binary onto the host." />
</Cards>
