# Control Flow
URL: /docs/workflow/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]

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

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

### RetryOptions [#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 [#when]

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

```ts
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 [#predicate-context]

```ts
// 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 [#foreach]

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

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

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

Run iterations in parallel with `concurrency`:

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

### ForEachOptions [#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 [#composing]

Control flow primitives nest naturally:

```ts
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}`);
      });
    },
  );
});
```
