TypeScript SDK#

@taskcloud/sdk is written by hand against the OpenAPI spec: request and response types mirror the spec's schemas, with a thin layer on top for sandbox handles, streaming, retries and webhook verification. It has no runtime dependencies and runs on Node.js 20+, Bun, Deno and edge runtimes with fetch.

Runtime notes:

  • files.upload(...) with File objects needs a global File (Node.js 20+, Bun, Deno, browsers).
  • pty.connect() needs a global WebSocket (Node.js 22+, Bun, Deno, browsers). On Node.js 20, install ws and pass it in (see PTY).
  • verifyWebhook uses node:crypto (Node.js, Bun, Deno). On edge runtimes use the async verifyWebhookAsync (Web Crypto).
npm install @taskcloud/sdk

Client#

import { Client } from "@taskcloud/sdk";

const client = new Client({
  apiKey: process.env.TASKCLOUD_API_KEY, // default: this env var
  baseUrl: "https://api.task.cloud/v1",       // default
  maxRetries: 2,                               // retries errors with retryable: true
  timeoutMs: 120_000,
});

Naming: methods are camelCase; request and response fields are snake_case, exactly as in the API (auto_pause, timeout_seconds, exit_code).

Sandboxes#

const sandbox = await client.sandboxes.create({
  template: "base",                 // or snapshot_id, or image: { ref: "ghcr.io/acme/env:1" }
  size: "default",                  // small | default | large
  name: "user-42",
  labels: { user: "42" },
  env: { NODE_ENV: "production" },
  auto_pause: { enabled: true, idle_timeout_seconds: 300 },
  always_on: false,
  priority: "standard",             // or "batch" (−50%, may queue)
  ttl_seconds: null,
  ports: [{ port: 3000, visibility: "private" }],
  wait: true,
}, { idempotencyKey: "user-42-create" });

sandbox.id;      // "sbx_..."
sandbox.state;   // "running"

const same = await client.sandboxes.get(sandbox.id);
for await (const s of client.sandboxes.list({ state: ["running"], label: ["user=42"] })) {
  console.log(s.id, s.state);
}

await sandbox.update({ auto_pause: { enabled: true, idle_timeout_seconds: 900 } });
await sandbox.update({ always_on: true });
await sandbox.pause();                 // waits until "paused"
await sandbox.resume();                // usually unnecessary: calls wake it
const fork = await sandbox.fork({ name: "user-42-try-2" });
const forks = await sandbox.fork({ count: 10 });   // Sandbox[]: 10 copies in one call
await sandbox.refresh();               // re-read state
await sandbox.waitFor("running", { timeoutMs: 30_000 });
await sandbox.delete();
Method Endpoint
client.sandboxes.create(params, opts?) POST /sandboxes
client.sandboxes.get(id) GET /sandboxes/{id}
client.sandboxes.list(params?) GET /sandboxes (auto-paginates)
sandbox.update(params) PATCH /sandboxes/{id}
sandbox.pause() / sandbox.resume() POST /sandboxes/{id}/pause / resume
sandbox.fork(params?) POST /sandboxes/{id}/fork
sandbox.forkMany(count, params?) POST /sandboxes/{id}/fork with count, always returns Sandbox[]
sandbox.delete() DELETE /sandboxes/{id}

Bulk fork#

fork({ count }) creates up to 50 copies of a sandbox (memory included) in one call. fork() and fork({ count: 1 }) return one Sandbox, as before; with count > 1 the API returns { "data": [...] } and the SDK returns Sandbox[]. With a literal count (for example { count: 10 }) the return type is Sandbox[]; when count is a number variable it is Sandbox | Sandbox[], so use forkMany(count) to always get an array.

Admission is all-or-nothing: if the source host cannot fit every fork, nothing is created and the call throws ApiError with capacity_unavailable (or concurrency_limit_reached if the plan's limit would be exceeded). With priority: "batch" the forks are queued instead and come back in state queued.

const rollouts = await sandbox.forkMany(32, { labels: { run: "eval-7" } });
const results = await Promise.all(rollouts.map((s) => s.exec({ command: "python3 rollout.py" })));

const queued = await sandbox.forkMany(50, { priority: "batch", wait: false }); // -50%, may queue

Exec#

// Sync
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.truncated);

// Without a shell
await sandbox.exec({ args: ["python3", "-c", "print(42)"] });

// Stream (SSE)
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);
}

// Background
const cmd = await sandbox.exec({ command: "npm run dev", background: true });
for await (const ev of cmd.stream()) { /* attach, replays earlier output */ }
await cmd.writeStdin("q\n", { eof: true });
await cmd.kill({ signal: "SIGTERM" });
const done = await cmd.wait();        // Command with exit_code

await sandbox.commands.list();
await sandbox.commands.get(cmd.id);

PTY#

