Sandboxes and keys
Create a sandbox, give it a key so you get the same one back next time, and reattach to it from any server process.
A sandbox is one isolated Linux machine at a provider. You get a handle to it from createSandbox, and every other call goes through that handle. Most apps want one sandbox per user or per project that survives between requests. A key gives you that.
Create a sandbox
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { e2b } from "@opencoredev/sandbox-sdk/e2b";
await using box = await createSandbox(e2b(), {
env: { NODE_ENV: "development" },
labels: { app: "builder" },
});
console.log(box.id, box.provider);
box.id is the provider’s native ID. box.provider is the provider name, here "e2b". The full option list is on the reference page.
Get or create with a key
Pass key and createSandbox looks for a sandbox with that key first. It creates one only if none exists.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { e2b } from "@opencoredev/sandbox-sdk/e2b";
const userId = "user_123";
await using box = await createSandbox(e2b(), { key: `user:${userId}` });
if (box.created) {
await box.git.clone("https://github.com/vercel/next.js", { dir: "app", depth: 1 });
}
box.created is true when this call made the sandbox and false when it found an existing one. Use it to run setup only once.
With a key, await using calls box.close() at the end of the block, not destroy(). The sandbox keeps running so the next request finds it. See Lifecycle and cleanup.
Exact and best-effort keys
How reliable the lookup is depends on the provider.
| Provider | Key mode | How the key is stored |
|---|---|---|
| Local | exact | Container name, a hash of the key |
| Vercel | exact | Sandbox name, a hash of the key |
| Memory | exact | In-process map |
| Local (agentOS) | exact, in-process | In-process map |
| E2B | best-effort | Sandbox metadata |
| Daytona | best-effort | Sandbox labels |
| Every other cloud provider | best-effort | Metadata, the VM name, or a marker file in the VM. Each provider page says which |
Exact means the provider enforces unique names, so two concurrent calls with the same key always land on one sandbox. Best-effort means the SDK finds the sandbox by searching metadata. Two requests that arrive at the same moment can both miss and create two sandboxes.
If that race matters, pass a locator. It is two functions backed by any key-value store you already run.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { e2b } from "@opencoredev/sandbox-sdk/e2b";
import { kv } from "./kv";
await using box = await createSandbox(e2b(), {
key: "project:42",
locator: {
get: (key) => kv.get(key),
set: (key, id) => kv.set(key, id),
delete: (key) => kv.delete(key),
},
});
The SDK reads the ID from get, and writes a new one with set after it creates a sandbox. If the stored ID points at a sandbox that no longer exists, the SDK creates a new one and stores that ID instead. Within one process, the SDK also queues calls for the same key, so they never race each other.
Across servers, get followed by set is not atomic, so these two functions alone can’t stop two servers from creating a sandbox each. For a guarantee, add withLock and ask for keyMode: "exact":
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { e2b } from "@opencoredev/sandbox-sdk/e2b";
import { kv } from "./kv";
await using box = await createSandbox(e2b(), {
key: "project:42",
keyMode: "exact",
locator: {
get: (key) => kv.get(key),
set: (key, id) => kv.set(key, id),
withLock: (key, action) => kv.withLock(`sandbox:${key}`, action),
},
});
withLock must run action while holding a distributed lock or transaction for that key. keyMode: "exact" on a best-effort provider without withLock fails with unsupported.
Reattach by ID
connectSandbox attaches to a sandbox you already know the ID of. It never creates one.
import { connectSandbox } from "@opencoredev/sandbox-sdk";
import { e2b } from "@opencoredev/sandbox-sdk/e2b";
const box = await connectSandbox(e2b(), "sbx_abc123");
const result = await box.run("ls");
console.log(result.stdout);
await box.close();
It throws SandboxError with code not_found if the sandbox is gone.
What a handle exposes
| Member | What it is |
|---|---|
box.id |
Native sandbox ID |
box.key |
The key you passed, if any |
box.provider |
Provider name, such as "vercel" |
box.created |
Whether this call created the sandbox |
box.labels |
Labels on the sandbox, including sandbox-sdk |
box.capabilities |
Optional features this provider supports |
box.native |
The provider’s own API, typed per provider |
Use box.native when you need a provider feature the SDK doesn’t wrap. For providers that call their API directly, it is a NativeApi that calls any endpoint and can load the official SDK on demand. For providers that wrap an SDK, it is the same object that SDK returns.
Optional capabilities
Some methods exist only on providers that support them: pause(), resume(), snapshot(), and terminal(). They are on the TypeScript type only when the provider has them, so calling box.pause() on a provider without pause is a type error, not a runtime surprise. box.capabilities holds the same set at runtime.
import { createSandbox } from "@opencoredev/sandbox-sdk";
import { e2b } from "@opencoredev/sandbox-sdk/e2b";
await using box = await createSandbox(e2b(), { key: "project:42" });
await box.run("npm install");
await box.pause();
The capability matrix lists which provider supports what.