Sandboxes

Custom templates

A custom template bakes your dependencies into the sandbox image once, so every sandbox you create from it boots in about 300 ms with everything already installed - no pip install tax on each run. Build a template from one of the built-in base images plus a recipe of shell steps, then pass its id wherever you'd pass a built-in template name.

The short version
Build once with a base image + a recipe (via the console, REST API, or MCP), wait for it to go ready, then Sandbox.create(template="data-stack") by name - or pin a version with "data-stack:v2" / the tmpl_ id. The image is captured on the host, so size never affects boot time.

Why templates

Installing packages at the start of every sandbox is slow and repetitive - a heavy pip install or apt-get can dwarf the actual work an agent does in the box. A template moves that cost to build time: the install runs once, the resulting filesystem is captured as a reusable image, and sandboxes boot straight into it. Because the image is hardlinked rather than copied, a 1 GB template boots just as fast as a bare base.

How a build works

When you create a template, orkestr boots a short-lived build VM from your base image with a writable root filesystem, runs each recipe step in order, and captures the result as a new image:

  • Each step runs as root with HOME=/root, so installs persist into the captured image.
  • If any step exits non-zero, the build stops and the template is marked failed with the failing step's output.
  • On success the image is scrubbed (package caches, logs, machine-id) and the template goes ready.
  • The recipe is the source of truth; the image is a derived artifact - builds are immutable, so to change one you build again under the same name and move its tags (see "Tags & versioning" below).

Build a template

Templates are built via the Sandboxes console, the REST API, or the MCP server. The SDKs consume templates (you boot sandboxes from them) but don't build them - building is a control-plane operation. From the console, open the Templates section, click New template, and fill in a name, base image, and one build step per line.

Or over the REST API:

terminal
curl -X POST https://api.orkestr.eu/v1/templates \
  -H "Authorization: Bearer $ORKESTR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "data-stack",
    "base_template": "python-3.12",
    "recipe": [
      "pip install pandas numpy pyarrow",
      "echo \"ready\" > /root/.template-built"
    ],
    "network": "restricted"
  }'

# => { "id": "tmpl_01J...", "status": "building", ... }

Over MCP, call the create_template tool with the same fields (name, base_template, recipe, optional network); poll with list_templates. See the MCP reference.

Build settings
Recipe steps usually need network access to fetch packages, so the build defaults to restricted egress (package registries, GitHub, and major APIs through an allowlisting proxy) - use open if a step reaches something else. Installs land in the system and /root; /workspace and /tmp are tmpfs and are not captured. Custom templates are a paid-plan feature.

Check build status

A build is asynchronous. The create call returns immediately with status: "building"; poll until it's ready (or failed, which carries an error_message). The console does this for you and flips the status badge live.

terminal
# Poll until status is "ready" (or "failed", with error_message).
curl https://api.orkestr.eu/v1/templates/tmpl_01J... \
  -H "Authorization: Bearer $ORKESTR_API_KEY"

# List all of your templates:
curl https://api.orkestr.eu/v1/templates \
  -H "Authorization: Bearer $ORKESTR_API_KEY"

Boot a sandbox from a template

Once a template is ready, use its id as the template exactly like a built-in. In the console, it appears under "Your templates" in the New sandbox picker.

agent
from orkestr import Sandbox

# Pass the template id wherever you'd pass a built-in name.
with Sandbox.create(template="tmpl_01J...") as sbx:
    result = sbx.exec("python -c 'import pandas; print(pandas.__version__)'")
    print(result.stdout)  # already installed - no pip at boot

Or directly over the API:

terminal
curl -X POST https://api.orkestr.eu/v1/sandboxes \
  -H "Authorization: Bearer $ORKESTR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "template": "tmpl_01J...", "size": "small" }'

Tags & versioning

Builds are immutable, but hardcoding tmpl_ ids means every caller must be updated whenever you rebuild. Tags fix that: a tag is a movable pointer from your template's name to one build. Reference a template three ways:

  • "data-stack" - resolves the name's current default tag.
  • "data-stack:v2" - pins the v2 tag.
  • "tmpl_01J..." - pins that exact build forever.
