---
title: "How to back up the output of any script"
description: "Every backup tool has a list of things it supports, and your thing is not on it. The honest answer is an escape hatch. If you can write a script that produces a file, that file can be backed up on a schedule like anything else."
url: "https://saved.sh/blog/back-up-the-output-of-any-script"
date: "2026-08-08"
author: "saved.sh"
tag: "Engineering"
---

Every backup product publishes a list of supported sources, and every list is
missing something you run. An in-house service with a bespoke export. A SaaS with
an API and no integration. A queue, a search index, a licence server, a thing
somebody wrote in 2019 that nobody wants to touch.

The usual answer is "it is on the roadmap". The better answer is an escape hatch.

## The contract [#the-contract]

If you can write a script that produces a file, you have a backup source. The
whole interface is one environment variable:

```bash
#!/usr/bin/env bash
set -euo pipefail

# SAVED_OUTPUT is a path. Write your backup there and exit zero.
curl --fail --silent --show-error \
  -H "Authorization: Bearer $VENDOR_TOKEN" \
  https://api.vendor.example/v1/export \
  > "$SAVED_OUTPUT"
```

That is the entire integration. Point a backup at that script, give it a
schedule, and it now has the same retention, the same encryption, the same
delivery to your own bucket and the same run history as a Postgres source.

## Three rules the contract enforces [#three-rules-the-contract-enforces]

**Write to `$SAVED_OUTPUT`, not stdout.** Anything on stdout is treated as a log,
which means it shows up in the run record where it is useful for debugging and
does not end up inside your backup. A script that prints its data instead of
writing it produces a zero-byte artifact.

**An empty file is a failure, not a success.** If the script exits zero and
`$SAVED_OUTPUT` is empty, the run fails and tells you so, rather than archiving
nothing and going green. This is the single most valuable line in the whole
feature, because "exited zero, produced nothing" is the most common way a
homemade backup lies to you.

**Exit non-zero to fail the run.** Use `set -euo pipefail` and let the shell do
it. A curl that 404s inside a pipe will not fail the script without it, which is
how people end up archiving an HTML error page.

<Aside title="The failure this prevents">
  The classic homemade backup writes `curl ... > backup.json` and never checks
  anything. When the token expires, curl writes a JSON error body, the file has a
  plausible size, and the cron job is green. Six weeks later somebody tries to use
  it. The `--fail` flag and the empty-output check are what turn that into an alert
  on day one.
</Aside>

## What it is good for [#what-it-is-good-for]

The pattern is worth reaching for in more cases than people expect:

| Situation                                  | The script                                                      |
| ------------------------------------------ | --------------------------------------------------------------- |
| SaaS with an export API and no integration | `curl` the export endpoint                                      |
| A database we do not support               | Its own dump tool, redirected to `$SAVED_OUTPUT`                |
| Several things that belong together        | `tar` them into one artifact                                    |
| A dump that needs pre-processing           | Dump, filter out the two tables you cannot store, then write    |
| Compliance evidence                        | Run the report, save the output, keep it under a retention lock |

The fourth row is the interesting one. Because you control the script, you can
strip what must not be stored *before* it is ever archived, which is much easier
than deleting it from artifacts afterwards.

<Stats>
  <Stat value="1" unit="env var" label="The entire interface" />

  <Stat value="0" label="Integrations you have to wait for" />

  <Stat value="0" unit="bytes" label="What silently passes as success" />
</Stats>

## Where it runs, and why that matters [#where-it-runs-and-why-that-matters]

Script sources run on a worker on your own machine, not on ours. That is a
deliberate constraint rather than a limitation we have not got to yet.

A script is arbitrary code with access to whatever credentials it needs. Running
that on our infrastructure would mean you hand us the credentials and trust us to
execute your code, which is a much larger ask than "back up this database". So
the script runs where you already have the access, and we never see the token,
the code, or the bytes.

## The honest caveat [#the-honest-caveat]

An escape hatch is not the same as an integration. You own the script: when the
vendor changes their export endpoint, your backup breaks and you fix it.

What you get in exchange is that you are never blocked. Everything around the
script, the scheduling, retries, retention, encryption, delivery to your own
bucket and a record of what each run produced, is the same machinery the built-in
sources use. Only the fifteen lines that produce the file are yours.
