---
lastModified: 2026-10-13
---

# Commands

Commands are how anyone acts on a run: run a program, read, write or edit a file, list a folder, copy a file in or out, open a terminal, and finish the run. You send them through the API or the CLI. Jinn's agent sends the same ones. Every run accepts every kind, with an agent or without one.

A key that can start a function can send commands to its runs.

## Drive a run from the CLI

A function without an agent starts a VM and waits. `jinn run` returns once the VM is ready.

```sh
$ jinn run go-sandbox
run_3d9e1c07a5b28f64e0d71c92 queued · go-sandbox v2
run_3d9e1c07a5b28f64e0d71c92 ready · jinn finish run_3d9e1c07a5b28f64e0d71c92 ends it
$ jinn cp ./api.tar.gz run_3d9e1c07a5b28f64e0d71c92:api.tar.gz
$ jinn exec run_3d9e1c07a5b28f64e0d71c92 -- 'tar xzf api.tar.gz && cd api && go test ./...'
ok      example.com/api/store   1.204s
ok      example.com/api/http    0.588s
$ jinn cp run_3d9e1c07a5b28f64e0d71c92:api/coverage.html ./coverage.html
$ jinn finish run_3d9e1c07a5b28f64e0d71c92
run_3d9e1c07a5b28f64e0d71c92 succeeded
image img_8b2f04d6e1a93c75f0e2d418
```

- `jinn exec` prints the command's output as it comes and exits with the command's own status. It exits with 125 when Jinn could not run the command.
- `jinn cp PATH RUN:PATH` sends one file. `jinn cp RUN:PATH PATH` fetches a file, or a folder, which arrives as a folder.
- `jinn finish` ends the run. `--failed REASON` ends it as failed.

## Send a command

```http
POST /v1/runs/run_3d9e1c07a5b28f64e0d71c92/commands
{"kind": "bash", "arguments": {"command": "go test ./...", "workdir": "/workspace/api"}}
← 201 {"id": "cmd_51a7e2c09d84b36f1e0a2d97", "kind": "bash", "state": "queued", "source": "caller", "sequence": 4, …}
```

