Sandbox API (JavaScript / TypeScript)¶
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)¶
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)¶
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:
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.