# Commands (/docs/api/commands)



A command is a top-level process invocation record. You can run a command
and wait for it to exit, or start it in the background and follow its logs
by polling or over a server-sent events (SSE) stream.

## The Command object [#the-command-object]

| Field               | Type    | Description                                                                               |
| ------------------- | ------- | ----------------------------------------------------------------------------------------- |
| `id`                | string  | Command ID, prefixed `cmd_`                                                               |
| `sandboxId`         | string  | Sandbox the command ran in, prefixed `sbx_`                                               |
| `command`           | string  | The shell command line                                                                    |
| `cwd`               | string  | Working directory                                                                         |
| `env`               | object  | Extra environment variables                                                               |
| `status`            | string  | `queued`, `starting`, `running`, `exited`, `failed`, `canceled`, `timed_out`, or `killed` |
| `cancelMode`        | string  | `graceful` or `force`, when canceled                                                      |
| `canceledAt`        | string  | ISO 8601 cancellation timestamp                                                           |
| `exitCode`          | integer | Process exit code, once exited                                                            |
| `stdout`            | string  | Captured stdout                                                                           |
| `stderr`            | string  | Captured stderr                                                                           |
| `stdoutTruncated`   | boolean | Whether `stdout` was truncated                                                            |
| `stderrTruncated`   | boolean | Whether `stderr` was truncated                                                            |
| `startedAt`         | string  | ISO 8601 start timestamp                                                                  |
| `finishedAt`        | string  | ISO 8601 finish timestamp                                                                 |
| `durationMs`        | integer | Wall-clock duration                                                                       |
| `collectStatus`     | string  | `failed`, `not_requested`, `partial`, `pending`, `skipped`, or `succeeded`                |
| `collectErrors`     | array   | Per-path errors from artifact collection                                                  |
| `killedReason`      | string  | `sandbox_destroyed`, when the sandbox died under the command                              |
| `terminationSignal` | string  | `SIGKILL` or `SIGTERM`, when terminated                                                   |

<Callout type="warn" title="Important">
  A non-zero exit code is not an API error. The HTTP request succeeds — check `status`
  and `exitCode` to learn how the process finished.
</Callout>

## Run a command [#run-a-command]

`POST /v1/sandboxes/{sandboxId}/commands` — requires scope
`command:run`, plus `artifact:create` if you pass `collect`. Accepts an
[`Idempotency-Key`](/docs/api/pagination-and-idempotency) header.

The endpoint runs the command and waits for it to exit by default. Set
`background` to `true` to return immediately and follow the command through
the get, logs, or SSE endpoints.

| Field        | Type    | Required | Default     | Constraints                                              |
| ------------ | ------- | -------- | ----------- | -------------------------------------------------------- |
| `command`    | string  | Yes      | —           | Non-empty shell command line                             |
| `background` | boolean | No       | `false`     | `collect` and `collectOn` aren't valid when `true`       |
| `cwd`        | string  | No       | —           | Working directory inside the workspace                   |
| `env`        | object  | No       | —           | String map; keys with the `CROWNEST` prefix are reserved |
| `timeoutMs`  | integer | No       | 60000       | Max 600000                                               |
| `collectOn`  | string  | No       | `"success"` | `success` or `always`; foreground commands only          |
| `collect`    | array   | No       | —           | Artifact paths and names; foreground commands only       |

`collect` exports the listed workspace paths as
[artifacts](/docs/api/artifacts) after the command finishes; `collectOn`
controls whether that happens only on a zero exit code (`success`) or
regardless of outcome (`always`).

```bash title="Terminal"
curl -X POST https://api.crownest.dev/v1/sandboxes/sbx_abc123/commands \
  -H "Authorization: Bearer $CROWNEST_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9d2e4f1b-run-01" \
  -d '{
    "command": "python3 train.py",
    "timeoutMs": 120000,
    "collect": [{ "path": "/workspace/model.bin", "name": "model" }]
  }'
```

With `background` omitted or `false`, the endpoint returns `200` with the
finished command:

```json
{
  "command": {
    "id": "cmd_abc123",
    "sandboxId": "sbx_abc123",
    "command": "python3 train.py",
    "status": "exited",
    "exitCode": 0,
    "stdout": "epoch 1 complete\n",
    "stderr": "",
    "stdoutTruncated": false,
    "stderrTruncated": false,
    "durationMs": 8421,
    "collectStatus": "succeeded"
  }
}
```

Errors: `invalid_request`, `reserved_env_key`, `forbidden`, `not_found`,
`sandbox_destroyed`, `quota_exceeded`.

To start the command in the background, use the same endpoint with
`background: true`:

