orkestr
v0.1.4
Published
EU-hosted sandbox VMs for AI agents - official JavaScript / TypeScript SDK
Maintainers
Readme
orkestr JavaScript SDK
EU-hosted sandbox VMs for AI agents. TypeScript-first client for the
api.orkestr.eu/v1/sandboxes REST API. Ships ESM and CJS dual builds.
Status: 0.1.0 - first stable release. The client surface may still have breaking changes within the 0.x line; 1.0.0 will signal API stability.
Install
npm install orkestr
# or
pnpm add orkestrNode 18+ or any modern browser-grade runtime (Bun, Deno via npm: prefix).
Works in serverless edge runtimes that ship fetch.
Authenticate
Mint an API token in the orkestr console with the sandboxes:write scope.
export ORKESTR_API_KEY="ork_..."The SDK picks the key up from ORKESTR_API_KEY or you pass it explicitly.
Quick start
One-shot execution inside a fresh sandbox. withTemp ensures the sandbox
is terminated when the callback returns, even if it throws.
import { Sandbox } from "orkestr";
await Sandbox.withTemp({ template: "python-3.12" }, async (sbx) => {
await sbx.files.write("/workspace/main.py", "print(sum(range(1_000_000)))");
const result = await sbx.exec("python /workspace/main.py");
console.log(result.stdout); // 499999500000
console.log(result.durationMs); // ~120
});API
Create a sandbox
const sbx = await Sandbox.create({
template: "python-3.12", // one of the templates below
size: "small", // "small" | "medium" | "large" (plan-capped)
network: "off", // "off" | "restricted" | "open"
timeoutSeconds: 600, // auto-terminate after this many seconds
env: { OPENAI_API_KEY: "sk-..." },
metadata: { agentRun: "r_123" },
region: "fsn1", // "fsn1" | "hel1" | undefined for auto
apiKey: process.env.ORKESTR_API_KEY,
});
console.log(sbx.id); // "sbx_01HXYZ..."
console.log(sbx.status); // "running"Templates
| Template | Description |
|---------------------|------------------------------------------|
| python-3.12 | CPython 3.12 with pip and common libs |
| python-3.12-bare | CPython 3.12 only, faster start |
| node-22 | Node 22 with npm |
| ubuntu-24.04 | Minimal Ubuntu shell environment |
Sizes
size picks from a fixed menu. Allowed sizes are plan-capped.
| Size | vCPU | RAM | Plans |
|----------|------|--------|------------------------|
| small | 0.5 | 512 MB | free, pro, team |
| medium | 2 | 4 GB | pro, team |
| large | 4 | 8 GB | team |
Check your plan limits
Sandbox.limits() reports the sizes and caps available to your API
key's plan — pick a size up front instead of discovering the limit
from a PlanLimitError. Handy when the same code runs under keys on
different plans (e.g. a reseller provisioning per customer).
const limits = await Sandbox.limits();
limits.plan; // "free"
limits.allowedSizes; // ["small"]
limits.allowedSizes.includes("medium"); // false
limits.maxConcurrent; // 1
limits.usageGbHoursUsed; // 2.5 (memory consumed this month)
limits.usageGbHoursIncluded; // 10.0 (memory budget for the month)
limits.usageCpuHoursUsed; // 0.42 (CPU consumed this month)
limits.usageCpuHoursIncluded; // 2.0 (CPU budget for the month)
limits.usageResetsAt; // Date - 1st of next month UTC
for (const s of limits.sizes) { // full menu, each with .allowed
console.log(s.size, s.cpu, s.memoryMb, s.allowed);
}When usageGbHoursUsed >= usageGbHoursIncluded, Sandbox.create()
throws PlanLimitError with code="usage_exhausted" until the counter
resets. The metering rolls up mem_mb_seconds from the host's
per-second compute samples, so an idle sandbox still accrues toward the
budget until it is terminated.
Live metrics
sbx.metrics() returns this sandbox's live CPU and memory - the latest
reading, a rolling ~60s sample window for sparklines, and its lifetime
totals. Use it to watch a workload for saturation or memory pressure
without instrumenting the workload itself.
const m = await sbx.metrics();
m.cpu.usagePercent; // 47.0 (% of allocated cores; pegged 1-core = 100)
m.cpu.usageCores; // 0.94 (cores in use of m.cpu.cores)
m.memory.usageBytes; // 1879048192 (working set, excludes reclaimable cache)
m.memory.usagePercent; // 43.7 (% of m.memory.limitBytes)
m.lifetime.cpuSeconds; // 1284.31 (on-CPU seconds since the sandbox started)
for (const s of m.samples) { // oldest first; s.t is a Date
console.log(s.t, s.cpuPercent, s.memBytes);
}It is telemetry, not a state change: a paused or terminated sandbox
resolves with null live usage (check m.sandboxStatus) and an empty
samples window - lifetime is still populated. Poll no faster than
m.sampleIntervalSeconds; pass { since: <Date> } to fetch only samples
newer than your last poll.
Run a command
const result = await sbx.exec("python /workspace/main.py", {
timeoutSeconds: 60,
});
result.stdout; // string
result.stderr; // string
result.exitCode; // number
result.durationMs; // numberStream a long command
for await (const chunk of sbx.execStream("python long_task.py")) {
if (chunk.stream === "stdout") {
process.stdout.write(chunk.data);
} else {
process.stderr.write(chunk.data);
}
}Files
await sbx.files.write("/workspace/data.json", '{"x": 1}');
await sbx.files.writeBytes("/workspace/blob.bin", new Uint8Array([0, 1, 2]));
const content = await sbx.files.read("/workspace/out.txt"); // string
const raw = await sbx.files.readBytes("/workspace/blob.bin"); // Uint8Array
for (const entry of await sbx.files.list("/workspace")) {
console.log(entry.name, entry.isDir, entry.size);
}
await sbx.files.delete("/workspace/out.txt");Pause + resume
Pausing snapshots the sandbox memory + disk and stops the compute meter.
Resume restores it on the same or a different host. pause() returns
the sandbox id; persist it across processes and pass to Sandbox.resume.
const sbx = await Sandbox.create({ template: "node-22", network: "restricted" });
const sandboxId = await sbx.pause();
// ... minutes or hours later, from any worker:
const resumed = await Sandbox.resume(sandboxId);Snapshot retention is plan-capped (free: 1, pro: 3, team: 10). Calling
pause() over the cap throws SnapshotCapReached.
Terminate
withTemp calls terminate() for you. Otherwise:
await sbx.terminate();After terminate() the sandbox row stays in your account history but
the VM and any in-memory state are gone.
List your sandboxes
for await (const sbx of Sandbox.list({ status: "running" })) {
console.log(sbx.id, sbx.template, sbx.createdAt);
}Errors
All SDK errors extend OrkestrError.
| Class | When |
|------------------------|---------------------------------------------------|
| AuthError | Missing / invalid / expired API key, or scope mismatch |
| RateLimitError | Plan rate limit hit |
| PlanLimitError | Sandbox limit, concurrent limit, snapshot cap |
| SandboxNotFound | sandboxId doesn't exist or isn't yours |
| SandboxNotReady | Operation called on a paused / terminated sandbox |
| ExecTimeout | exec() exceeded timeoutSeconds |
| NetworkPolicyError | Command tried to reach a host the policy blocks |
| OrkestrError | Any other API-level error |
Field naming
The REST API uses snake_case. The JS SDK exposes camelCase to feel
native; the client converts in both directions. The Python SDK uses
snake_case end-to-end.
Versioning
Follows Semantic Versioning. The SDK targets the
/v1/sandboxes API; bumps to v1 of the API are non-breaking for SDK
callers. v2 of the API will require an SDK major.
Links
- Sandbox waitlist
- Public API docs
- Questions and bug reports: [email protected]
