# Running commands

> Run a command in a machine, wait for its result or stream its output as it arrives, and handle exit codes, timeouts, and retries.

`POST /v1/machines/{id}/exec` runs one command in a running machine and returns its exit code and output. The command is an [operation](/docs/concepts/operations), so it gets an idempotency key and runs after any earlier operation on the same machine. Past commands and their output stay in the machine's [command history](/docs/guides/command-history).

## Run and wait

```sh tab="CLI"
vy exec dev -- uname -a                 # argv, run directly
vy exec dev -c 'cd /work && make test'  # a shell command
```

```ts tab="TypeScript"
const { exitCode, stdout, stderr } = await machine.exec("cd /work && make test");
const listing = await machine.exec(["ls", "-l", "/work"]);
```

```python tab="Python"
result = machine.exec("cd /work && make test")
print(result.exit_code, result.stdout, result.stderr)
listing = machine.exec(["ls", "-l", "/work"])
```

```sh tab="curl"
curl -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/exec" \
  -H "Authorization: Bearer $VMYARD_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"command": "uname -a", "timeout_ms": 30000}'
# 202 Accepted: poll operation_url until it is terminal, then read exec_result.
```

Send exactly one of:

- `argv`: the program and its arguments, run directly without a shell.
- `command`: a string run as `/bin/sh -c <command>`, at most 8 KiB.

In the SDKs a string becomes `command` and an array becomes `argv`. In the CLI, words after `--` are `argv` and `-c` is `command`. On a machine with an [environment](/docs/guides/environments), commands run with its variables loaded.

## The result

The finished operation's `exec_result` holds:

| Field | Meaning |
| --- | --- |
| `exit_code` | The command's exit status; `null` if it didn't exit normally. |
| `stdout_base64`, `stderr_base64` | The output, base64-encoded. |
| `timed_out` | It hit `timeout_ms` and was killed. |
| `cancelled` | It was cancelled. |
| `truncated` | Output went over 1 MiB and was cut. |

The operation **succeeds** only when the command exits with code 0. Otherwise it ends `failed`, still carries `exec_result` with the output, and its `error_code` says why:

| `error_code` | Meaning |
| --- | --- |
| `exec_nonzero` | The command exited with a non-zero code. |
| `exec_timed_out` | It ran past `timeout_ms`. |
| `exec_cancelled` | It was cancelled. |
| `exec_no_exit_status` | It ended without an exit status, for example killed by a signal. |

The SDKs raise `OperationError` for a failed exec; its `operation.exec_result` holds the output. The CLI exits with the remote exit code, `124` if the command timed out, and `125` if VMYard couldn't run it. A failed exec also emits the `operation.failed` [webhook](/docs/guides/webhooks).

```ts tab="TypeScript"
import { OperationError } from "@vmyard/sdk";

try {
  await machine.exec("make test");
} catch (error) {
  if (error instanceof OperationError && error.errorCode === "exec_nonzero") {
    console.log("tests failed", error.operation.exec_result?.exit_code);
  } else throw error;
}
```

```python tab="Python"
from vmyard import OperationError

try:
    machine.exec("make test")
except OperationError as error:
    if error.error_code != "exec_nonzero":
        raise
    print("tests failed", error.operation["exec_result"]["exit_code"])
```

## Timeouts

`timeout_ms` is 1 to 600,000 (10 minutes) and defaults to the maximum. A larger value is refused with `422 timeout_exceeds_runtime_limit`. When the timeout passes, the command's process group is killed. For anything longer, use a [background command](/docs/guides/background-commands).

## Stream output

Set `stream: true` to get output while the command runs. The response is `200 application/x-ndjson`: one JSON object per line.

```sh tab="CLI"
vy exec dev -- npm test          # streams by default; --no-stream waits instead
```

```ts tab="TypeScript"
for await (const frame of machine.exec(["npm", "test"], { stream: true })) {
  if (frame.type === "stdout") process.stdout.write(frame.data);
  if (frame.type === "stderr") process.stderr.write(frame.data);
  if (frame.type === "exit") console.log("exit", frame.exitCode);
}
```

```python tab="Python"
with machine.exec(["npm", "test"], stream=True) as stream:
    for frame in stream:
        if frame.type in ("stdout", "stderr"):
            print(frame.data.decode(), end="")
        elif frame.type == "exit":
            print("exit", frame.exit_code)
```

```sh tab="curl"
curl -N -X POST "$VMYARD_API_URL/v1/machines/$MACHINE_ID/exec" \
  -H "Authorization: Bearer $VMYARD_API_KEY" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{"argv": ["npm", "test"], "stream": true}'
```

```json title="Frames"
{"type":"started","operation_id":"6f1c…","operation_url":"/v1/operations/6f1c…"}
{"type":"stdout","data_base64":"PiB0ZXN0Cg=="}
{"type":"stderr","data_base64":"d2FybmluZwo="}
{"type":"exit","operation_id":"6f1c…","status":"succeeded","exit_code":0,"timed_out":false,"cancelled":false,"truncated":false}
```

The stream starts with `started` and ends with exactly one `exit` or `error` frame. `stdout` and `stderr` frames are a preview; the `exit` frame and the operation's `exec_result` are the authoritative result.

- If the connection drops, the command keeps running. Send the **same idempotency key and body** again to stream it from the start, or follow the operation from the `started` frame.
- Output is kept with the operation, up to 1 MiB per operation, and also appears as `stdout`/`stderr` events on `GET /v1/operations/{id}/events`.
- An `error` frame (`code`, `message`) means VMYard couldn't run or finish the command.

## Retries and ordering

Retrying with the same idempotency key and body returns the original operation; it never runs the command twice. A different body with the same key is `409 idempotency_conflict`. Commands on one machine run one at a time in the order they were accepted.

## Who can run commands

Owners, Admins, and Operators, or an API key with at least the `use_existing` preset. The machine must be running.