```bash title="Terminal"
curl -X POST https://api.crownest.dev/v1/sandboxes/sbx_abc123/commands \
  -H "Authorization: Bearer $CROWNEST_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 9d2e4f1b-background-01" \
  -d '{ "command": "npm run dev", "background": true, "timeoutMs": 600000 }'
```

Returns `202` with the command in `queued` or `starting` status. Follow it
with get, logs, or the SSE stream.

### Deprecated aliases [#deprecated-aliases]

The previous foreground and background paths remain available until July 10, 2027. Their responses include `Deprecation`, `Sunset`, and `Link` headers that
identify the canonical endpoint.

* `POST /v1/sandboxes/{sandboxId}/commands/run` runs a foreground command.
* `POST /v1/sandboxes/{sandboxId}/commands/start` runs a background command.

## Get a command [#get-a-command]

`GET /v1/commands/{commandId}` — requires scope `command:read`.

Returns the command record, including its current status, exit code, and
captured output.

```bash title="Terminal"
curl https://api.crownest.dev/v1/commands/cmd_abc123 \
  -H "Authorization: Bearer $CROWNEST_API_KEY"
```

Returns `200` with `{ "command": { ... } }`. Errors: `forbidden`,
`not_found`.

## Cancel a command [#cancel-a-command]

`POST /v1/commands/{commandId}/cancel` — requires scope `command:cancel`.

Stops a running command. The operation is idempotent by state — canceling
an already-finished command returns the current record.

| Field  | Type   | Required | Default      | Constraints           |
| ------ | ------ | -------- | ------------ | --------------------- |
| `mode` | string | No       | `"graceful"` | `graceful` or `force` |

Graceful cancellation sends `SIGTERM` and lets the process clean up; force
sends `SIGKILL`.

```bash title="Terminal"
curl -X POST https://api.crownest.dev/v1/commands/cmd_abc123/cancel \
  -H "Authorization: Bearer $CROWNEST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "mode": "force" }'
```

Returns `200` with `{ "command": { ... } }` showing status `canceled`.
Errors: `forbidden`, `not_found`.

## Read logs [#read-logs]

`GET /v1/commands/{commandId}/logs` — requires scope `command:read`.

Returns log chunks in sequence order. Use `afterSeq` to poll for new
output incrementally.

| Parameter  | Type    | Default | Constraints                       |
| ---------- | ------- | ------- | --------------------------------- |
| `afterSeq` | integer | —       | Return chunks after this sequence |
| `limit`    | integer | 100     | Max 100                           |

```bash title="Terminal"
curl "https://api.crownest.dev/v1/commands/cmd_abc123/logs?afterSeq=12" \
  -H "Authorization: Bearer $CROWNEST_API_KEY"
```

```json
{
  "data": [
    {
      "commandId": "cmd_abc123",
      "seq": 13,
      "createdAt": "2026-06-11T13:00:05.000Z",
      "data": "warning: ignored flag\n",
      "stream": "stderr"
    }
  ],
  "hasMore": false,
  "nextSeq": 13
}
```

Each chunk has `commandId`, `seq`, `createdAt`, `data`, and `stream`. Pass
the returned `nextSeq` as `afterSeq` on your next poll.

## Stream logs over SSE [#stream-logs-over-sse]

`GET /v1/commands/{commandId}/stream` — requires scope `command:read`.

Streams log output as server-sent events, ending when the command
finishes. Event types:

| Event       | Meaning                                                           |
| ----------- | ----------------------------------------------------------------- |
| `log`       | A log chunk with `seq`, `type`, `createdAt`, `data`, and `stream` |
| `heartbeat` | Keep-alive signal while the command is quiet                      |
| `terminal`  | The command reached a terminal status                             |
| `error`     | The stream failed; reconnect or fall back to polling              |

To resume after a disconnect, send the last `seq` you saw, either as the
`afterSeq` query parameter or as the standard `Last-Event-ID` header —
SSE clients send `Last-Event-ID` automatically on reconnect. If the
requested sequence has fallen outside the retention window, the stream
returns a `stream_gap` error; fetch the full logs again instead.

```bash title="Terminal"
curl -N "https://api.crownest.dev/v1/commands/cmd_abc123/stream" \
  -H "Authorization: Bearer $CROWNEST_API_KEY" \
  -H "Last-Event-ID: 13"
```

## Get a logs download URL [#get-a-logs-download-url]

`POST /v1/commands/{commandId}/logs/download-url` — requires scope
`command:read`.

<Callout type="warn" title="Not yet available">
  Log download URLs require log-archive storage, which isn't enabled yet. This endpoint
  currently returns `feature_not_enabled` (403). Page through the logs endpoint or
  follow the SSE stream instead.
</Callout>

## Next steps [#next-steps]

* Export command outputs durably in [Artifacts](/docs/api/artifacts).
* Expose a long-running server in [Previews](/docs/api/previews).
