Exec and files#
Run commands in a sandbox (sync, in the background, as an SSE stream, or in an interactive PTY) and move files in and out. All calls wake a paused sandbox automatically unless you pass auto_resume=false.
Run a command (sync)#
POST /sandboxes/{sandbox_id}/exec waits for the command to exit and returns ExecResult.
const r = await sandbox.exec({ command: "npm test", cwd: "/home/user/app", timeout_seconds: 300 });
console.log(r.exit_code, r.stdout, r.stderr, r.duration_ms);
r = sandbox.exec(command="npm test", cwd="/home/user/app", timeout_seconds=300)
print(r.exit_code, r.stdout)
curl -sS -X POST https://api.task.cloud/v1/sandboxes/sbx_2fqxnt4bkq7m/exec \
-H "Authorization: Bearer $TASKCLOUD_API_KEY" -H "Content-Type: application/json" \
-d '{"command":"npm test","cwd":"/home/user/app","timeout_seconds":300}'
# {"command_id":"cmd_9fz2cq4kd7mw","exit_code":0,"stdout":"...","stderr":"","truncated":false,"duration_ms":8123}
taskcloud exec sbx_2fqxnt4bkq7m --cwd /home/user/app --timeout 300 -- npm test
Request fields:
| Field | Default | Notes |
|---|---|---|
command |
– | run with /bin/bash -lc |
args |
– | program + arguments, executed without a shell (use instead of command) |
cwd |
/home/user |
|
env |
– | extra variables for this command |
user |
user |
user has passwordless sudo; root is allowed |
stdin |
– | written to stdin, then closed (sync and stream) |
timeout_seconds |
60 | max 600; sync and stream only |
background |
false |
return immediately, no time limit |
stream |
false |
respond with SSE (same as Accept: text/event-stream) |
- A non-zero exit code is a normal
200response, not an error. - If the command exceeds
timeout_seconds, it is killed and the API returns408 exec_timeoutwith partial output indetails. - Captured output is limited to 8 MiB per stream;
truncated: truetells you it was cut. Use stream mode for large output. - Non-UTF-8 output is returned base64-encoded with
encoding: "base64". - At least 10 commands can run concurrently in one sandbox.
Background commands#
For dev servers, long builds and agents: background: true returns 202 with a Command right away. There is no time limit.
const server = await sandbox.exec({ command: "npm run dev -- --port 3000", cwd: "/home/user/app", background: true });
console.log(server.id); // cmd_...
// later
for await (const ev of server.stream()) {
if (ev.type === "stdout") process.stdout.write(ev.data);
if (ev.type === "exit") console.log("exited", ev.exit_code);
}
await server.kill({ signal: "SIGTERM" });
server = sandbox.exec(command="npm run dev -- --port 3000", cwd="/home/user/app", background=True)
for ev in server.stream():
if ev.type == "stdout":
print(ev.data, end="")
server.kill(signal="SIGTERM")
taskcloud exec sbx_2fqxnt4bkq7m --background -- npm run dev -- --port 3000
taskcloud commands list sbx_2fqxnt4bkq7m
taskcloud commands logs sbx_2fqxnt4bkq7m cmd_9fz2cq4kd7mw --follow
taskcloud commands kill sbx_2fqxnt4bkq7m cmd_9fz2cq4kd7mw
Endpoints:
| Endpoint | Purpose |
|---|---|
GET /sandboxes/{sandbox_id}/commands |
recent commands (last 7 days), filter state |
GET /sandboxes/{sandbox_id}/commands/{command_id} |
state: running, exited, killed, timed_out, lost |
GET .../commands/{command_id}/stream |
SSE; replays earlier output (up to 8 MiB per stream); resume with Last-Event-ID |
POST .../commands/{command_id}/stdin |
{data, encoding, eof} |
POST .../commands/{command_id}/kill |
{signal}; default SIGTERM |
A running command counts as activity, so the sandbox will not auto-pause while it runs. For a service that should survive pause/resume and forks, a systemd unit is more robust than a background command.
Streaming output (SSE)#
Stream mode returns Server-Sent Events: start, stdout, stderr, exit, error. There is no output limit.
for await (const ev of sandbox.execStream({ command: "pip install -r requirements.txt" })) {
if (ev.type === "stdout" || ev.type === "stderr") process.stdout.write(ev.data);
if (ev.type === "exit") console.log("exit", ev.exit_code);
}
for ev in sandbox.exec_stream(command="pip install -r requirements.txt"):
if ev.type in ("stdout", "stderr"):
print(ev.data, end="")
curl -sS -N -X POST https://api.task.cloud/v1/sandboxes/sbx_2fqxnt4bkq7m/exec \
-H "Authorization: Bearer $TASKCLOUD_API_KEY" -H "Content-Type: application/json" \
-H "Accept: text/event-stream" \
-d '{"command":"pip install -r requirements.txt"}'
event: start
data: {"type":"start","command_id":"cmd_9fz2cq4kd7mw","pid":4211}
event: stdout
data: {"type":"stdout","command_id":"cmd_9fz2cq4kd7mw","data":"Collecting requests\n"}
event: exit
data: {"type":"exit","command_id":"cmd_9fz2cq4kd7mw","exit_code":0,"duration_ms":8123}
Interactive terminal (PTY over WebSocket)#
POST /sandboxes/{sandbox_id}/pty starts a pseudo-terminal and returns websocket_url, a signed URL valid for 60 s until first connect. It needs no Authorization header, so you can hand it to a browser terminal such as xterm.js.
Protocol:
- Binary frames: raw keystrokes (client → server) and raw terminal output (server → client).
- Text frames (JSON): client sends
{"type":"resize","cols":120,"rows":40},{"type":"signal","signal":"SIGINT"},{"type":"ping"}; server sends{"type":"ready","pty_id":"..."},{"type":"exit","exit_code":0},{"type":"pong"},{"type":"error","error":{...}}. - After a drop, call
GET .../pty/{pty_id}within 60 s for a fresh URL; up to 64 KiB of missed output is replayed. - The session ends when the process exits or after
idle_timeout_seconds(default 1800) without traffic. An open PTY prevents auto-pause.
const term = await sandbox.pty.connect({
cols: 120,
rows: 40,
onData: (bytes) => process.stdout.write(bytes),
});
term.write("htop\r");
term.resize(160, 48);
term.close();
# Raw API
curl -sS -X POST https://api.task.cloud/v1/sandboxes/sbx_2fqxnt4bkq7m/pty \
-H "Authorization: Bearer $TASKCLOUD_API_KEY" -H "Content-Type: application/json" \
-d '{"cols":120,"rows":40}'
# {"id":"pty_7hc2zq5mxk3d","state":"waiting","websocket_url":"wss://api.task.cloud/v1/sandboxes/sbx_2fqxnt4bkq7m/pty/pty_7hc2zq5mxk3d/connect?token=pt_...","expires_at":"..."}
# Or simply
taskcloud shell sbx_2fqxnt4bkq7m
Files#
Paths are absolute, or relative to /home/user.
Read and write#
await sandbox.files.write("/home/user/app/.env", "PORT=3000\n", { mode: "0600" });
const text = await sandbox.files.readText("/home/user/app/package.json");
const bytes = await sandbox.files.read("/home/user/app/logo.png"); // Uint8Array
sandbox.files.write("/home/user/app/.env", "PORT=3000\n", mode="0600")
text = sandbox.files.read_text("/home/user/app/package.json")
data = sandbox.files.read("/home/user/app/logo.png") # bytes
# write (raw body)
curl -sS -X PUT "https://api.task.cloud/v1/sandboxes/sbx_2fqxnt4bkq7m/files/content?path=/home/user/app/.env&mode=0600" \
-H "Authorization: Bearer $TASKCLOUD_API_KEY" -H "Content-Type: application/octet-stream" \
--data-binary @.env
# read (raw bytes; supports Range)
curl -sS "https://api.task.cloud/v1/sandboxes/sbx_2fqxnt4bkq7m/files/content?path=/home/user/app/package.json" \
-H "Authorization: Bearer $TASKCLOUD_API_KEY"
PUT .../files/contentcreates parent directories by default (create_parents=true), acceptsmodeandowner, and supportsIf-Matchwith the file'sETag(412 precondition_failedon mismatch).GET .../files/contentstreams the file and supportsRange.- Up to 1 GiB per request; larger files return
413 payload_too_large. Use signed URLs for those.
List, stat, mkdir, move, delete#
const entries = await sandbox.files.list("/home/user/app", { recursive: true });
const info = await sandbox.files.stat("/home/user/app/package.json");
await sandbox.files.mkdir("/home/user/data");
await sandbox.files.move("/home/user/a.txt", "/home/user/data/a.txt");
await sandbox.files.remove("/home/user/tmp", { recursive: true });
| Endpoint | Notes |
|---|---|
GET /sandboxes/{sandbox_id}/files?path= |
recursive, max_depth (default 8), paginated |
GET .../files/stat?path= |
FileEntry: name, path, type, size, mode, owner, modified_at, etag |
POST .../files/mkdir |
{path, parents, mode} |
POST .../files/move |
{from, to, overwrite} |
DELETE /sandboxes/{sandbox_id}/files?path= |
recursive=true for non-empty directories |
Upload and download many files#
await sandbox.files.upload("/home/user/app", {
"src/index.ts": "console.log('hi')\n", // relative name -> string, Uint8Array or Blob
"package.json": pkgJson,
});
const archive = await sandbox.files.downloadArchive("/home/user/app/dist", { format: "tar.gz" });
sandbox.files.upload("/home/user/app", [("src/index.ts", b"print('hi')\n")])
archive = sandbox.files.download_archive("/home/user/app/dist", format="tar.gz")
# multipart upload; filenames may include sub-directories
curl -sS -X POST https://api.task.cloud/v1/sandboxes/sbx_2fqxnt4bkq7m/files/upload \
-H "Authorization: Bearer $TASKCLOUD_API_KEY" \
-F path=/home/user/app -F extract=true -F files=@repo.tar.gz
# directory as an archive (tar.gz, tar or zip)
curl -sS -o dist.tar.gz "https://api.task.cloud/v1/sandboxes/sbx_2fqxnt4bkq7m/files/download?path=/home/user/app/dist&format=tar.gz" \
-H "Authorization: Bearer $TASKCLOUD_API_KEY"
taskcloud cp -r ./app sbx_2fqxnt4bkq7m:/home/user/app
taskcloud cp sbx_2fqxnt4bkq7m:/home/user/app/report.pdf ./report.pdf
Signed URLs (large files, browsers)#
Pre-signed URLs work without an API key, for a single PUT (upload) or GET (download) of up to 10 GiB. Default lifetime 900 s, max 3600 s.
const up = await sandbox.files.uploadUrl({ path: "/home/user/data/dataset.parquet", expires_in_seconds: 900 });
// up.method === "PUT"; give up.url to your user's browser or a worker
const down = await sandbox.files.downloadUrl({ path: "/home/user/app/out.zip" });
up = sandbox.files.upload_url(path="/home/user/data/dataset.parquet")
down = sandbox.files.download_url(path="/home/user/app/out.zip")
Watch for changes#
GET .../files/watch?path= streams FileEvents (created, modified, deleted, renamed) as SSE. Options: recursive (default true), include_hidden (default false). A watch does not keep the sandbox awake; it stays open across pause/resume and delivers events again after the VM wakes.
for await (const ev of sandbox.files.watch("/home/user/app/src", { recursive: true })) {
console.log(ev.type, ev.path);
}
for ev in sandbox.files.watch("/home/user/app/src", recursive=True):
print(ev.type, ev.path)
taskcloud files watch sbx_2fqxnt4bkq7m /home/user/app/src
Errors you will see#
| Status | code |
Retry? | Meaning |
|---|---|---|---|
| 404 | not_found |
no | sandbox, command or path does not exist |
| 408 | exec_timeout |
no | sync command killed after timeout_seconds |
| 409 | sandbox_paused |
no | only with auto_resume=false |
| 409 | sandbox_busy |
yes | transient; retry after Retry-After |
| 413 | payload_too_large |
no | use signed URLs |
| 502 | sandbox_unreachable |
per retryable |
guest did not answer; the command may have run |
Always branch on error.code and error.retryable:
import { ApiError } from "@taskcloud/sdk";
try {
await sandbox.exec({ command: "make build", timeout_seconds: 600 });
} catch (e) {
if (e instanceof ApiError && e.retryable) { /* back off and retry */ }
else throw e;
}
Related#
- Networking: expose the dev server you just started
- Lifecycle
- Limits
- API reference