# How it works
URL: /docs/core/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 [#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.

```mermaid
flowchart LR
  A["client.sandbox(opts)"] --> B["SandboxHandle<br/>(you hold this)"]
  B --> C["exec() / execCode()<br/>→ ExecHandle"]
  B --> D["writeFile() / readFile()"]
  B --> E["checkpoint()<br/>snapshot the container"]
  B --> F["close()<br/>delete the container"]
```

Always wrap sandbox usage in `try/finally` so the container is cleaned up even if an exec throws:

```ts
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 [#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 [#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](#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 [#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 [#the-alineo-labsworkflow-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.
