Runs
Reading executions, and the manual upload flow.
View as MarkdownA 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
curl -fsS "https://api.saved.sh/v1/backups/$BID/runs" \
-H "Authorization: Bearer $SAVED_API_KEY"{
"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.
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 for the longer view.
One run
curl -fsS "https://api.saved.sh/v1/runs/$RUN_ID" -H "Authorization: Bearer $SAVED_API_KEY"{
"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.
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.
Manual uploads
Four steps. The bytes never pass through this API.
1. Create the run
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-..."}'{ "run_id": "manual-018f3c2a-...", "expires_at": "2026-08-08T11:14:02Z" }The run waits for the bytes until expires_at. Permission: artifacts:upload.
Only manual backups call this. A local or cloud worker already holds a run ID from the
scheduler and starts at the next step.
2. Ask for an upload target
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}'{ "method": "PUT", "url": "https://...", "expires_at": "2026-08-08T10:14:02Z" }For a large file the response is multipart instead:
{
"method": "MULTIPART",
"upload_id": "2~abc...",
"part_size": 134217728,
"part_count": 40,
"expires_at": "2026-08-08T10:14:02Z"
}3. Upload
Single PUT:
curl -fsS -X PUT --upload-file ./export.tar.gz "$URL"Multipart: request part URLs in batches, PUT each part, and keep every ETag.
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]}'{ "urls": [ { "part_number": 1, "url": "https://..." } ] }Then complete, or abort:
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...\""}]}'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..."}'Abort a failed multipart upload rather than abandoning it. Parts left behind occupy storage until they are cleaned up.
4. Confirm
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.
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.
The checksum is verified on arrival. A mismatch fails the run with ChecksumMismatch rather
than storing a corrupt artifact.
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.