---
title: "Runs"
description: "Reading executions, and the manual upload flow."
url: "https://saved.sh/docs/api/runs"
---

A run is one execution of a backup. Scheduled runs are created by the scheduler; you create
one only when submitting to a `manual` backup.

## Listing [#listing]

```bash
curl -fsS "https://api.saved.sh/v1/backups/$BID/runs" \
  -H "Authorization: Bearer $SAVED_API_KEY"
```

```json
{
  "data": [
    {
      "run_id": "scheduled-018f3c2a-1754640000",
      "workflow_type": "PostgresBackupWorkflow",
      "status": "Completed",
      "started_at": "2026-08-08T02:00:03Z",
      "closed_at": "2026-08-08T02:04:15Z",
      "duration_ms": 252000
    }
  ],
  "next_cursor": null
}
```

Runs are listed **per backup**; there is no workspace-wide run endpoint. Permission:
`backups:read`.

<Callout>
  Run history is retained for **30 days**. An empty list means no history, not that nothing
  ever ran. Artifacts outlive their runs, so use [artifacts](/docs/api/artifacts) for the
  longer view.
</Callout>

## One run [#one-run]

```bash
curl -fsS "https://api.saved.sh/v1/runs/$RUN_ID" -H "Authorization: Bearer $SAVED_API_KEY"
```

```json
{
  "run_id": "scheduled-018f3c2a-1754553600",
  "kind": "local",
  "phases": [
    {
      "name": "produce",
      "workflow_id": "scheduled-018f3c2a-1754553600",
      "status": "Failed",
      "started_at": "2026-08-07T02:00:02Z",
      "closed_at": "2026-08-07T02:00:40Z",
      "duration_ms": 38000,
      "failure": "pg_dump app: FATAL: password authentication failed for user \"backup\"",
      "steps": [
        {
          "name": "DumpPostgresActivity",
          "status": "Failed",
          "attempt": 2,
          "started_at": "2026-08-07T02:00:12Z",
          "closed_at": "2026-08-07T02:00:40Z",
          "duration_ms": 28000,
          "failure": "pg_dump app: FATAL: password authentication failed for user \"backup\""
        }
      ]
    },
    { "name": "post-backup", "workflow_id": "", "status": "not_started", "steps": [] }
  ]
}
```

| Phase         | Runs on                                              |
| ------------- | ---------------------------------------------------- |
| `produce`     | Your worker for a local backup, ours for a cloud one |
| `post-backup` | Ours, always                                         |

`attempt` is the useful field when triaging: a step that failed once and succeeded on retry is
not a failure, while one that burned every attempt with the same message is a
misconfiguration.

<Callout type="warn">
  The run ID is not a UUID. It is `scheduled-<backup-id>-<unix-timestamp>` or
  `manual-<uuidv7>`, and it contains characters that need URL-encoding in some clients. It is
  stable across retries.
</Callout>

## Manual uploads [#manual-uploads]

Four steps. &#x2A;*The bytes never pass through this API.**

### 1. Create the run [#1-create-the-run]

```bash
curl -fsS -X POST https://api.saved.sh/v1/runs \
  -H "Authorization: Bearer $SAVED_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"backup_id": "018f3c2a-..."}'
```

```json
{ "run_id": "manual-018f3c2a-...", "expires_at": "2026-08-08T11:14:02Z" }
```

The run waits for the bytes until `expires_at`. Permission: `artifacts:upload`.

<Callout type="warn">
  Only `manual` backups call this. A local or cloud worker already holds a run ID from the
  scheduler and starts at the next step.
</Callout>

### 2. Ask for an upload target [#2-ask-for-an-upload-target]

```bash
curl -fsS -X POST "https://api.saved.sh/v1/runs/$RUN_ID/upload-url" \
  -H "Authorization: Bearer $SAVED_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"content_length": 5368709120}'
```

```json
{ "method": "PUT", "url": "https://...", "expires_at": "2026-08-08T10:14:02Z" }
```

For a large file the response is multipart instead:

```json
{
  "method": "MULTIPART",
  "upload_id": "2~abc...",
  "part_size": 134217728,
  "part_count": 40,
  "expires_at": "2026-08-08T10:14:02Z"
}
```

### 3. Upload [#3-upload]

Single `PUT`:

```bash
curl -fsS -X PUT --upload-file ./export.tar.gz "$URL"
```

Multipart: request part URLs in batches, `PUT` each part, and keep every `ETag`.

```bash
curl -fsS -X POST "https://api.saved.sh/v1/runs/$RUN_ID/upload-parts" \
  -H "Authorization: Bearer $SAVED_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"upload_id": "2~abc...", "part_numbers": [1,2,3]}'
```

```json
{ "urls": [ { "part_number": 1, "url": "https://..." } ] }
```

Then complete, or abort:

```bash
curl -fsS -X POST "https://api.saved.sh/v1/runs/$RUN_ID/upload-complete" \
  -H "Authorization: Bearer $SAVED_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"upload_id": "2~abc...", "parts": [{"part_number": 1, "etag": "\"9f86...\""}]}'
```

```bash
curl -fsS -X POST "https://api.saved.sh/v1/runs/$RUN_ID/upload-abort" \
  -H "Authorization: Bearer $SAVED_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{"upload_id": "2~abc..."}'
```

<Callout type="warn">
  Abort a failed multipart upload rather than abandoning it. Parts left behind occupy storage
  until they are cleaned up.
</Callout>

### 4. Confirm [#4-confirm]

```bash
curl -fsS -X POST "https://api.saved.sh/v1/runs/$RUN_ID/confirm" \
  -H "Authorization: Bearer $SAVED_API_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
        "checksum": "sha256:9f86d081884c7d65...",
        "size_bytes": 5368709120,
        "filename": "export.tar.gz"
      }'
```

Permission: `artifacts:confirm`, which is **worker-only** for keys created by
`sctl worker provision`. An ordinary API key cannot hold it.

<Callout type="error">
  Confirming is what releases the run. Until then nothing is processed, and an unconfirmed run
  produces no artifact however completely the upload succeeded. The upload goes straight to
  storage, so a completed `PUT` tells us nothing on its own.
</Callout>

The checksum is verified on arrival. A mismatch fails the run with `ChecksumMismatch` rather
than storing a corrupt artifact.

## Statuses [#statuses]

| Status                   | Artifact               |
| ------------------------ | ---------------------- |
| `Running`                | Not yet                |
| `Completed`              | Yes                    |
| `Failed`                 | Usually not. See below |
| `TimedOut`               | No                     |
| `Terminated`, `Canceled` | No                     |

A run that stored **some** copies but not all is `Failed` **with the artifact written**. The
artifact's `locations` say which copies exist. A run that stored nothing fails before the
artifact record is written.

## Next [#next]

<Cards>
  <Card href="/docs/api/artifacts" title="Artifacts" description="What a successful run produced." />

  <Card href="/docs/backups/manual" title="Manual backups" description="The same flow, explained." />

  <Card href="/docs/backups/runs" title="Runs in depth" description="Phases, statuses and history." />
</Cards>