| Kind | Arguments | Result |
|---|---|---|
| `bash` | `command`; `workdir`, `timeout_ms` (5 minutes when left out; at most 24 hours), `env` | `exit_code`, `output` (its first and last 16 KiB), `output_bytes`, `truncated`, `timed_out`, `oom_killed` |
| `read` | `file_path`; `offset`, `limit` (lines) | `output`: the text. An image (PNG, JPEG, GIF or WebP, at most 3.75 MiB and 2000 pixels a side) comes as `image_url`. |
| `write` | `file_path`, `content` | `bytes` written. Folders above it are made. |
| `edit` | `file_path`, `edits`: `[{old_text, new_text}]` | `applied`. Each `old_text` must occur exactly once; all edits apply or none do. |
| `list` | `path` | `entries`: each file and folder directly in it, files with their size, at most 1,000 (`truncated` says there are more). Links are listed in `skipped`, not followed. |
| `put` | `file` (an upload, `file_…`), `path` | `bytes` written. It replaces a file at `path`. |
| `get` | `path` | `download_url`: the file, or a folder as `.tar.gz`, valid for 15 minutes. |
| `finish` | `state` (`succeeded` or `failed`); `detail` | `accepted`, and `problems` when the output does not match the manifest. See [Finish a run](/runs#finish-a-run). |
| `terminal` | `rows`, `cols`, `public_key`; `env` | `relay_url`, `route`, `public_key`. See [Open a terminal](#open-a-terminal). |

- A command's arguments are at most 128 KiB of JSON. Send larger files with `put`.
- Commands run as root, at the same time, in the order they arrive. Relative paths resolve from `/workspace`.
- Each `bash` command runs in its own `bash -c`, with the run's environment and its own `env`. Nothing but files carries over between commands.
- A `bash` command that ran out of memory says `oom_killed`. Its other processes are killed with it; the run goes on.
- A command sent while the run is `queued` starts when the run is ready.
- A command that the VM answered `succeeded`, even when its program exited with 1 or its result has an `error`. One that could not run `failed`, with `error`. One cancelled, or still running when the run ended, is `cancelled`.
- A run that has ended refuses commands with `run_ended`.

## Wait for a result

Three ways, use any:

- Long-poll one command: `GET /v1/runs/{id}/commands/{cid}?wait=25` answers when the command ends, or after 25 seconds.
- Read what changed: `GET /v1/runs/{id}/commands?since=N&wait=25` returns the commands that changed after sequence `N`, the run's `state`, and the `sequence` to send next time. One read holds about 4 MiB at most; read again from its `sequence` for the rest.
- Get the `command.finished` event at the run's [webhook](/webhooks). It is sent for commands you send, not for the agent's.

`wait` is 0 to 25 seconds.

## Follow a command's output

```http
GET /v1/runs/{id}/commands/{cid}/output?after=0&wait=25
← 200 {"output": "ok  \texample.com/api/store\t1.204s\n", "next": 33, "output_bytes": 33, "stored_bytes": 33, "done": false}
```

Send `next` as `after` to read on. One read returns at most 512 KiB, ending on a whole UTF-8 character. `done` is `true` once the command has ended and you have read everything kept. Jinn keeps the first 4 MiB of each command's output; the result keeps the first and last 16 KiB of all of it.

## Cancel a command

`POST /v1/runs/{id}/commands/{cid}/cancel` stops a command: its processes are killed, and it ends as `cancelled`. Cancelling a command that has ended changes nothing.

## Copy files

To send a file, upload it with `POST /v1/files`, as for a run's [input](/runs#start-a-run), then send `put` with the upload's id. An upload is at most 5 GB. A `get` uploads the file or folder and returns a link; it is at most 5 GiB.

## Open a terminal

```sh
$ jinn shell run_3d9e1c07a5b28f64e0d71c92
root@jinn:/workspace# go version
go version go1.19.8 linux/amd64
root@jinn:/workspace# exit
```

`jinn shell` opens a login shell as root in `/workspace`, with the run's variables, and exits with the shell's status. The console's **Terminal** tab does the same. Both reach the shell through Jinn's relay, end to end encrypted: only your end and the run's machine can read the session.

To open one from your own code, the SDKs' `OpenTerminal` and `openTerminal` do these steps:

1. Make an ECDH P-256 key pair, and send `terminal` with `rows`, `cols` and your public key (uncompressed, base64url) as `public_key`.
2. The command's result holds `relay_url`, `route` and the host's `public_key`. Derive the shared secret, then 64 bytes with HKDF-SHA-256: the salt is the SHA-256 of `jinn-terminal-v1`, a zero byte, your public key and the host's; the info is `jinn-terminal-v1 keys`. The first 32 bytes are the AES-256-GCM key for what you send, the last 32 for what you receive.
3. Open a WebSocket to `relay_url?route=…&side=caller`. Each binary message is a frame: its kind (1 byte), its sequence (8 bytes, big-endian, from 0 in each direction), and the sealed data. The nonce is 4 zero bytes and the sequence; the kind and sequence are the associated data.
4. Send kind 1 for input and kind 3 to resize (rows and columns, two big-endian 16-bit numbers). You receive kind 2, the shell's output, and kind 4, its exit status (a big-endian 32-bit number), last.

The session ends when the shell exits, either end closes, or the run ends. The relay waits 2 minutes for your end, then closes the session. Each frame holds at most 32 KiB.

## Use your own agent

`GET /v1/tools` returns the tools Jinn's agent shows its model: `bash`, `read`, `write`, `edit`, `screenshot`, `web_search`, `complete` and `give_up`, each with its description and JSON Schema. Your own loop can show its model the same tools, and send each call as a command:

| Tool | Command |
|---|---|
| `bash`, `read`, `write`, `edit` | The command of the same name, with the call's arguments. |
| `screenshot` | `bash` with `grim /tmp/screen.png`, then `read` of `/tmp/screen.png`. |
| `complete` | `finish` with `state: succeeded`. |
| `give_up` | `finish` with `state: failed` and the reason as `detail`. |
| `web_search` | Not a command: search from your own code. |
