Declarative builder
Define a template image as a chain of steps in code. Platinum caches builds by content.
Template defines an image as a chain of steps in code. Build the chain as a named template, or pass it inline when you create a sandbox. Platinum content-hashes each spec, so a repeat spec is a cache hit, not a rebuild.
Build a template
The step chain is SDK-only. The CLI and REST accept the compiled spec, not the chain.
A failed build does not throw. It returns state: "failed" with the tail of build_logs. Always check state.
Chain steps from a base image, then call build().
import { Platinum, Template } from "@platinum-dev/sdk";
const client = new Platinum({ url: process.env.PT_API_URL!, token: process.env.PT_TOKEN! });
const tpl = await Template
.fromPythonImage("3.13-slim")
.pipInstall(["pandas", "fastapi", "uvicorn"])
.aptInstall(["curl", "ripgrep"])
.env("LOG_LEVEL", "info")
.workdir("/app")
.copy("app.py", "/app/app.py")
.entrypoint("uvicorn app:app --host 0.0.0.0 --port 8000")
.build(client, { name: "my-agent", default_ram_mb: 1024 });
if (tpl.state === "failed") throw new Error(tpl.build_logs);import os
from platinum import Platinum, Template
client = Platinum(token=os.environ["PT_TOKEN"], api_url=os.environ["PT_API_URL"])
tpl = (Template.from_python_image("3.13-slim")
.pip_install(["pandas", "fastapi", "uvicorn"])
.apt_install(["curl", "ripgrep"])
.env("LOG_LEVEL", "info")
.workdir("/app")
.copy("app.py", "/app/app.py")
.entrypoint("uvicorn app:app --host 0.0.0.0 --port 8000")
.build(client, "my-agent", default_ram_mb=1024))
if tpl["state"] == "failed":
raise RuntimeError(tpl["build_logs"])# No dedicated CLI command for from-spec builds. Use the API passthrough.
pt api POST /v1/templates/from-spec -d @spec.jsoncurl -X POST $PT_API_URL/v1/templates/from-spec \
-H "Authorization: Bearer $PT_TOKEN" \
-d '{
"name": "my-agent",
"base_image": "python:3.13-slim",
"steps": [
{ "op": "pip", "packages": ["pandas", "fastapi", "uvicorn"] },
{ "op": "apt", "packages": ["curl", "ripgrep"] },
{ "op": "env", "key": "LOG_LEVEL", "value": "info" },
{ "op": "workdir", "path": "/app" }
],
"entrypoint": "uvicorn app:app --host 0.0.0.0 --port 8000",
"default_ram_mb": 1024
}'When the template is ready, create sandboxes with template: "my-agent". See Sandboxes.
Choose a base image
The build rejects floating tags: latest, lts, stable, main, master, edge, rolling. Pin a version (python:3.13-slim) or a @sha256: digest.
| Factory (TS / Python) | Base image |
|---|---|
Template.fromImage(ref) / from_image | any Docker image ref |
fromPythonImage(v) / from_python_image | python:<v> |
fromNodeImage(v) / from_node_image | node:<v> |
fromBunImage(v) / from_bun_image | oven/bun:<v> |
fromUbuntuImage(v) / from_ubuntu_image | ubuntu:<v> |
fromDebianImage(v) / from_debian_image | debian:<v> |
fromAlpineImage(v) / from_alpine_image | alpine:<v> |
Add steps
Every step returns the builder, so calls chain. TS is camelCase, Python is snake_case.
| Step (TS / Python) | Compiles to | Example |
|---|---|---|
.runCmd(cmd) / .run_cmd | RUN <cmd> | .runCmd("make install") |
.pipInstall(pkgs) / .pip_install | RUN pip install … | .pipInstall(["pandas"]) |
.npmInstall(pkgs) / .npm_install | RUN npm install -g … | .npmInstall(["typescript"]) |
.aptInstall(pkgs) / .apt_install | RUN apt-get install -y … | .aptInstall(["curl"]) |
.containerRuntime() / .container_runtime() | full guest kernel module tree | .containerRuntime() |
.env(key, value) | ENV | .env("LOG_LEVEL", "info") |
.workdir(path) | WORKDIR | .workdir("/app") |
.user(user) | USER | .user("app") |
.copy(src, dst, mode?) | COPY — inlined, up to 256 KiB per file | .copy("app.py", "/app/app.py") |
.entrypoint(cmd) | template entrypoint, launched at boot | .entrypoint("node server.js") |
.readyCmd(cmd) / .ready_cmd | readiness check — see note | .readyCmd("curl -fsS localhost:8000/health") |
copy inlines a local file into the spec, up to 256 KiB. For larger files, write them after create with the filesystem API, or use a build context (see Templates).
Use containerRuntime() or container_runtime() only when the sandbox must run dockerd or Podman. It
adds the complete module tree for the Platinum guest kernel. The default
runtime profile includes only the transport modules required by Platinum and
keeps the template smaller.
The equivalent REST step is:
{ "op": "kernel_modules", "profile": "container" }readyCmd does not gate boot. A sandbox turns running when it boots, not when your app is ready. To wait for your app, poll its health endpoint via exec.
Configure build()
TS takes an options object: build(client, { name, ... }). Python takes name/version as arguments and the rest as keywords: build(client, "name", default_cpu=2, ...).
| Option | Default | Notes |
|---|---|---|
name | required | ^[a-z0-9][a-z0-9._-]*$, max 64 chars; unique per org. A rebuild of an existing name reuses its row. |
version | "1.0.0" | floating values rejected |
default_cpu | 1 | 1–16 — sandbox default when create does not override |
default_ram_mb | 512 | 128–32768 |
default_disk_gb | 2 | 1–100 |
size_mb | 1024 | initial rootfs size in MB; per-org cap 20,480 |
wait_ms | 300000 | how long to poll for ready/failed. 0 returns at once as building. A non-zero wait that expires throws PlatinumTimeoutError. The build keeps running — poll templates.get(id) to pick it up. |
The SDK polls every 3 s until the template leaves building.
Build inline at sandbox create
Inline builds must finish within 300 s. A slower build fails the create. For large images, build() a named template first, then create from it by name.
Pass the builder as image. Platinum builds and boots in one call.
const image = Template.fromImage("alpine:3.20").runCmd("apk add --no-cache curl jq");
const sbx = await client.sandboxes.create(
{ image, env: { GREETING: "hi" } },
{ waitForRunning: true, waitTimeoutMs: 600_000 },
);image = Template.from_image("alpine:3.20").run_cmd("apk add --no-cache curl jq")
sbx = client.sandboxes.create(image=image, env={"GREETING": "hi"},
wait_for_running=True, wait_timeout_ms=600_000)# No CLI flag for an inline image spec. Use the API passthrough.
pt api POST /v1/sandboxes --query "wait_for_state=running" -d @body.jsoncurl -X POST "$PT_API_URL/v1/sandboxes?wait_for_state=running&wait_timeout_ms=60000" \
-H "Authorization: Bearer $PT_TOKEN" \
-d '{
"image": {
"base_image": "alpine:3.20",
"steps": [{ "op": "run", "cmd": "apk add --no-cache curl jq" }],
"size_mb": 256
}
}'| Rule | Behavior |
|---|---|
image + template | Mutually exclusive — sending both is a 400. Omit both to use the default pt-base template. |
| Caching | An identical spec reuses the cached build; a new spec builds; a previously failed one rebuilds. |
| Hash coverage | Only base_image, steps, entrypoint, and ready_cmd count. A change to size_mb or a default_* still hits the old cached build. |
| Cache scope | Per-org — two orgs with identical specs each pay their own first build. |
size_mb | Caps at 20,480. |
See also
- Templates — other build routes, listing, updates, deletion, quotas
- Sandboxes — create options, wait semantics, lifecycle
- API reference — exact
/v1/templates/from-specrequest/response schemas