Snapshot, fork and restore sandboxes
Save a sandbox’s workspace, roll it back after a bad change, start new sandboxes from it, or split a running sandbox into copies that each try something different.
Snapshot, break, revert
import { Sandbox } from "@tangle-network/sandbox";
const client = new Sandbox({
apiKey: process.env.TANGLE_API_KEY!,
baseUrl: "https://sandbox.tangle.tools",
});
// A snapshot is the whole workspace. Revert to it and the damage is gone.
const box = await client.create({
git: { url: "https://github.com/tangle-network/sandbox-demo" },
});
try {
const clean = await box.snapshot({ tags: ["clean"] });
await box.exec("rm -rf packages test");
console.log("after", (await box.exec("ls")).stdout.trim().split("\n"));
await box.revertToSnapshot(clean.snapshotId);
console.log("reverted", (await box.exec("ls")).stdout.trim().split("\n"));
} finally {
await box.delete();
}snapshot() captures the whole workspace and returns { snapshotId, createdAt, sizeBytes, tags }. Snapshots of a single path are not supported; take a whole snapshot and restore the paths you need.
Restore all or part of a workspace
await box.revertToSnapshot(snapshotId); // the whole workspace, in place
await box.restorePaths(snapshotId, ["src", "package.json"]); // only these pathsrestorePaths replaces the listed workspace-relative paths and leaves every other file as it is.
Start a new sandbox from a snapshot
const copy = await client.create({
fromSnapshot: snapshotId,
fromSandboxId: box.id,
});fromSandboxId is required with fromSnapshot. The source sandbox is untouched. In the CLI, tangle snapshot restore <sandbox-id> <snapshot-id> creates the new sandbox and waits for it to run; the first argument must be a sandbox id, not a name.
To publish a snapshot so others can start from it, see Templates and images.
List and delete snapshots
for (const s of await box.listSnapshots()) console.log(s.snapshotId, s.tags, s.sizeBytes);
const { outcome } = await box.deleteSnapshot(snapshotId);
console.log(outcome); // "deleted", "already_absent" or "unknown"tangle snapshot list demo
tangle snapshot delete demo <snapshot-id>Snapshot without waiting
box.snapshot({ wait: false }) returns a SandboxOperation once the snapshot is accepted. await op.wait() resolves when it finishes. Pass idempotencyKey so a retried request answers with the same snapshot. In the CLI, tangle snapshot create --no-wait prints the operation id and status URL.
Keep data after delete or share it between sandboxes
Write snapshots to your own S3, GCS or R2 bucket, and they stay there until you delete them, whatever happens to the sandbox.
const storage = {
type: "s3" as const,
bucket: "acme-sandbox-snapshots",
region: "us-east-1",
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
},
prefix: "checkout-service/",
};
const box = await client.create({ storage });
await box.exec("npm ci");
await box.snapshot({ storage, tags: ["deps-installed"] });
await box.delete();
// Later, in a new sandbox:
const next = await client.create({ storage });
await next.restoreFromStorage(storage); // the latest snapshot in the bucket, or nullstorage field | Meaning |
|---|---|
type | "s3", "gcs" or "r2". |
bucket | Bucket name. |
endpoint | Required for R2, optional for S3 and GCS. |
region | Bucket region. |
credentials | { accessKeyId, secretAccessKey }. Required. |
prefix | Path inside the bucket. |
listSnapshots(storage) lists what is in your bucket. A snapshot to your own storage refuses idempotencyKey and wait: false. The CLI takes the same settings as --storage-type, --storage-bucket, --storage-region, --storage-endpoint, --storage-prefix, --storage-access-key-id and --storage-secret-access-key-env on tangle sandbox create.
Within the platform, any sandbox you own can start from another sandbox’s snapshot with fromSnapshot and fromSandboxId.
Branch a running sandbox
branch(count) makes copy-on-write copies of a running sandbox. The parent keeps running, and each child starts with the parent’s files.
import { Sandbox } from "@tangle-network/sandbox";
const client = new Sandbox({
apiKey: process.env.TANGLE_API_KEY!,
baseUrl: "https://sandbox.tangle.tools",
});
const parent = await client.create();
try {
await parent.exec("echo 'shared setup' > base.txt");
const [a, b] = await parent.branch(2);
try {
await a.exec("echo 'child a' >> base.txt");
await b.exec("echo 'child b' >> base.txt");
for (const child of [a, b]) {
const { stdout } = await child.exec("cat base.txt");
console.log(child.id, stdout.trim().split("\n"));
}
} finally {
await Promise.all([a.delete(), b.delete()]);
}
} finally {
await parent.delete();
}What a child inherits depends on the runtime:
| Runtime | The child gets |
|---|---|
firecracker | The parent’s disk and memory, so running processes carry over. |
docker | A copy-on-write fork of the disk. Processes start fresh. |
fork(count) does the same and reports the whole fan-out: { children, requestedCount, materializedCount, complete }. complete: false means fewer children started than you asked for. Pass idempotencyKey so a retry returns the same children instead of making more.
const forked = await parent.fork(5, { idempotencyKey: "run-42", metadata: { experiment: "a" } });
if (!forked.complete) console.warn(`${forked.materializedCount} of ${forked.requestedCount}`);A fan-out larger than the host can hold fails with BRANCH_FANOUT_EXCEEDS_HOST, and the error’s maxChildren says how many fit. The allowed count is on Limits.
A parent with live children cannot be deleted: the delete answers 409 CHILDREN_EXIST. Delete the children first, or delete the lineage with cascade=true through REST or client.deleteMatching(filter, { cascade: true }). See Delete.
tangle branch create demo --count 2 --idempotency-key run-42Python and REST
Python has snapshot(SnapshotOptions(tags, storage)), list_snapshots(), restore_paths(snapshot_id, paths), and from_snapshot with from_sandbox_id on create. It has no revert, snapshot delete, restore from storage, branch or fork. Call those routes with client.fetch, which returns the raw response and does not raise on an error status:
client.fetch("POST", f"/v1/sandboxes/{box.id}/snapshots/{snapshot_id}/restore", json={})
client.fetch("DELETE", f"/v1/sandboxes/{box.id}/snapshots/{snapshot_id}")
client.fetch("POST", f"/v1/sandboxes/{box.id}/branch", json={"count": 2}, headers={"Idempotency-Key": "run-42"})