---
title: "Workers"
description: "Provisioning and rotating worker credentials."
url: "https://saved.sh/docs/api/workers"
---

A worker is a **credential**, not a machine. These endpoints manage the credential; the
process that uses it is [installed separately](/docs/workers/install).

`workers:*` is human-only, so a machine key cannot provision or rotate a worker.

## Create [#create]

```bash
curl -fsS -X POST https://api.saved.sh/v1/workers \
  -H "Authorization: Bearer $TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"name": "prod-worker-1"}'
```

```json
{
  "id": "0f2c9a1e-...",
  "name": "prod-worker-1",
  "secret": "sk_live_...",
  "created_at": "2026-08-08T09:14:02Z",
  "updated_at": "2026-08-08T09:14:02Z"
}
```

Permission: `workers:write`. Returns `201`.

<Callout type="error">
  **`secret` appears here and nowhere else.** It goes into that machine's `config.yaml` as
  `token`. If you lose it, [rotate](#rotate) rather than deleting the worker.
</Callout>

The key is created carrying exactly `artifacts:upload` and `artifacts:confirm`. You cannot
choose its permissions, and there is no field to try.

## List [#list]

```bash
curl -fsS https://api.saved.sh/v1/workers -H "Authorization: Bearer $TOKEN"
```

```json
{
  "data": [
    {
      "id": "0f2c9a1e-...",
      "name": "prod-worker-1",
      "instances": 1,
      "last_seen": "2026-08-08T09:41:12Z",
      "created_at": "2026-08-08T09:14:02Z",
      "updated_at": "2026-08-08T09:14:02Z"
    }
  ],
  "next_cursor": null
}
```

Permission: `workers:read`.

### Presence [#presence]

`instances` and `last_seen` are read **live from the polling connections**, not from a
heartbeat we store.

<Callout type="warn">
  **Connected is trustworthy; disconnected lags.** Poller records expire on a TTL, so a worker
  that died thirty seconds ago can still report as present for a few minutes. Never render this
  as a definite red light: show "last seen", and treat a stale value as unknown rather than
  down.
</Callout>

`instances` above 1 is worth investigating. Several processes may share one credential and all
poll the same queue, but a run's steps hand each other a path to a local file, so splitting
them across machines breaks the run. See
[one process per credential](/docs/workers/lifecycle#one-process-per-credential).

`instances` is `null` when presence could not be read, which is not the same as `0`.

## Get [#get]

```bash
curl -fsS "https://api.saved.sh/v1/workers/$WORKER_ID" -H "Authorization: Bearer $TOKEN"
```

Permission: `workers:read`. Presence fields are not included on the single-worker read.

## Rotate [#rotate]

```bash
curl -fsS -X POST "https://api.saved.sh/v1/workers/$WORKER_ID/rotate" \
  -H "Authorization: Bearer $TOKEN"
```

```json
{ "id": "0f2c9a1e-...", "name": "prod-worker-1", "secret": "sk_live_...", "...": "..." }
```

Permission: `workers:write`.

**The worker ID is unchanged**, so every backup assigned to it keeps working once the new key
is deployed. This is the answer to a leaked key, a decommissioned host, or ordinary hygiene.

<Callout type="warn">
  The old key is revoked the moment the new one is issued. Any process still holding it stops
  polling, so expect a short gap: rotate, write the new key into `config.yaml`, restart the
  worker. A run already in flight is retried when the worker returns, because the run belongs
  to the queue rather than the connection.
</Callout>

## Delete [#delete]

```bash
curl -fsS -X DELETE "https://api.saved.sh/v1/workers/$WORKER_ID" -H "Authorization: Bearer $TOKEN"
```

Permission: `workers:revoke`, which is a &#x2A;*separate permission from `workers:write`**. Returns
`204`.

Deletion revokes the key permanently. Backups assigned to the worker are left with nowhere to
run: their scheduled runs queue on a task queue nothing polls, silently.

|                  | Rotate            | Delete              |
| ---------------- | ----------------- | ------------------- |
| Worker ID        | Unchanged         | Gone                |
| Assigned backups | Keep working      | Have nowhere to run |
| Reversible       | No new key needed | No                  |

Reassign or pause those backups before deleting the worker they point at.

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

| Permission          | Allows                                                     |
| ------------------- | ---------------------------------------------------------- |
| `artifacts:upload`  | Request 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 entire surface. A worker key cannot read backup definitions, list artifacts,
download anything, or manage workers. On the orchestration side it is bound to one namespace
and one task queue, its own.

`artifacts:confirm` is refused on an ordinary API key, which makes it the worker/automation
discriminator.

## Quotas [#quotas]

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

## Next [#next]

<Cards>
  <Card href="/docs/workers/registration" title="Registration" description="The same flow, in depth." />

  <Card href="/docs/workers/configuration" title="Worker configuration" description="Where the key goes." />

  <Card href="/docs/api/api-keys" title="API keys" description="The other machine credential." />
</Cards>
