# 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](https://orkestr.eu/sandbox), 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: ```bash 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](https://orkestr.eu/docs/sandboxes/mcp). > **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. ```bash # 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. **Python:** ```python 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 ``` **JavaScript:** ```javascript import { Sandbox } from "orkestr"; // Pass the template id wherever you'd pass a built-in name. await Sandbox.withTemp({ template: "tmpl_01J..." }, async (sbx) => { const result = await sbx.exec( "python -c 'import pandas; print(pandas.__version__)'", ); console.log(result.stdout); // already installed - no pip at boot }); ``` Or directly over the API: ```bash 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. **Python:** ```python 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: ... ``` **JavaScript:** ```javascript import { Sandbox } from "orkestr"; // "data-stack" resolves the name's current 'default' tag. const sbx = await Sandbox.create({ template: "data-stack" }); // "data-stack:v2" pins the v2 tag; a raw tmpl_ id pins one build forever. const pinned = await Sandbox.create({ template: "data-stack:v2" }); ``` 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: ```bash # 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"] }' ``` ```bash # 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: ```bash # 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" }' ``` **Python:** ```python 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) ``` **JavaScript:** ```javascript import { Sandbox } from "orkestr"; // The template's start_command has already launched on boot - no exec call. const sbx = await Sandbox.create({ template: "dev-server", network: "restricted" }); const url = await sbx.getHost(3000); // public preview URL for the dev server console.log(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:** `building` → `ready` | `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. ```bash 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 - [REST API reference](https://orkestr.eu/docs/sandboxes/api-reference) - the full `/v1/templates` surface - [MCP server](https://orkestr.eu/docs/sandboxes/mcp) - build and manage templates from an agent - [Cookbook](https://orkestr.eu/docs/sandboxes/cookbook) - end-to-end patterns, including a custom-stack example