Templates
Start from a baked snapshot, pick a coding-agent preset, or bake your own template with the typed SDK baker.
Two template fields on POST /v1/sandboxes, for two different things:
templateId— fork from a baked snapshot in the template registry (e.g."react-ts"). The sandbox boots with files andnode_modulesalready in place.template— a coding-agent preset slug (opencode | pi | codex | claude | lfg). The sandbox comes up with that CLI agent installed and ready in the terminal.
They are mutually exclusive — sending both returns 400.
Snapshot templates (templateId)
curl -sS https://infra.omg.dev/v1/sandboxes \
-H "Authorization: Bearer $OMG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "templateId": "react-ts", "ports": [5173] }'react-ts gives you a ready-to-run web app at /home/user/project:
- Vite + React 19 + TypeScript, Tailwind CSS v4
@omg-dev/*SDK preinstalled (sdk, schema, server, auth, ai, media, pwa, db-collection) withfunctions/andserver/scaffolding- Dependencies already installed —
bun run devserves on:5173immediately, nobun installwait
Because the sandbox is restored from a snapshot instead of cold-booting and installing, create-to-serving is seconds, not minutes.
Agent presets (template)
A preset creates a raw sandbox (skipAppProcesses is forced true) with
one or more coding-agent CLIs installed. Unknown slugs fail with 400.
| Slug | Installs | Serves |
|---|---|---|
opencode | opencode CLI | TUI only |
pi | pi coding agent | TUI only |
codex | OpenAI Codex CLI | TUI only |
claude | Claude Code | TUI only |
lfg | all four agents + the lfg web UI | web UI on :8766 |
curl -sS https://infra.omg.dev/v1/sandboxes \
-H "Authorization: Bearer $OMG_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "template": "claude" }'The create response adds preset fields on top of the normal sandbox shape:
{
"id": "abc123def456",
"status": "running",
"template": "claude",
"templateStatus": "ready",
"installLogPath": "/home/user/.omg/agent-install.log"
}templateStatusisreadyorfailed(install/verify runs before the response returns). Onfailed,bootstrapErrorsays why and the sandbox is kept so you can inspect the install log.lfgalso returnsservePort: 8766andserveLogPath; its web UI is reachable viaGET /v1/sandboxes/{id}/url/8766. Forlfg,portsdefaults to[8766]when you don't pass any.- TUI presets have no web server — connect over the
shell WebSocket
and run the agent by name (
claude,codex, …).
What the default image has
Every sandbox (no templateId) boots from the same base rootfs:
| OS | Ubuntu 22.04 |
| Runtime | Bun (system-wide, /usr/local/bin/bun) |
| Tools | git, curl, wget, unzip, python3, openssh-server |
| User | user (uid 1000, passwordless sudo), home + cwd /home/user |
| Package cache | pre-warmed Bun cache for the @omg-dev/* SDK and the shadcn/Radix/Tailwind stack — bun install of those is near-instant |
There is no Node.js binary — Bun stands in (agent presets shim
node → bun for #!/usr/bin/env node CLIs). exec also injects
ANTHROPIC_BASE_URL, OPENAI_BASE_URL, and OMG_AI_URL pointing at the
per-sandbox LLM proxy, so AI SDKs inside the sandbox work without keys.
Bake your own template
@omg-dev/sandbox ships a typed template baker: declare install steps,
checks, and a start command, and bakeTemplate runs the whole
create → apply → verify → snapshot → publish sequence.
import {
SandboxClient,
defineTemplate,
bakeTemplate,
apt,
run,
check,
} from "@omg-dev/sandbox"
const client = new SandboxClient({
baseUrl: "https://infra.omg.dev",
token: process.env.OMG_API_KEY!,
})
const template = defineTemplate({
id: "my-agent",
version: "1",
title: "My agent",
ports: [3000],
install: [
apt.packages(["tmux"]),
run({ user: "user", command: "bun install -g my-agent@1.2.3" }),
],
checks: [check.command("tmux"), check.command("my-agent", { user: "user" })],
start: {
user: "user",
command: "my-agent serve --host 0.0.0.0 --port 3000",
readiness: { port: 3000 },
},
})
await bakeTemplate(client, template)It publishes both my-agent-v1 and my-agent (latest); create from either
with { "templateId": "my-agent" }. Direct service clients mark the system
version immutable. User template names are private to their account, so two
users can publish the same name without replacing each other. If an account
has no private template with that name, create falls back to the global system
catalogue.
The registry records the start command, ports, and readiness probe. Creation
copies that runtime contract onto the sandbox row, so hibernate/resume restores
the service without the caller resending options and a later publish cannot
change an existing sandbox. Re-bake by bumping version.
install steps run as root unless you pass user; apt.packages and
download.archive are the other typed steps. checks fail the bake early
(check.command / check.file), before anything is published.
Raw HTTP: snapshot + fork
No SDK? Snapshot + fork gives you the same instant-boot behavior: set up a sandbox once, snapshot it, then fork the snapshot for every fresh instance.
const BASE = "https://infra.omg.dev"
const auth = { Authorization: `Bearer ${process.env.OMG_API_KEY}` }
const json = { ...auth, "Content-Type": "application/json" }
// 1. create a base sandbox (and poll it to "running" — see Quickstart)
const sb = await fetch(`${BASE}/v1/sandboxes`, {
method: "POST",
headers: json,
body: JSON.stringify({ size: "medium", ports: [3000] }),
}).then((r) => r.json())
// 2. write your files — body is a JSON ARRAY, content is base64
await fetch(`${BASE}/v1/sandboxes/${sb.id}/files`, {
method: "POST",
headers: json,
body: JSON.stringify([
{
path: "/home/user/project/package.json",
content: btoa(JSON.stringify({ name: "my-template", type: "module" })),
mode: 0o644,
},
]),
})
// 3. run setup (exec runs as root; returns stdout/stderr/exitCode)
await fetch(`${BASE}/v1/sandboxes/${sb.id}/exec`, {
method: "POST",
headers: json,
body: JSON.stringify({
cmd: "bash",
args: ["-lc", "cd /home/user/project && bun install"],
timeoutMs: 300_000,
}),
}).then((r) => r.json())
// 4. snapshot — this is your "template"
const snap = await fetch(`${BASE}/v1/sandboxes/${sb.id}/snapshot`, {
method: "POST",
headers: auth,
}).then((r) => r.json())
// 5. fork the snapshot whenever you need a fresh instance (~sub-second boot)
const fresh = await fetch(`${BASE}/v1/sandboxes/fork`, {
method: "POST",
headers: json,
body: JSON.stringify({
snapshotId: snap.id,
ports: [3000],
skipAppProcesses: true,
}),
}).then((r) => r.json())
// Or publish/update a name in your private template registry.
await fetch(`${BASE}/v1/templates/my-template/latest`, {
method: "POST",
headers: json,
body: JSON.stringify({
snapshotId: snap.id,
startCommand: "cd /home/user/project && exec bun run start",
readinessPort: 3000,
ports: [3000],
}),
})Notes:
- The snapshot captures the full VM state — filesystem and memory — so a fork resumes with your processes' output already on disk.
- Forks are owner-checked and automatically routed to the node that holds the
snapshot; you don't need an
X-Vibes-Nodepin forPOST /v1/sandboxes/fork. - Pass
skipAppProcesses: trueon the fork unless your snapshot is a dashboard dev-editor sandbox — it keeps the fork a raw sandbox that only runs what you set up. - Keep the source sandbox or delete it after snapshotting — the snapshot
stays forkable either way. Re-baking = run the steps again and switch your
code to the new
snapshotId. - A user may publish only snapshots produced by their own sandbox.
GET,POST, andDELETE /v1/templates/{id}/latestare scoped to that user's namespace. Global system templates and immutable version refs can be changed only by a service principal. - The start command, readiness port, and ports are copied onto each sandbox when it is created. Updating or deleting the registry row cannot change what an existing sandbox executes after wake.
Underneath, the typed baker makes exactly these calls — the raw flow and
bakeTemplate produce the same registry entries that templateId resolves
against. First-party templates such as react-ts are published into the
service-owned global catalogue.