Command Execution
The exec endpoints let you run commands inside the workspace VM. Commands can run synchronously (wait for result) or asynchronously (stream output via SSE). Long-running tasks persist in the background and can be polled, streamed, or killed.
Requires the internal server to be enabled.
Overview
Two execution modes:
| Mode | How | Use case |
|---|---|---|
| Synchronous | POST /exec (no stream flag) | Quick commands - get stdout/stderr in response |
| Streaming | POST /exec with stream: true, or GET /exec/stream?task_id=ID | Long-running tasks - real-time output via SSE |
Run command
Execute a command inside the workspace.
// Synchronous - wait for result
const result = await ws.exec.run('ws_a1b2c3d4', ['echo', 'hello']);
console.log(result.exit_code); // 0
console.log(result.stdout); // "hello\n"
console.log(result.stderr); // ""// Streaming - real-time output
const task = await ws.exec.run('ws_a1b2c3d4', ['npm', 'install'], { stream: true });
task.on('stdout', (data) => process.stdout.write(data));
task.on('stderr', (data) => process.stderr.write(data));
task.on('exit', (code) => console.log(`Done: ${code}`));Synchronous:
POST https://workspace.oblien.com/exec
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
{
"cmd": ["echo", "hello"]
}Streaming (SSE):
POST https://workspace.oblien.com/exec
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
{
"cmd": ["npm", "install"],
"stream": true
}# Synchronous
curl -X POST "https://workspace.oblien.com/exec" \
-H "Authorization: Bearer $GATEWAY_JWT" \
-H "Content-Type: application/json" \
-d '{"cmd": ["echo", "hello"]}'
# Streaming (SSE)
curl -N -X POST "https://workspace.oblien.com/exec" \
-H "Authorization: Bearer $GATEWAY_JWT" \
-H "Content-Type: application/json" \
-d '{"cmd": ["npm", "install"], "stream": true}'Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
cmd | string[] | Yes | The command to execute as an array (e.g. ["node", "app.js"]) |
stream | boolean | No | If true, returns SSE stream instead of waiting |
exec_mode | string | No | auto (default), shell, or direct |
timeout_seconds | integer | No | Kill command after N seconds. Default 0 (no timeout) |
ttl_seconds | integer | No | Keep task metadata for N seconds after exit. Default 0 (uses 5-minute server default). Set to -1 to never expire |
keep_logs | boolean | No | Retain stdout/stderr after completion. Default false |
Execution modes
| Mode | Behavior |
|---|---|
auto | Uses shell if cmd contains shell metacharacters (|, &, ;, etc.), otherwise runs directly |
shell | Always wraps in /bin/sh -c "..." |
direct | Splits and runs directly - no shell interpretation |
Synchronous response
The response is the full task object:
{
"id": "abc123",
"command": ["echo", "hello"],
"status": "exited",
"guest_pid": 4521,
"exit_code": 0,
"stdout": "hello\n",
"stderr": "",
"created_at": "2025-01-15T10:30:00Z",
"started_at": "2025-01-15T10:30:00Z",
"exited_at": "2025-01-15T10:30:01Z",
"ttl_seconds": 300
}Streaming response (SSE)
When stream: true, the response is an SSE stream. The data payloads are JSON. stdout/stderr content is base64-encoded:
event: task_id
data: {"task_id":"abc123"}
event: stdout
data: {"data":"SW5zdGFsbGluZyBkZXBlbmRlbmNpZXMuLi4="}
event: stderr
data: {"data":"bnBtIHdhcm4gZGVwcmVjYXRlZA=="}
event: exit
data: {"exit_code": 0, "pid": 4521}| Event | Data format | Description |
|---|---|---|
task_id | {"task_id": "..."} | Task identifier for future polling |
stdout | {"data": "..."} | Standard output chunk (base64-encoded) |
stderr | {"data": "..."} | Standard error chunk (base64-encoded) |
exit | {"exit_code": N, "pid": N} | Process finished |
List tasks
List all tracked tasks.
const tasks = await ws.exec.list('ws_a1b2c3d4');
for (const task of tasks) {
console.log(`${task.id}: ${task.command.join(' ')} (status: ${task.status})`);
}GET https://workspace.oblien.com/exec
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...curl "https://workspace.oblien.com/exec" \
-H "Authorization: Bearer $GATEWAY_JWT"Response
{
"success": true,
"tasks": [
{
"id": "abc123",
"command": ["npm", "install"],
"status": "running",
"guest_pid": 4521,
"created_at": "2025-01-15T10:30:00Z",
"started_at": "2025-01-15T10:30:00Z",
"ttl_seconds": 300
}
]
}Task status values: pending, running, exited, failed.
Get task
Get the status and output of a specific task.
const task = await ws.exec.get('ws_a1b2c3d4', 'abc123');
console.log(task.status); // "exited"
console.log(task.exit_code); // 0
console.log(task.stdout); // "full output..."GET https://workspace.oblien.com/exec/abc123
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...curl "https://workspace.oblien.com/exec/abc123" \
-H "Authorization: Bearer $GATEWAY_JWT"Response
{
"id": "abc123",
"command": ["npm", "install"],
"status": "exited",
"guest_pid": 4521,
"exit_code": 0,
"stdout": "added 150 packages in 12s\n",
"stderr": "",
"created_at": "2025-01-15T10:30:00Z",
"started_at": "2025-01-15T10:30:00Z",
"exited_at": "2025-01-15T10:30:12Z",
"ttl_seconds": 300
}Kill task
Remove a task from tracking and close its stdin pipe.
await ws.exec.kill('ws_a1b2c3d4', 'abc123');DELETE https://workspace.oblien.com/exec/abc123
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...curl -X DELETE "https://workspace.oblien.com/exec/abc123" \
-H "Authorization: Bearer $GATEWAY_JWT"Response
{
"success": true
}Delete all tasks
Remove all tasks from tracking and close stdin pipes.
DELETE https://workspace.oblien.com/exec
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...curl -X DELETE "https://workspace.oblien.com/exec" \
-H "Authorization: Bearer $GATEWAY_JWT"Response
{
"success": true,
"deleted": 3
}Send stdin
Send input to a running task's stdin. The request body is sent as raw bytes (not JSON).
await ws.exec.input('ws_a1b2c3d4', 'abc123', 'yes\n');POST https://workspace.oblien.com/exec/abc123/input
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/octet-stream
yescurl -X POST "https://workspace.oblien.com/exec/abc123/input" \
-H "Authorization: Bearer $GATEWAY_JWT" \
--data-binary 'yes
'Response
{
"success": true,
"bytes_written": 4
}Stream output (SSE)
Subscribe to real-time output from a running task. This is useful when you started a task with stream: false or from another client and want to attach to its output.
const stream = await ws.exec.stream('ws_a1b2c3d4', 'abc123');
stream.on('stdout', (data) => process.stdout.write(data));
stream.on('stderr', (data) => process.stderr.write(data));
stream.on('exit', (code) => console.log(`Exited: ${code}`));GET https://workspace.oblien.com/exec/stream?task_id=abc123
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Accept: text/event-streamOr create and stream a new task via POST (alias for POST /exec with stream: true):
POST https://workspace.oblien.com/exec/stream
Authorization: Bearer eyJhbGciOiJIUzI1NiIs...
Content-Type: application/json
{
"cmd": ["npm", "install"]
}curl -N "https://workspace.oblien.com/exec/stream?task_id=abc123" \
-H "Authorization: Bearer $GATEWAY_JWT"SSE events
event: stdout
data: {"data":"SW5zdGFsbGluZyBkZXBlbmRlbmNpZXMuLi4="}
event: stderr
data: {"data":"bnBtIHdhcm4gZGVwcmVjYXRlZA=="}
event: exit
data: {"exit_code": 0, "pid": 4521}When subscribing to a task that has already finished, the server sends an output event with buffered stdout/stderr as raw text, then the exit event.
Error responses
| Status | Meaning |
|---|---|
400 | Missing cmd field or invalid parameters |
401 | Missing or invalid token |
404 | Task not found |
405 | Method not allowed |
429 | Too many tasks (max 50 concurrent) |
500 | Failed to start process |