Command Reference
A command is a unit of work sent from the studio to an agent. The contract is exact: what each command takes, what it returns, and how its state moves.
The envelope
Every command travels inside the same message envelope:
{
"v": 1,
"type": "command",
"kind": "request",
"id": "0a3bβ¦",
"timestamp": "2026-10-21T08:14:22.101Z",
"payload": {
"command": "shell",
"args": { "script": "uname -a", "cwd": "/tmp" },
"timeout": 300000
}
}| Field | Meaning |
|---|---|
v | The protocol version. 3. |
type | Always command for a command. |
kind | request for the command the studio sends, response for the agent's answer. |
id | The command's id β the key you read the result back with. It is the same value the studio stores in agent_commands.id. |
timestamp | ISO 8601, when the message was created. |
payload.command | Which command to run. |
payload.args | The command's arguments. |
payload.timeout | Optional overall timeout, in milliseconds. |
Command status
pending βββΆ in_progress βββΆ completed
| Status | Meaning |
|---|---|
pending | The studio has accepted the command and written it to agent_commands. Nobody has run it yet. |
in_progress | The agent has claimed it and is executing it. |
completed | The result is stored. |
The status lives in the database, not in memory, so both delivery paths β the socket and the polling fallback β write the same rows and the same states.
There is no server-side timeout and no cancellation. A command that was accepted but never claimed
stays pending until an agent picks it up.
Result envelope
The agent answers with the same envelope, kind set to response and the command's id:
{
"v": 1,
"type": "command",
"kind": "response",
"id": "0a3bβ¦",
"timestamp": "2026-10-21T08:14:22.229Z",
"payload": {
"ok": true,
"data": {
"status": "success",
"output": {
"stdout": "Linux bench-01 β¦",
"stderr": "",
"exitCode": 0,
"durationMs": 128
}
}
}
}| Field | Meaning |
|---|---|
payload.ok | Whether the command succeeded. |
payload.data.status | success, error or timeout. |
payload.data.output.stdout | The standard output. |
payload.data.output.stderr | The standard error. |
payload.data.output.exitCode | The process exit code. |
payload.data.output.durationMs | How long the command took. |
payload.error.code | Present when ok is false. |
payload.error.message | The human-readable error. |
A command that runs but ends badly β a non-zero exit code, say β still answers with ok: true; the
failure shows up as data.status: "error". An ok: false answer means the command could not be run
at all, and carries an error object instead of data:
{
"v": 1,
"type": "command",
"kind": "response",
"id": "0a3bβ¦",
"timestamp": "β¦",
"payload": {
"ok": false,
"error": { "code": "MISSING_SCRIPT", "message": "No script provided" }
}
}The studio stores the whole result β including the error β as JSON.
Error codes
| Code | Raised by | Meaning |
|---|---|---|
UNKNOWN_COMMAND | Any unrecognised command name | The agent does not know the command. The message is Unknown command: <name>. |
MISSING_SCRIPT | shell | No script argument was supplied. |
MISSING_PATH | open_serial, close_serial | No path argument was supplied. |
MISSING_ARGS | serial_write | path or data was missing. |
NOT_IMPLEMENTED | run_test_suite, upload_artifact | The command is part of the contract but the agent does not implement it. |
EXECUTION_ERROR | Any command | The handler threw. The message is the exception text. |
COMMAND_TIMEOUT | Any command | The command exceeded its timeout. |
The same set is produced by both delivery paths β the socket and the polling fallback β because both call the same handlers.
Timeout and spawn failures
Two failures are reported through the output rather than through the result's status.
| Failure | Result |
|---|---|
| The command exceeded its timeout | status is timeout, output.exitCode is -2, and output.stderr reads Command timed out after <ms>ms. The code COMMAND_TIMEOUT names the same condition. |
| The process could not be spawned | status is error, payload.error.code is EXECUTION_ERROR, and output.exitCode is -1. |
The commands
| Command | Arguments | What it does |
|---|---|---|
shell | script, cwd | Runs a script on the device under test. |
get_system_info | none | Returns a snapshot of the machine. |
get_serial_ports | none | Lists the serial ports on the machine. |
open_serial | path, baudRate | Opens a serial port on the agent. |
close_serial | path | Closes a port the agent has open. |
serial_write | path, data | Writes to a port the agent has open. |
run_test_suite | suiteId, projectId, deviceId | Runs a test suite on the device under test. |
upload_artifact | filePath | Uploads an artifact from the bench to the studio. |
screen_watch | platform, deviceId, enabled, intervalMs | Starts or stops the screen watch on a device. |
The argument list of each command is a contract, not a convention: the studio's Agents console builds its form from it, and the agent's local command test builds the same form from it.
shell
Runs a shell script on the machine under test.
| Argument | Type | Default | Meaning |
|---|---|---|---|
script | string | β | The script body. Required. |
cwd | string | /tmp | Where the script file is written and executed. |
The script's timeout is the envelope's payload.timeout, in milliseconds. It defaults to 300000.
The agent writes the script to <cwd>/.agent_script_<timestamp>.sh, makes it executable and spawns
/bin/bash on it with cwd as the working directory.
| Exit code | Meaning |
|---|---|
0 | Success. |
| other | The script's own failure code. |
-1 | The process could not be spawned. |
-2 | The command hit its timeout and was killed with SIGTERM. |
On timeout, stderr reads Command timed out after <ms>ms.
{
"command": "shell",
"args": {
"script": "#!/bin/bash\necho hello",
"cwd": "/tmp"
},
"timeout": 300000
}get_system_info
Takes no arguments. Returns a snapshot of the machine.
{
"command": "get_system_info",
"args": {}
}The result has no top-level system fields. output.stdout holds a JSON string, so a consumer
has to parse it a second time.
{
"ok": true,
"data": {
"status": "success",
"output": {
"stdout": "{\n \"hostname\": \"bench-01\",\n \"platform\": \"linux\",\n \"arch\": \"x64\",\n \"cpuCount\": 8,\n \"totalMemory\": 16654786560,\n \"freeMemory\": 9123409920,\n \"uptime\": 864021,\n \"loadAverage\": [0.4, 0.3, 0.2],\n \"networkInterfaces\": []\n}",
"durationMs": 0
}
}
}The parsed object holds hostname, platform, arch, cpuCount, totalMemory, freeMemory,
uptime, loadAverage and networkInterfaces.
get_serial_ports
Takes no arguments. Returns the serial ports available on the machine, each with its path,
manufacturer, serialNumber, vendorId and productId where the platform reports them.
Like get_system_info, the answer is a JSON string in output.stdout β an array of port
objects, or [] when the serialport native bindings are missing.
open_serial
Opens a serial port on the agent.
| Argument | Type | Default | Meaning |
|---|---|---|---|
path | string | β | The port path, e.g. /dev/ttyUSB0 or COM3. Required. |
baudRate | number | 115200 | The baud rate to open at. |
{
"command": "open_serial",
"args": { "path": "/dev/ttyUSB0", "baudRate": 115200 }
}path and baudRate are the whole contract of this command. dataBits, stopBits and parity
exist only on the agent's own local HTTP surface, POST /api/serial, which runs on the machine
under test and is not reachable from the studio.
The local endpoint takes them:
{ "action": "open", "path": "/dev/ttyUSB0", "baudRate": 115200, "dataBits": 8, "stopBits": 1, "parity": "none" }close_serial
Closes a port the agent has open. Takes path.
serial_write
Writes to a port the agent already has open.
| Argument | Type | Default | Meaning |
|---|---|---|---|
path | string | β | The port to write to. Required. |
data | string | β | The bytes to write. Required. |
The result reports whether the write succeeded: stdout reads Data sent to <path> or Failed to send to <path>. A write to a port the agent does not have open is refused and the result is
status: "error".
screen_watch
Starts or stops the screen watch on a device under test. While the watch is on, the bench sends a
screen_frame event for every frame it takes.
| Argument | Type | Default | Meaning |
|---|---|---|---|
platform | string | β | ios, android or harmonyos. Required. |
deviceId | string | β | The device to watch. Required. |
enabled | string | β | "true" starts the watch, "false" stops it. Required. |
intervalMs | number | 1000 | How often to take a frame, in milliseconds. |
The bench takes one frame per device per tick, and skips a tick when the previous capture is still running β a slow device ends up at a lower frame rate rather than a growing queue.
The frames themselves travel as screen_frame events, not as the
result of this command. The command's own result says only whether the watch was started or
stopped.
run_test_suite
Runs a test suite on the device under test. This is the command the platform's test runner issues when it executes a suite.
| Argument | Type | Default | Meaning |
|---|---|---|---|
suiteId | string | β | The suite to run. Required. |
projectId | string | β | The project the run belongs to. Required. |
deviceId | string | β | The device the suite runs on. The platform checks that this device can automate before it hands the task over; if it cannot, the platform prepares the device and the task goes out when the device is ready. |
The result is a normal command result: status, the output of the runner, and β on failure β an
error with a code and a message.
upload_artifact
Uploads an artifact from the bench to the studio.
| Argument | Type | Default | Meaning |
|---|---|---|---|
filePath | string | β | The absolute path of the file on the device under test. Required. |
The file is read on the agent and transferred to the studio, where it is kept with the artifacts of the project. The result is a normal command result.
See also
- Remote Shell and Serial β the same commands, in use.
- Socket Protocol β how the envelope travels.
- API Reference β the endpoints that carry commands.
- Error Codes β the codes above, in one list.