Concepts

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:

ModeHowWhen to use
Awaitconst { stdout, exitCode } = await sb.exec(...)You need the full result before continuing
Pipeawait sb.exec(...).pipe(process.stdout)Real-time output to a writable
Generatorfor 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 container

The @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 sandbox

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