# Commands&#x20;

> Run a command in a Sandbox as one durable step, then read its output as text, lines, or JSON.

> **Experimental.** `getCommandError` is exported from `inngest/experimental`, and `group.sandbox` is experimental. The API can change.

- **Write commands like a script.** Each interpolated value is one argument, so there is no quoting to get wrong.
- **Read output directly.** `.text()`, `.lines()`, and `.json()`.
- **Fail loudly.** A command that exits with an unexpected code fails its step, with the output attached.
- **Run long commands.** A command that outlasts one step's wait is still one row in the trace.

## Run a command

Every Sandbox has a `$`. Each command is one step.

```typescript {{ title: "TypeScript" }}
await group.sandbox("build", async (sb) => {
  await sb.$`git clone --depth 1 ${repoUrl} /work`;

  const sha = await sb.$({ cwd: "/work" })`git rev-parse HEAD`.text();

  await sb.$({ cwd: "/work", timeout: "20m" }).sh`pnpm install && pnpm test`;
});
```

- **No shell, no quoting** (line 2): The text splits on whitespace and each `${value}` is exactly one argument, so a URL with spaces or quotes can't change the command. The first word is looked up on the Sandbox's `PATH`. [Run a command](#run-a-command)

- **Options before the command** (line 4): `$({ ... })` returns a `$` with those options set. Use it once, or keep it for many commands. [Set options](#set-options)

- **`$.sh` runs a shell script** (line 6): Pipes, `&&`, redirects, and shell builtins need a shell, so use `$.sh`. [Use a shell](#use-a-shell)

`group` comes from the function's arguments. `$` is also on the Sandbox from `step.sandbox.create()`. See [Managed lifecycle](/docs-markdown/sandboxes/features/managed-lifecycle#scope-a-sandbox-to-a-block).

## Use a shell

`` $.sh`...` `` runs the script with `/bin/sh -c`. Interpolated values are quoted for you, so put them on their own.

```typescript {{ title: "TypeScript" }}
await sb.$.sh`ls ${dir} | wc -l`;      // quoted for you
await sb.$.sh`ls "${dir}"`;            // throws: no values inside quotes or after a backslash
```

In `$` and `$.sh`:

- Arrays spread into several arguments: `` $`rm ${files}` ``.
- `null`, `undefined`, and `false` are dropped: `` $`npm test ${verbose && "--verbose"}` ``.

## Read the output

The result is a promise with helpers. Awaiting it directly gives `{ stdout, stderr, exitCode, output }`.

```typescript {{ title: "TypeScript" }}
const sha = await sb.$`git rev-parse HEAD`.text();       // trimmed stdout
const files = await sb.$`git ls-files`.lines();          // string[], [] when empty
const pkg = await sb.$`cat package.json`.json<{ name: string }>();   // parsed stdout
const { stdout, stderr, exitCode } = await sb.$`node -v`;
```

`.json()` throws a `NonRetriableError`, naming the command, when the output is not JSON.

## Set options

```typescript {{ title: "TypeScript" }}
const work$ = sb.$({
  cwd: "/work",
  env: { NODE_ENV: "test", API_TOKEN: token },
  timeout: "10m",
  exitCodes: [0, 1],
});

await work$`pnpm lint`;
await work$.sh`pnpm test | tee out.log`;

await sb.$({ id: "install" }).sh`pnpm install`;
```

| Option      | Does                                                                                                                                                 |
| ----------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `cwd`       | The working directory.                                                                                                                               |
| `env`       | Environment for this command, over the Sandbox's. Values are kept out of the trace.                                                                  |
| `timeout`   | How long it may run. Defaults to `5m`. [Longer is fine](#long-running-commands). Takes any [duration](/docs-markdown/sandboxes/reference#durations). |
| `exitCodes` | The exit codes that are not failures. Defaults to `[0]`; `"any"` allows all.                                                                         |
| `id`        | The step ID for commands run by that `$({ id })`; later `$()` calls do not inherit it. Defaults to the command text; repeats are numbered.           |

A configured `$` can be reused for many commands, and calling it again merges: `work$({ cwd: "/other" })` keeps `env` and `exitCodes`.

`commands.run` takes `exitCodes` too, but defaults to `"any"`, so there the exit code comes back as data. `exitCodes` can't be combined with `tools` on `commands.run`.

## Handle failures

A command that exits with a code not in `exitCodes` fails its step and is not retried. A program that isn't found also fails the step. Read the details with `getCommandError`.

```typescript {{ title: "TypeScript" }}
import { getCommandError } from "inngest/experimental";

try {
  await sb.$`pnpm test`;
} catch (error) {
  const failure = getCommandError(error);

  if (failure) {
    console.log(failure.type);        // "failed" or "timeout"
    console.log(failure.exitCode);    // 1 (not set for a timeout)
    console.log(failure.stderrTail);  // last 8 KiB of stderr
  }

  throw error;
}
```

`getCommandError` returns a plain object, or `undefined` for any other error:

| Field                      | Value                                                                                                                      |
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| `type`                     | `"failed"` for a disallowed exit or a process that ended without one, or `"timeout"` for a command stopped at its timeout. |
| `message`                  | The error message.                                                                                                         |
| `command`                  | The command text.                                                                                                          |
| `exitCode`                 | The exit code, when it exited.                                                                                             |
| `stdoutTail`, `stderrTail` | The last 8 KiB of each stream. Both are empty when a command with a timeout of 5m or less times out.                       |

The error reaches your handler as a `StepError` (a `NonRetriableError` for a timeout of 5m or less), so check it with `getCommandError`, not `instanceof`. To treat an exit as success, allow it with `exitCodes`.

## Long-running commands

A timeout over five minutes runs the command as a process that the steps follow, with one wait step per five minutes. It is still one `[$]` row in the trace.

```typescript {{ title: "TypeScript" }}
await sb.$({ timeout: "45m" }).sh`pnpm test:e2e`;
```

- Output is the last 512 KiB.
- Each wait counts against the run's step limit, one per five minutes of `timeout`.
- The `timeout` does not extend the Sandbox's one hour of active runtime. See [Runtime expiry](/docs-markdown/sandboxes/limits#runtime-expiry).
- A command that times out is killed, and fails with `type: "timeout"`.

## In the trace

Each command is one row tagged `[$]` and named after the command. See [Traces](/docs-markdown/sandboxes/features/traces#what-appears-in-the-trace).

`env` values are redacted from the trace, but the command text and step ID are not. Pass secrets in `env` rather than interpolating them into the command. See [Secrets](/docs-markdown/sandboxes/features/secrets).

```typescript {{ title: "TypeScript" }}
await sb.$({ env: { NPM_TOKEN: token } }).sh`pnpm publish`;   // token stays out of the trace
await sb.$`npm login --token ${token}`;                       // token is in the command text
```