# Refs & state
URL: /docs/core/concepts/refs-and-state

Capture exec output and thread values between steps using ExecResult and the workflow builder's vars.



There's no special `Ref<T>` type. Output from one step is just a TypeScript variable — you capture it from the `ExecResult` and pass it to the next step however you like.

## Capturing stdout [#capturing-stdout]

`await sb.exec(cmd)` returns `{ stdout, stderr, exitCode }`. Use `stdout` directly in subsequent commands:

```ts
const sb = await client.sandbox({
  image: "node:20-slim",
  resources: { cpu: "500m", memory: "256Mi" },
});
try {
  const { stdout: nodeVersion } = await sb.exec('node -e "process.stdout.write(process.version)"');

  // Use the captured value in the next exec
  await sb.exec(`echo "Running on Node ${nodeVersion.trim()}"`).pipe(process.stdout);

  // Or write it to a file
  await sb.writeFile(
    "/tmp/info.json",
    JSON.stringify({ node: nodeVersion.trim(), capturedAt: new Date().toISOString() }),
  );
} finally {
  await sb.close();
}
```

## Reading files as state [#reading-files-as-state]

`sb.readFile(path)` returns the file content as a string:

```ts
await sb.exec("node -e \"require('fs').writeFileSync('/tmp/version.txt', process.version)\"");
const version = await sb.readFile("/tmp/version.txt");
console.log(version.trim()); // "v20.x.x"
```

## State in the workflow builder [#state-in-the-workflow-builder]

When using `@alineo-labs/workflow`, the `SandboxBuilder` doesn't return values from `exec()` — steps are queued, not executed yet. Use `readFile(path, as)` to capture a file into the `vars` map, then read from `vars` after the workflow resolves:

```ts
import { workflow } from "@alineo-labs/workflow";

const { vars } = await workflow(client)
  .sandbox({ image: "node:22", resources: { cpu: "500m", memory: "256Mi" } }, (sb) => {
    sb.exec("node --version > /tmp/version.txt");
    sb.readFile("/tmp/version.txt", "nodeVersion"); // captured into vars["nodeVersion"]
  })
  .result();

console.log(vars.nodeVersion); // "v22.x.x\n"
```

The `when` predicate also receives runtime state:

```ts
sb.exec("test -f /etc/hostname", { strict: false });
sb.when(
  (ctx) => ctx.exitCode === 0, // ctx.exitCode, ctx.stdout, ctx.vars
  (sb) => sb.exec("echo exists"),
  (sb) => sb.exec("echo missing"),
);
```

`ctx.stdout` is the accumulated stdout from all execs in the sandbox so far. `ctx.vars` holds anything captured via `readFile(..., as)`.

## Passing state between sandboxes [#passing-state-between-sandboxes]

In `.sequence()`, each stage receives the previous stage's `WorkflowResult`:

```ts
await workflow(client)
  .sequence([
    {
      image: "node:22",
      name: "build",
      resources: { cpu: "1", memory: "512Mi" },
      run: (sb) => {
        sb.exec("npm run build 2>&1 | tee /tmp/build.log");
        sb.readFile("/tmp/build.log", "buildLog");
      },
    },
    {
      image: "ubuntu:22.04",
      name: "deploy",
      resources: { cpu: "500m", memory: "256Mi" },
      run: (sb, prev) => {
        // prev.vars.buildLog from the first sandbox
        sb.exec(`echo "Build output lines: $(echo '${prev?.vars.buildLog}' | wc -l)"`);
      },
    },
  ])
  .pipe(process.stdout);
```
