Skip to content
sistemo.io beta

Sandbox API (JavaScript / TypeScript)

import { Sandbox } from "@sistemo/sdk";

Sandbox.create(options?)

Provisions a real microVM and resolves once it has booted.

new Sandbox() does not work — the constructor is private

Creating a sandbox is asynchronous (it blocks until the microVM has booted), and a JavaScript constructor cannot await. Sandbox.create() is the only way in; new Sandbox({ apiKey }) throws a TypeError. The Python SDK spells it Sandbox() because Python can do the work in __init__ — if you are porting an example across, this is the line that changes.

const sb = await Sandbox.create({
  apiKey:        undefined,   // defaults to SISTEMO_API_KEY
  baseUrl:       undefined,   // defaults to SISTEMO_BASE_URL or https://api.sistemo.io
  timeoutMs:     120000,      // per-request timeout (create blocks until boot)
  name:          "",
  vcpus:         1,
  memoryMb:      1024,
  rootfsSizeGb:  10,
  stack:         "base",      // base | python | node | ai | debian13 | ubuntu26.04
  metadata:      {},
  rootVolumeId:  undefined,   // boot an existing detached root disk
  idempotencyKey: undefined,  // defaults to a fresh UUID
});
Option Default Notes
apiKey SISTEMO_API_KEY Must be a full-scope key to create/run
baseUrl https://api.sistemo.io Override the API endpoint
vcpus / memoryMb / rootfsSizeGb 1 / 1024 / 10 The machine shape
stack "base" Base image; see Images
metadata {} Stored with the machine
rootVolumeId — Boot a disk kept by an earlier close(true). Size and stack come from the volume, so rootfsSizeGb and stack are ignored
idempotencyKey random UUID See below

SandboxOptions extends ClientOptions, so timeoutMs, maxRetries, retryBackoffMs and retryMaxBackoffMs can all be passed here too.

Properties: sb.id (machine UUID), sb.state.

The defaults fit the free quota

1 vCPU / 1024 MB / 10 GB fits a free account (up to 2 vCPU / 2 GB per machine, 3 machines). That is what keeps a bare Sandbox.create() working on a new account — and it means raising a default breaks first-time users unless their quota is raised too.

sb.run(script, timeoutSec = 30)

const r = await sb.run("ls -la /", 60);

Resolves to an ExecResult:

Field Type Meaning
exitCode number Process exit code (-1 if unknown)
stdout string Standard output
stderr string Standard error

timeoutSec defaults to 120 (ceiling 24 hours), enforced in the machine. This is start + wait on POST /execs. For streaming or reconnect use start() — long-running.

sb.start()

Start a command that outlives this request. Resolves to a Job immediately.

const job = await sb.start("npm ci && npm run build", 3600);
for await (const chunk of job.stream()) {
  process.stdout.write(chunk);
}
const result = await job.wait();
console.log(result.state, result.exitCode);

timeoutSec is the job's lifetime in the machine (default 1 hour, max 24). Pass { execId } to reconnect after a crash.

Job
id / state / exitCode Handle, status, code (null until known)
done / ok Terminal? Exited 0?
truncated Output hit the 32 MiB cap
wait(timeoutSec?) Until terminal. Client timeout does not cancel the job
stream(stream?) Async-iterate output as it appears
output(stream?) Bytes so far
cancel() SIGTERM, then SIGKILL
sb.job(id) / sb.jobs() Re-attach / list

If start throws ExecStartUnconfirmed, the command may be running. Poll sb.job(err.execId) — do not start again.

sb.close(preserveStorage = false)

await sb.close();                 // destroy disk too
await sb.close(true);             // keep the volume

Idempotency — why a retry cannot double-boot

Creating a machine sends an Idempotency-Key header. If the response is lost to a dropped connection, the retry returns the machine that was already created instead of booting a second one you would be billed for and never see.

The SDK generates a fresh UUID per create(), which is right for almost everyone. Pass your own only when your code may re-enter the same logical create:

const sb = await Sandbox.create({ idempotencyKey: `job-${jobId}` });

Timeouts and retries

const sb = await Sandbox.create({
  timeoutMs:          120_000,   // per request; create blocks until boot
  maxRetries:         2,         // extra attempts after the first → up to 3 tries
  retryBackoffMs:     400,       // exponential, with jitter
  retryMaxBackoffMs:  8_000,
});

What gets retried:

Retried on 408/429/5xx? Retried on a dropped connection?
GET, DELETE Yes Yes
POST with an idempotency key (create) Yes Yes
POST without one — including run() No Yes

run() is never retried on an HTTP error, on purpose

A replayed exec runs your script a second time. Since the SDK cannot know whether your script is safe to repeat, it does not decide for you: an HTTP error from run() is yours to handle. Connection failures before a response are still retried, because nothing ran.

Low-level Client (advanced)

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

const c = new Client();                       // reads env, or pass { apiKey, baseUrl }

const machines = await c.request("GET", "/v1/machines");
await c.request("POST", `/v1/machines/${id}/stop`);
const quotas   = await c.request("GET", "/v1/quotas");
request<T>(method: string, path: string, body?: unknown, opts?: {
  headers?: Record<string, string>;
  idempotencyKey?: string;      // enables safe retries for a mutating method
  maxRetries?: number;          // override for this call
}): Promise<T>

See the API reference for paths and shapes.

Volume helpers

Client wraps the volume endpoints with typed methods, so you do not have to hand-write the paths:

const { volumes } = await c.listVolumes();                 // root and data
const vol  = await c.createVolume("agent-data", 10);       // name, sizeGb
await c.attachVolume(machineId, vol.id);                   // machine must be STOPPED
await c.detachVolume(machineId, vol.id);
await c.deleteVolume(vol.id);
Method Returns
listVolumes() { volumes: Volume[] } — never undefined, empty is []
createVolume(name, sizeGb) Volume
attachVolume(machineId, volumeId) { status: string }
detachVolume(machineId, volumeId) { status: string }
deleteVolume(volumeId) { status: string }

Volume is { id, name, size_gb, role, state, machine_id? }. A detached root volume can be booted onto a new sandbox with rootVolumeId — that is how you keep a disk and come back to it. See Persist data with volumes.

Python has the same helpers, but listVolumes unwraps differently

sistemo's Client ships list_volumes / create_volume / attach_volume / detach_volume / delete_volume too. The one difference: Python's list_volumes() returns the list directly, while listVolumes() here returns { volumes }. Both normalise the API's null to empty.

Types

The package ships .d.ts types — Sandbox, SandboxOptions, ExecResult, Client, ClientOptions, and the error classes are all typed.