How it works
SandboxHandle as a first-class object, ExecHandle, the durable ledger, and the @alineo-labs/workflow lazy layer.
SandboxHandle as a first-class object
client.sandbox() creates a container and returns a live SandboxHandle object. You hold it as a variable, call methods on it, and close() it when done. Multiple sandboxes are just multiple variables — no special API.
Always wrap sandbox usage in try/finally so the container is cleaned up even if an exec throws:
const sb = await client.sandbox({
image: "ubuntu:22.04",
resources: { cpu: "500m", memory: "256Mi" },
});
try {
await sb.exec("npm ci");
await sb.exec("npm test").pipe(process.stdout);
} finally {
await sb.close();
}ExecHandle
sb.exec() returns an ExecHandle — a PromiseLike<ExecResult> with three consumption modes:
| Mode | How | When to use |
|---|---|---|
| Await | const { stdout, exitCode } = await sb.exec(...) | You need the full result before continuing |
| Pipe | await sb.exec(...).pipe(process.stdout) | Real-time output to a writable |
| Generator | for await (const chunk of sb.exec(...).stdout()) | Process chunks individually |
All three modes drive the same underlying SSE stream from execd. .result() resolves to { stdout, stderr, exitCode } once the stream is fully consumed.
The durable ledger
Every live exec() call writes three events to the storage adapter (replayed execs on a resumed sandbox skip this — see Checkpoint and resume below):
exec.started { cmd, seq }
exec.output { seq, type: "stdout", text: "..." } ← one per chunk
exec.completed { seq, exitCode }client.sandbox() writes sandbox.created. sb.close() writes sandbox.closed.
This means every run is fully replayable from the ledger — even if the process crashes mid-exec.
Checkpoint and resume
sb.checkpoint() calls the OpenSandbox snapshot API, waits for the snapshot to become ready, and writes a sandbox.checkpoint_created event with the snapshot ID.
client.resume(sandboxId) reads the ledger, finds the last checkpoint, restores a new container from the snapshot, and populates a replay cache from execs that completed before the checkpoint.
On the resumed sandbox, calling sb.exec() with the same sequence hits the cache — returning the stored result instantly, without writing any new exec.started/exec.output/exec.completed events (they're already in the ledger from the original run). Subsequent execs run live on the restored container and are logged normally.
Original run:
exec #1: "pip install requests" → logged, result cached
checkpoint() → snapshot_id written to ledger
exec #2: "python3 script.py" → logged
Resume:
exec #1: "pip install requests" → replayed from cache (instant)
exec #2: "python3 script.py" → runs live on restored containerThe @alineo-labs/workflow lazy layer
@alineo-labs/workflow adds a declarative layer on top of the core API. You queue operations synchronously inside a builder callback; the queue is flushed when .pipe() or .result() is awaited.
workflow(client)
.sandbox(opts, (sb) => {
sb.exec("npm ci") ← queued, not executed yet
sb.checkpoint() ← queued
sb.retry(3, (sb) => { ← queued
sb.exec("npm test")
})
})
.pipe(process.stdout) ← flush: create sandbox, run ops, close sandboxThe workflow builder handles sandbox lifecycle automatically. Use it when you want control flow (retry, when, forEach) or multi-sandbox pipelines (.parallel(), .sequence()).
For straightforward tasks — run a command, capture output, read a file — the direct SandboxHandle API is simpler.