Concepts
Templates
Install system packages and tools into your environments with templates, built from steps or from a Dockerfile.
Every environment starts from the same runtime: Debian 13 with systemd, Python 3 (with venv, without pip), curl and git. A template adds what
your agent needs on top of it: system packages, language runtimes, a browser, CLI tools, configuration files. You build a template
once, as an immutable version, and environments created from that version all have the same software installed.
Templates and agent packages do different jobs. The agent package is your agent itself,
mounted at /opt/agent. The template is the system around it. Each has its own versions, and you can ship one without rebuilding the
other.
Install system software through templates
Everything outside /workspace is rebuilt whenever the VM starts (see what survives what).
If your agent runs apt-get install at run time, the package is gone after the VM powers off or is replaced. So:
- System packages and global tools belong in a template.
- Per-user data, and things like a Python virtual environment or
node_modulesthat the agent creates for a user, belong under/workspace, where they are kept.
Build steps
A template version is built from a list of steps, run in order. Each step is an object with one key:
| Step | What it does |
|---|---|
{"run": "..."} | Runs a shell command as root with /bin/sh -c: apt-get install, pip install, curl and so on. |
{"env": {"NAME": "value"}} | Sets environment variables for later steps and for every environment using the template. |
{"workdir": "/srv/app"} | Sets the directory later run steps (and start) run in. It is created if it is missing. |
{"copy": {"path": "...", "content": "...", "encoding": "utf8", "mode": "0644"}} | Writes a file. Use "encoding": "base64" for binary content. |
A version has at most 64 steps, and all copy contents together are limited to 4 MiB. Download anything bigger in a run step.
Files written by copy are owned by root and readable by everyone; add a run step with chown agent:agent ... if your agent has to
write to them.
Template variables have the lowest priority: the agent package's /opt/agent/env and the environment's
config override them. They are not expanded when they are loaded, so write out a full PATH rather than
$PATH; the runtime's default is /usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin.
start is optional: the command an environment runs when it has no agent package, as a string (run with /bin/sh -c) or an argv
array, in the last workdir. With an agent package, /opt/agent/bin/start runs instead. With neither, the environment simply waits for
exec calls.
import { Temper } from "@temper-hq/sdk";
const temper = new Temper();
await temper.templates.createVersion("py-data", "1", {
steps: [
{ run: "apt-get update && apt-get install -y --no-install-recommends ffmpeg" },
{ run: "python3 -m venv /opt/py && /opt/py/bin/pip install pandas==2.2.3" },
{ env: { PATH: "/opt/py/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" } },
],
});
const version = await temper.templates.waitForVersion("py-data", "1");
console.log(version.state); // "ready"from temper_hq import Temper
with Temper() as temper:
temper.templates.create_version(
"py-data",
"1",
steps=[
{"run": "apt-get update && apt-get install -y --no-install-recommends ffmpeg"},
{"run": "python3 -m venv /opt/py && /opt/py/bin/pip install pandas==2.2.3"},
{"env": {"PATH": "/opt/py/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}},
],
)
version = temper.templates.wait_for_version("py-data", "1")
print(version.state) # "ready"client, err := temper.New()
if err != nil {
log.Fatal(err)
}
_, err = client.Templates.CreateVersion(ctx, "py-data", "1", temper.CreateTemplateVersionParams{
Steps: []temper.TemplateStep{
temper.RunStep("apt-get update && apt-get install -y --no-install-recommends ffmpeg"),
temper.RunStep("python3 -m venv /opt/py && /opt/py/bin/pip install pandas==2.2.3"),
temper.EnvStep(map[string]string{"PATH": "/opt/py/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"}),
},
})
if err != nil {
log.Fatal(err)
}
version, err := client.Templates.WaitForVersion(ctx, "py-data", "1", nil)To build from a Dockerfile instead of writing steps, see Build a template from a Dockerfile.
How a build runs
createVersion returns at once with state: "building". Temper then runs the steps in a temporary environment that belongs to you:
runsteps execute as root inside the runtime, with the same isolation as your agent. The files they leave behind become the template's layer.- Egress during the build is limited to common package registries (Debian, PyPI, npm, crates.io and GitHub releases) plus your developer-level egress allowlist. Add hosts there if a step downloads from somewhere else.
- A step can run for up to 30 minutes, and a whole build for up to an hour. The first step that exits with a non-zero status fails the build.
- While the build runs, the temporary environment appears in your environment list with
end_user_idtemplate-build:<name>:<version>. It is destroyed when the build ends. Its CPU time and egress count toward your usage.
The version then becomes ready or failed. getVersion returns the error and the last 64 KiB of the build log, with each
step's command and output. waitForVersion (Python wait_for_version, Go WaitForVersion) polls until the build finishes and raises
template_build_failed with the end of the log if it fails.
A version whose base, steps and start match one you have already built reuses that build instead of running the steps again.
Versions
- Template names are 1–64 lowercase letters, digits and
-. Versions are up to 64 letters, digits,.,_and-. - A version can't change. Creating one that exists returns
409 version_exists, so build changes as a new version. frombuilds on top of anotherreadyversion of yours ("base:3") instead of the plain runtime, so several templates can share a common base. A base that is still building or failed returns409 template_not_ready.deleteVersionremoves a version. A version that is still building returns409 template_building; one used by an environment that hasn't been destroyed, or by another version as its base, returns409 template_in_use.
Use a template
Pass template as name:version when you create an environment. The version must be ready; otherwise the call returns
409 template_not_ready.
const env = await temper.environments.create({ end_user_id: "user_42", template: "py-data:1" });
console.log(env.template); // "py-data:1"env = temper.environments.create(end_user_id="user_42", template="py-data:1")
print(env.template) # "py-data:1"env, err := client.Environments.Create(ctx, temper.CreateEnvironmentParams{EndUserID: "user_42", Template: "py-data:1"})An environment keeps the template version it was created with; publishing a new version doesn't change existing environments.
Environments created without template use the plain runtime, and their template is null.
Limits
| Limit | Value |
|---|---|
| Steps per version | 64 |
run command | 16 KiB |
copy contents, all steps together | 4 MiB |
| One step | 30 minutes |
| One build | 1 hour |
| Build log kept | last 64 KiB |