Runs and exec
One-shot runs, session exec, streaming output, interactive PTYs and cancellation.
POST /v1/runs one-shot, or against an existing session
POST /v1/sessions/{id}/exec exec in a session (alias that produces a Run)
GET /v1/runs/{id} read
GET /v1/runs list
DELETE /v1/runs/{id} cancel
GET /v1/runs/{id}/logs fetch output after the fact
WS /v1/sessions/{id}/pty interactive terminal
One-shot run
No session to manage. The platform creates one, runs, returns and releases.
curl -sS "$WB_BASE_URL/runs" \
-H "Authorization: Bearer $WB_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"tier": 1,
"runtime": "python3.12",
"code": "import json,sys; print(json.dumps({\"n\": sum(range(10**6))}))",
"timeout_ms": 15000,
"end_user_id": "eu_customer_4821"
}'
{
"id": "run_8Hc2Vd",
"object": "run",
"session_id": "sess_ephemeral_3Jd9",
"kind": "code",
"status": "succeeded",
"exit_code": 0,
"stdout": "{\"n\": 499999500000}\n",
"stderr": "",
"started_at": "2026-10-06T09:14:22.118Z",
"finished_at": "2026-10-06T09:14:22.503Z",
"duration_ms": 385,
"usage": { "cpu_ms": 362, "mem_gib_ms": 46080, "session_s": 1, "egress_bytes": 0, "cold_start": false }
}
One-shot runs are the right shape for independent work where a fresh filesystem per call is a feature — evaluation harnesses, untrusted multi-tenant code. They are the wrong shape for an agent loop against one workspace, because each pays a session start.
Exec in a session
curl -sS "$WB_BASE_URL/sessions/$SESSION/exec" \
-H "Authorization: Bearer $WB_API_KEY" -H "Content-Type: application/json" \
-d '{
"command": ["bash", "-lc", "pnpm install && pnpm test"],
"cwd": "/workspace/app",
"env": { "CI": "true" },
"timeout_ms": 600000
}'
Equivalent to POST /runs with session_id. Both return a Run.
Request shapes by kind
kind | Fields | Tiers |
|---|---|---|
code | runtime, code, optional files | 1–4 |
command | command (argv array), cwd, env, stdin | 2–4 |
browser | actions, observe | 3, 4 |
desktop | actions, observe_after | 4 |
tool | tool, arguments | 0–4 |
command is an argv array, never a string. A string would need shell parsing somewhere, and the
somewhere is always ambiguous about quoting. To use a shell explicitly, say so:
["bash", "-lc", "…"].
| Field | Default | Notes |
|---|---|---|
timeout_ms | 60000 | Max 3600000; the process is killed on expiry |
stdin | — | String or base64 |
files | — | Written before execution: [{ path, content_b64, mode }] |
capture | ["stdout","stderr"] | [] discards output, useful for noisy builds |
max_output_bytes | 1048576 | Beyond this, output is truncated with a flag |
Streaming
curl -N "$WB_BASE_URL/sessions/$SESSION/exec?stream=true" \
-H "Authorization: Bearer $WB_API_KEY" \
-H "Accept: text/event-stream" \
-H "Content-Type: application/json" \
-d '{ "command": ["bash", "-lc", "pnpm build"] }'
event: start
id: 0
data: {"run_id":"run_8Hc2Vd","started_at":"2026-10-06T09:14:22.118Z"}
event: stdout
id: 1
data: {"text":"> build\n> vite build\n"}
event: stderr
id: 2
data: {"text":"warning: unused export\n"}
event: exit
id: 148
data: {"exit_code":0,"duration_ms":18442,"usage":{"cpu_ms":14320,"egress_bytes":48211004}}
| Event | Meaning |
|---|---|
start | Run began; carries run_id |
stdout, stderr | Output chunk, UTF-8, not line-aligned |
progress | Platform-level progress, e.g. still provisioning |
exit | Terminal: exit code, duration, usage |
error | Terminal: the run failed; carries a problem document |
Reconnect with Last-Event-Id to resume rather than re-run:
curl -N "$WB_BASE_URL/runs/run_8Hc2Vd/logs?stream=true" \ -H "Last-Event-Id: 2" -H "Authorization: Bearer $WB_API_KEY"
A stream ending without exit or error is a transport failure, not a finished run.
for await (const ev of wb.sessions.execStream(session.id, { command: ['make', 'test'] })) {
if (ev.type === 'stdout') process.stdout.write(ev.text)
if (ev.type === 'exit') return ev.exit_code
if (ev.type === 'error') throw new RunError(ev)
}
Interactive PTY
wss://connect.workspaces.build/v1/sessions/{id}/pty?token=wb_st_…
A real terminal: job control, Ctrl-C, full-screen programs, resize.
// client -> server
{ "type": "input", "data": "ls -la\n" }
{ "type": "resize", "cols": 120, "rows": 40 }
{ "type": "signal", "signal": "SIGINT" }
// server -> client
{ "type": "output", "data": "total 24\ndrwxr-xr-x …" }
{ "type": "exit", "code": 0 }
Use a session token rather than an API key when the terminal is in an end user's browser. See authentication.
Concurrency within a session
Multiple runs can be in flight in one session, up to max_concurrent_runs (default 4, raise to the
session's vCPU count). They share the filesystem and the resource envelope, so a run that saturates
the CPU slows its siblings.
For tier 1 sessions with persist_process: true, runs are serialised — there is one interpreter,
and interleaving execution in it would be incoherent.
Cancellation
curl -sS -X DELETE "$WB_BASE_URL/runs/run_8Hc2Vd" -H "Authorization: Bearer $WB_API_KEY"
SIGTERM, then SIGKILL after a 5-second grace period. The run becomes cancelled. The session
keeps running — cancel the run to stop a command, delete the session to stop paying.
Closing a stream does not cancel the run. Streams are observers; the run continues and bills.
Output handling
Output above max_output_bytes is truncated:
{
"stdout": "…first 1 MiB…",
"stdout_truncated": true,
"stdout_total_bytes": 48211004,
"stdout_ref": "https://api.workspaces.build/v1/runs/run_8Hc2Vd/logs?stream=stdout"
}
Full output stays retrievable for seven days. For genuinely large output, redirect to a file in the session and read it through the filesystem API — streaming a 2 GiB build log through SSE is slower and more expensive than writing it to disk and fetching the part you need.
Usage attribution
Every run carries its share of the session's meters:
"usage": { "cpu_ms": 14320, "mem_gib_ms": 288400, "egress_bytes": 48211004, "wall_ms": 18442 }
cpu_ms and egress_bytes are measured per run. session_s is not divided across concurrent runs
— the session is billed once for its wall clock regardless of how many runs occupy it, which is
precisely why one session with many runs is cheaper than many one-shot runs.