Skip to content

Guides

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).

TypeScript
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: typescript

node 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:

Python
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: python

From a shell#

--json makes every command print one JSON document, so a script can pick out the ids:

Terminal
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: bash

promigence 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:

  1. sbx.fork() on a sandbox object never copies your install: after any command it asks for fromOrigin: true and then copies the snapshot the sandbox started from. To copy it as it is now, take a snapshot first and fork that.
  2. 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 (secrets on Sandbox.fork).
  3. 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:

Terminal
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: bash

A 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 sandboxSnapshot built from a recipe
Commandpromigence sandbox snapshot SID, client.sandboxes.snapshot(id)promigence snapshot create --image REF --setup CMD --verify CMD
Starts fromWhatever the sandbox holds at that momentAn image or a Dockerfile, plus your setup steps
Checked by a verify commandNoYes: stored only if the command exits 0
Best forBranching a session mid-task, retries from one pointAn environment many runs share, rebuilt per commit
Background commandsEnded to take the snapshotA --start command runs in every copy
CopiesSandbox.fork(id, { count }), promigence run --snapshot NAME --count NThe same
Install once, try ten timesYour agent framework installs dependencies once in one sandbox and takes a snapshot of it. Ten copies start from that snapshot with node_modules already installed, each runs one attempt and its tests, and the framework keeps the attempt that passed.Sandbox.createsandboxes.snapshotSandbox.fork,count 10one attempt eachresultsdiff of the winnerYour agent frameworkOne sandbox: gitclone and npminstallSnapshot:app-installedTen copies withnode_modules inplaceTen attempts, thennpm testKeep the attemptthat passed, killthe rest
Your agent framework branches one prepared sandbox into ten attempts

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#

Updated 2026-10-17.

Get started

Leave your work email and we will get you started.

See our Privacy Policy for how we use your email.