Concepts

Control Flow

retry, when, and forEach — conditional branching and iteration inside a SandboxBuilder.

Control flow primitives let you express branching and looping inside the SandboxBuilder callback. They queue lazily like every other operation, and the predicates/callbacks run at flush time with the current execution context.

retry

Retry a block of operations up to maxAttempts times on failure.

sb.retry(
  3,
  (sb) => {
    sb.exec("curl -f https://api.example.com/health");
  },
  { delayMs: 2000, backoff: "exponential" },
);

RetryOptions

OptionTypeDefaultDescription
delayMsnumber1000Wait between attempts (ms)
backoff"fixed" | "exponential""fixed"Delay growth strategy

With backoff: "exponential", the delay doubles each attempt (delayMs * 2^(attempt-1)): 1s, 2s, 4s, ...

when

Branch on runtime state. The predicate receives the current execution context.

sb.exec("test -f /tmp/cache.tar.gz", { strict: false });

sb.when(
  (ctx) => ctx.exitCode === 0,
  (sb) => sb.exec("tar -xzf /tmp/cache.tar.gz"),
  (sb) => sb.exec("npm ci"),
);

Predicate context

// shape of the predicate's ctx argument (a subset of the internal FlushContext)
interface WhenPredicateContext {
  stdout: string; // accumulated stdout so far
  exitCode: number; // exit code of the last exec
  vars: Record<string, unknown>; // values captured by readFile(path, as)
}

The otherwise branch is optional. If the predicate is false and no otherwise is provided, the when block is skipped.

forEach

Iterate over a list of items, queueing the same operations for each.

const packages = ["lodash", "zod", "tsx"];

sb.forEach(packages, (sb, pkg) => {
  sb.exec(`npm install ${pkg}`);
});

Run iterations in parallel with concurrency:

sb.forEach(packages, (sb, pkg) => sb.exec(`npm install ${pkg}`), { concurrency: 3 });

ForEachOptions

OptionTypeDefaultDescription
concurrencynumber1Max parallel iterations

With concurrency > 1, each parallel branch flushes against a shallow copy of the outer FlushContext. stdout/exitCode writes inside those branches don't propagate back to the outer context afterward — only vars (an object reference) does. Don't rely on ctx.stdout/ctx.exitCode reflecting what happened inside a concurrent forEach after it completes.

Composing

Control flow primitives nest naturally:

sb.retry(3, (sb) => {
  sb.exec("./deploy.sh", { strict: false });

  sb.when(
    (ctx) => ctx.exitCode !== 0,
    (sb) => {
      sb.exec("./rollback.sh");
      sb.forEach(["web", "api", "worker"], (sb, svc) => {
        sb.exec(`systemctl restart ${svc}`);
      });
    },
  );
});