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
| Option | Type | Default | Description |
|---|---|---|---|
delayMs | number | 1000 | Wait 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
| Option | Type | Default | Description |
|---|---|---|---|
concurrency | number | 1 | Max 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}`);
});
},
);
});