agent
from orkestr import Sandbox

# "data-stack" resolves the name's current 'default' tag.
with Sandbox.create(template="data-stack") as sbx:
    ...

# "data-stack:v2" pins the v2 tag; a raw tmpl_ id pins one build forever.
with Sandbox.create(template="data-stack:v2") as sbx:
    ...

The first successful build of a name claims default automatically, so plain-name references work right away. Later builds of the same name never steal a tag implicitly - pass tags at create time, or promote explicitly once you've tested the new build:

terminal
# Build with tags; they're assigned when the build goes ready.
curl -X POST https://api.orkestr.eu/v1/templates \
  -H "Authorization: Bearer $ORKESTR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "data-stack",
    "base_template": "python-3.12",
    "recipe": ["pip install pandas==2.2.0 numpy pyarrow"],
    "tags": ["v2"]
  }'
terminal
# Promote: point a tag at another build of the same name - no rebuild.
curl -X PUT https://api.orkestr.eu/v1/templates/tmpl_01K.../tags/default \
  -H "Authorization: Bearer $ORKESTR_API_KEY"

# Remove a tag from the build it points at:
curl -X DELETE https://api.orkestr.eu/v1/templates/tmpl_01K.../tags/v2 \
  -H "Authorization: Bearer $ORKESTR_API_KEY"

Assigning a tag that already exists on another build of the same name moves it (the old build keeps its id and stays pinnable), so "staging to production" promotion is one call and never rebuilds. Template listings show each build's tags, and the console's Templates section manages them inline. Deleting a build deletes the tags pointing at it - they don't fall back to an older build, so reassign first if the name must keep resolving.

Boot-time start command

A template can carry an optional start_command that runs once, detached, on the first boot of every sandbox created from it - so the sandbox is self-starting with no post-create exec call. This is the piece that makes "create from template → persistent dev server → public preview URL" a single call:

terminal
# Build a self-starting template: start_command runs on first boot.
curl -X POST https://api.orkestr.eu/v1/templates \
  -H "Authorization: Bearer $ORKESTR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "dev-server",
    "base_template": "node-22",
    "recipe": ["cd /workspace && npm install"],
    "start_command": "cd /workspace && npm run dev"
  }'
agent
from orkestr import Sandbox

# The template's start_command has already launched on boot - no exec call.
sbx = Sandbox.create(template="dev-server", network="restricted")
url = sbx.get_host(3000)   # public preview URL for the dev server
print(url)
  • Runs as root with working directory /workspace and the sandbox's own environment, so PORT and any create-time env vars are visible.
  • Fires on first boot only. It does not re-run on resume - a paused sandbox's snapshot already holds the running process, so a resumed sandbox picks up exactly where it left off.
  • Non-fatal and retrievable: if it fails to launch or exits non-zero, the sandbox still comes up. The create response returns a start_command_id you can poll via the background-commands API for status and logs, and any launch failure is recorded on the sandbox's activity feed.
  • Immutable per build, like the recipe - to change it, build a new version and move the tag.
Building vs booting
recipe runs at build time (installing dependencies into the captured image); start_command runs at boot time (starting your process in each sandbox). Put installs in the recipe and the long-running server in the start command.

Lifecycle & limits

  • Statuses: buildingready | failed. Only ready templates can be booted.
  • Tier caps: the number of templates you can keep is tier-limited (free: 1, payg: 15, enterprise: 50). Failed builds don't count - delete and retry freely.
  • Deleting: removing a template frees the slot and its image. Sandboxes already running from it keep working.
terminal
curl -X DELETE https://api.orkestr.eu/v1/templates/tmpl_01J... \
  -H "Authorization: Bearer $ORKESTR_API_KEY"
Keep recipes deterministic
Pin versions where it matters (pip install pandas==2.2.0) so a rebuild reproduces the same image. A template is captured at build time and never changes afterwards; to pick up new package versions, build a new template.

Next steps

Reading with an agent? This page is also plain markdown at /docs/sandboxes/templates.md, and the full docs index lives at /docs/llms.txt.