Internal API

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:

ModeHowUse case
SynchronousPOST /exec (no stream flag)Quick commands - get stdout/stderr in response
StreamingPOST /exec with stream: true, or GET /exec/stream?task_id=IDLong-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

ParameterTypeRequiredDescription
cmdstring[]YesThe command to execute as an array (e.g. ["node", "app.js"])
streambooleanNoIf true, returns SSE stream instead of waiting
exec_modestringNoauto (default), shell, or direct
timeout_secondsintegerNoKill command after N seconds. Default 0 (no timeout)
ttl_secondsintegerNoKeep task metadata for N seconds after exit. Default 0 (uses 5-minute server default). Set to -1 to never expire
keep_logsbooleanNoRetain stdout/stderr after completion. Default false

Execution modes

ModeBehavior
autoUses shell if cmd contains shell metacharacters (|, &, ;, etc.), otherwise runs directly
shellAlways wraps in /bin/sh -c "..."
directSplits 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}
EventData formatDescription
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

yes
curl -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-stream

Or 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

StatusMeaning
400Missing cmd field or invalid parameters
401Missing or invalid token
404Task not found
405Method not allowed
429Too many tasks (max 50 concurrent)
500Failed to start process