Snapshot after npm install, then fork 10 copies
Install dependencies once in a sandbox, take a snapshot of it while it runs, and start ten parallel attempts from that snapshot, in TypeScript, Python or the CLI.
Also as markdown: /docs/guides/snapshot-and-fork.md
npm install is the slow, identical part of every attempt an agent makes. Run it once, take a snapshot of the sandbox as it is at that moment, and start every attempt from the snapshot: ten copies, each with node_modules already in place, each free to change its own files without touching the others.
The short answer#
Install once in one sandbox, snapshot it while it runs, and start ten sandboxes from the snapshot. On Promigence that is client.sandboxes.snapshot(id) followed by Sandbox.fork(snapshotId, { count: 10 }) in TypeScript, the same two calls in Python, or promigence sandbox snapshot and promigence run --count 10 in a shell. Every copy starts with the files the sandbox had when you took the snapshot.
In TypeScript#
It has to run as an ES module, for the top-level await. The base sandbox already has Node.js with npm, and git (promigence templates show base lists everything in it).
import { PromigenceClient, Sandbox } from "@promigence/sdk";
const client = new PromigenceClient(); // reads PROMIGENCE_API_KEY, or the key signup stored
const REPO = "https://github.com/your-org/your-app";
// 1. One sandbox, and the slow part, once
const setup = await Sandbox.create({ client, spendCap: 1, timeoutMs: 30 * 60_000 });
await setup.commands.run(`git clone --depth 1 ${REPO} /work/app && cd /work/app && npm install`, {
timeoutMs: 15 * 60_000,
});
// 2. A snapshot of the sandbox as it is now
const snap = await client.sandboxes.snapshot(setup.sandboxId, { alias: "app-installed", moveAlias: true });
await setup.kill();
// 3. Ten copies, each with node_modules already installed
const copies = await Sandbox.fork(snap.snapshot_id, { client, count: 10, spendCap: 3, timeoutMs: 20 * 60_000 });
// 4. One attempt per copy, all at once
const results = await Promise.all(
copies.map(async (sbx, i) => {
const r = await sbx.commands.run("cd /work/app && node agent.js && npm test", {
envs: { ATTEMPT: String(i) },
timeoutMs: 15 * 60_000,
throwOnExit: false,
});
return { sbx, attempt: i, passed: r.exitCode === 0 };
}),
);
const winner = results.find((r) => r.passed);
console.log(winner ? `attempt ${winner.attempt} passed in ${winner.sbx.sandboxId}` : "no attempt passed");
await Promise.all(results.filter((r) => r !== winner).map((r) => r.sbx.kill()));
Language: typescriptnode agent.js stands for your agent's attempt; ATTEMPT tells each copy which one it is, so it can vary a seed or a prompt. The winner keeps running until you kill it or it reaches its timeout, so you can read its diff with sbx.commands.run("git -C /work/app diff") before you end it.
spendCap: 3 caps what the ten copies may spend together: ten small sandboxes for up to 20 minutes cost at most 10 × 1,200 × $0.000046 = $0.55 at list, so the cap leaves room. A sandbox that reaches its cap is paused with its files kept, and if your script dies halfway, every copy still ends at its 20-minute timeout.
In Python#
The same four steps, with a thread per attempt:
from concurrent.futures import ThreadPoolExecutor
from typing import List
from promigence import Promigence, Sandbox
REPO = "https://github.com/your-org/your-app"
with Promigence() as client: # reads PROMIGENCE_API_KEY
setup = Sandbox.create(client=client, spend_cap=1, timeout_ms=30 * 60_000)
setup.commands.run(f"git clone --depth 1 {REPO} /work/app && cd /work/app && npm install", timeout_ms=15 * 60_000)
snap = client.sandboxes.snapshot(setup.sandbox_id, alias="app-installed", move_alias=True)
setup.kill()
copies: List[Sandbox] = Sandbox.fork(
snap["snapshot_id"], client=client, count=10, spend_cap=3, timeout_ms=20 * 60_000,
)
def attempt(i: int) -> bool:
result = copies[i].commands.run(
"cd /work/app && node agent.js && npm test",
envs={"ATTEMPT": str(i)}, timeout_ms=15 * 60_000, throw_on_exit=False,
)
return result.exit_code == 0
with ThreadPoolExecutor(max_workers=10) as pool:
passed = list(pool.map(attempt, range(10)))
winner = passed.index(True) if True in passed else None
print(f"attempt {winner} passed" if winner is not None else "no attempt passed")
for i, sbx in enumerate(copies):
if i != winner:
sbx.kill()
Language: pythonFrom a shell#
--json makes every command print one JSON document, so a script can pick out the ids:
SID=$(promigence fork --count 1 --cap 1 --timeout 30m --fresh --json | jq -r '.sandboxes[0].sandbox_id')
promigence exec --sandbox "$SID" --timeout 15m -- \
sh -c "git clone --depth 1 https://github.com/your-org/your-app /work/app && cd /work/app && npm install"
promigence sandbox snapshot "$SID" --alias app-installed --move-alias
promigence sandbox kill "$SID"
promigence run --snapshot app-installed --count 10 --cap 3 --timeout 20m \
-- sh -c 'cd /work/app && ATTEMPT=$PROMIGENCE_INDEX node agent.js && npm test'
Language: bashpromigence run starts the ten copies, runs the attempt in each with PROMIGENCE_INDEX set from 0 to 9, and prints a validity report that says which attempts passed and whether any failure was the environment's or the attempt's.
What the snapshot keeps, and what to watch#
The sandbox you snapshot keeps running. Its files go into the snapshot, node_modules included, and every copy starts with them. Commands started in the background (background: true, or exec --background) are ended to take the snapshot, so start long-lived processes again in the copies.
Three details save a debugging session:
sbx.fork()on a sandbox object never copies your install: after any command it asks forfromOrigin: trueand then copies the snapshot the sandbox started from. To copy it as it is now, take a snapshot first and fork that.- A sandbox that was given a secret is refused a snapshot unless you force it, because the snapshot and every copy could then contain the value. So install first, and give the secret to the copies (
secretsonSandbox.fork). - An alias names one snapshot in your organization.
moveAlias(--move-alias) takes it from the snapshot that has it, so the script can run again.
When every task installs the same dependencies, build the snapshot from a recipe instead and let a verify command check it. It is stored only if the verify command exits 0, the build is billed per second while it runs, up to $0.17 at the default small size (quickstart), and a connected GitHub repository can rebuild it on every push:
promigence snapshot create --image node:22 \
--setup "git clone https://github.com/your-org/your-app /work/app && git -C /work/app checkout <40-char sha>" \
--setup "cd /work/app && npm ci" \
--verify "cd /work/app && npm test" \
--alias app --wait --wait-timeout 10m
Language: bashA build that fails its verify command is billed for the time it ran, and the sandboxes started from the snapshot pay no further build charge.
At a glance#
| Snapshot of a running sandbox | Snapshot built from a recipe | |
|---|---|---|
| Command | promigence sandbox snapshot SID, client.sandboxes.snapshot(id) | promigence snapshot create --image REF --setup CMD --verify CMD |
| Starts from | Whatever the sandbox holds at that moment | An image or a Dockerfile, plus your setup steps |
| Checked by a verify command | No | Yes: stored only if the command exits 0 |
| Best for | Branching a session mid-task, retries from one point | An environment many runs share, rebuilt per commit |
| Background commands | Ended to take the snapshot | A --start command runs in every copy |
| Copies | Sandbox.fork(id, { count }), promigence run --snapshot NAME --count N | The same |
FAQ#
How do you snapshot a sandbox after npm install and fork it for 10 parallel attempts?#
In one sandbox, run npm install, take a snapshot with client.sandboxes.snapshot(sandboxId), then start ten copies with Sandbox.fork(snapshotId, { count: 10 }). The copies start with node_modules already installed, and each runs its own attempt. The full program is above in TypeScript, in Python and as CLI commands.
Which sandbox platforms let me snapshot and fork a running agent environment?#
Promigence is one: promigence sandbox snapshot takes a snapshot of a running sandbox without stopping it, and any number of sandboxes can start from the result. When you compare platforms, ask whether a snapshot can come from a running sandbox or only from a template build, whether copies start with the files you captured, what happens to running processes, and whether a secret in the sandbox can end up in the snapshot.
Does the original sandbox stop when I take the snapshot?#
No. It keeps running, and you get an ordinary snapshot id you can start sandboxes from. Commands started in the background are ended to take the snapshot; the CLI and the SDK say how many.
Do the ten copies share files?#
Each copy starts with the snapshot's files, and what one attempt writes is its own. No sandbox can see, reach or address another, so a failing attempt cannot damage the others.
What does forking ten copies cost?#
Every copy bills per second at the rate for its size, and snapshot storage is included in every plan. At list, ten small sandboxes for up to 20 minutes cost at most $0.55, and spendCap holds the batch to the number you set.
Sources#
- CLI reference:
sandbox snapshot,fork,exec,run,snapshot create, read 17 Oct 2026 - TypeScript SDK:
Sandbox.create,commands.run,kill, spend caps, read 17 Oct 2026 - Pricing: rates per size, read 17 Oct 2026
- Quickstart: builds billed per second while they run, failed ones included, read 19 Oct 2026
- Limits and notices: what happens at a spend cap, read 17 Oct 2026
- Security: isolation between sandboxes, secrets kept out of snapshots, read 17 Oct 2026
- What is snapshot fork?, Promigence glossary, read 17 Oct 2026
- @promigence/sdk on npm: version 0.1.0-beta.7, read 17 Oct 2026
- promigence on PyPI: the Python SDK, version 0.1.0b7, read 17 Oct 2026
Updated 2026-10-17.