const term = await sandbox.pty.connect({
  cols: 120, rows: 40,
  onData: (bytes) => process.stdout.write(bytes),
  onExit: (code) => console.log("exit", code),
});
term.write("ls -la\n");
term.resize(160, 48);
term.close();

On Node.js 20, pass a WebSocket implementation: import WebSocket from "ws" and add WebSocket to the pty.connect({ ... }) options.

sandbox.pty.create() returns the raw { id, websocket_url, expires_at } if you want to connect from a browser (for example with xterm.js). Protocol: see Exec and files.

Files#

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/out.png");     // Uint8Array
const entries = await sandbox.files.list("/home/user/app", { recursive: false });
const info = await sandbox.files.stat("/home/user/app");
await sandbox.files.mkdir("/home/user/data");
await sandbox.files.move("/home/user/a.txt", "/home/user/b.txt");
await sandbox.files.remove("/home/user/tmp", { recursive: true });
await sandbox.files.upload("/home/user/app", [new File(["hi"], "src/hello.txt")]);
const tgz = await sandbox.files.downloadArchive("/home/user/app", { format: "tar.gz" });
const { url } = await sandbox.files.downloadUrl({ path: "/home/user/big.bin" });
const up = await sandbox.files.uploadUrl({ path: "/home/user/dataset.parquet", expires_in_seconds: 900 });

for await (const ev of sandbox.files.watch("/home/user/app/src", { recursive: true })) {
  console.log(ev.type, ev.path);
}

Networking#

const port = await sandbox.ports.expose({ port: 3000, visibility: "private" });
port.url; // https://<short_id>-3000.taskhost.cloud
const { token } = await sandbox.accessTokens.create({ ttl_seconds: 3600, ports: [3000] });
await sandbox.ports.list();
await sandbox.ports.remove(3000);

const ssh = await sandbox.ssh({ public_key: "ssh-ed25519 AAAA...", ttl_seconds: 3600 });
ssh.command; // ssh sbx_...@ssh.taskhost.cloud

Desktop#

const stream = await sandbox.desktop.stream({ ttl_seconds: 600, read_only: true });
stream.url;                                   // embed in an <iframe>
await sandbox.desktop.revoke(stream.id);

const shot = await sandbox.desktop.screenshot({ format: "png" }); // { data (base64), media_type, width, height, ... }
await sandbox.desktop.input({
  actions: [
    { action: "left_click", coordinate: [640, 412] },
    { action: "type", text: "hello" },
    { action: "key", text: "Return" },
  ],
  return_screenshot: true,
});
const { x, y } = await sandbox.desktop.cursor();

See Desktop and computer use for a full agent loop.

Snapshots and templates#

const snap = await sandbox.snapshots.create({ name: "after-npm-install" });
const copy = await client.sandboxes.create({ snapshot_id: snap.id });
await client.snapshots.list({ sandbox_id: sandbox.id });
await client.snapshots.delete(snap.id);

const tpl = await client.templates.create({
  name: "agent-env",
  image: { ref: "ghcr.io/acme/agent-env:1.4" },
  ready_check: { command: "curl -sf localhost:8080/health", timeout_seconds: 120 },
});
await client.templates.logs(tpl.id);
await client.sandboxes.create({ template: "agent-env" });

Webhooks, usage, keys, account#

const hook = await client.webhooks.create({
  url: "https://example.com/hooks/taskcloud",
  events: ["sandbox.ready", "sandbox.paused", "sandbox.resumed", "sandbox.error", "sandbox.deleted"],
});
hook.secret; // whsec_... (shown once)

import { verifyWebhook, verifyWebhookAsync } from "@taskcloud/sdk/webhooks";
const event = verifyWebhook({ payload: rawBody, headers: req.headers, secret: process.env.WEBHOOK_SECRET! });
// Edge runtimes (Cloudflare Workers, Vercel Edge): Web Crypto is async-only
const edgeEvent = await verifyWebhookAsync({ payload: rawBody, headers: req.headers, secret: env.WEBHOOK_SECRET });

const usage = await client.usage.get({ group_by: "sandbox" });
const limits = await client.limits.get();
const key = await client.apiKeys.create({ name: "user-42", preset: "full-sandbox", restrict: { labels: { user: "42" } } });
await client.account.setSpendLimit({ monthly_limit_usd: 500, on_limit: "pause_running" });

Errors#

Every API error becomes an ApiError:

import { ApiError } from "@taskcloud/sdk";

try {
  await client.sandboxes.create({ size: "large" });
} catch (err) {
  if (err instanceof ApiError) {
    console.log(err.status, err.code, err.retryable, err.requestId, err.details);
    if (err.code === "concurrency_limit_reached") { /* pause something first */ }
  } else throw err;
}

Errors with retryable: true (rate_limited, capacity_unavailable, sandbox_busy, ...) are retried automatically up to maxRetries, honouring Retry-After.