Concepts
Environments and the idle ladder
One long-lived cloud computer per end user, its lifecycle states, and how Temper suspends and wakes it.
An environment is the computer Temper keeps for one of your end users: a microVM where your agent runs, plus an encrypted data disk that belongs to that user. You create one environment per end user and keep it for as long as the user has an account with you. Temper moves it between running, suspended and powered off on its own, and wakes it when you need it.
One environment per end user
Every environment has an end_user_id, which is your own identifier for the user (1 to 200 characters). An end user can have only one
environment that is not destroyed. Creating a second one returns 409 environment_exists, and the message names the existing
environment's id. After you destroy an environment, you can create a new one for the same end user.
Each environment runs your agent package at agent_version. If you leave it out when you create the environment, Temper uses your
default version (see Data and upgrades). If you have no default version either, agent_version is null.
Creating environments
import { Temper, isTemperError } from "@temper-hq/sdk";
const temper = new Temper(); // reads TEMPER_API_KEY and TEMPER_BASE_URL
try {
const env = await temper.environments.create({
end_user_id: "alice",
resources: { cpus: 2, memory_mib: 4096 },
idle: { suspend_after_secs: 3600 },
});
console.log(env.id, env.desired_state, env.state); // env_... running pending
} catch (err) {
if (!isTemperError(err, "environment_exists")) throw err;
}from temper_hq import Temper, TemperError
temper = Temper() # reads TEMPER_API_KEY and TEMPER_BASE_URL
try:
env = temper.environments.create(
end_user_id="alice",
resources={"cpus": 2, "memory_mib": 4096},
idle={"suspend_after_secs": 3600},
)
print(env.id, env.desired_state, env.state) # env_... running pending
except TemperError as e:
if e.code != "environment_exists":
raiseimport temper "github.com/temper-hq/sdk-go"
client, err := temper.New() // reads TEMPER_API_KEY and TEMPER_BASE_URL
if err != nil {
log.Fatal(err)
}
env, err := client.Environments.Create(ctx, temper.CreateEnvironmentParams{
EndUserID: "alice",
Resources: &temper.ResourcesInput{CPUs: 2, MemoryMiB: 4096},
Idle: &temper.IdleSettings{SuspendAfterSecs: temper.Ptr(3600)},
})
if err != nil && !temper.IsCode(err, temper.CodeEnvironmentExists) {
log.Fatal(err)
}Retrying safely
If a create times out, you don't know whether it went through. Send an idempotency key (one per logical create, for example a UUID or
your own record id) and retry with the same key: Temper returns the environment the first request created, with status 200. A key
stays bound to its environment until that environment is destroyed; using it for a different end user fails with 409 idempotency_key_reused.
Without a key, a retry fails with 409 environment_exists. That error, and idempotency_key_reused, carry the existing
environment's id: err.environmentId in TypeScript, e.environment_id in Python, (*temper.Error).EnvironmentID in Go.
const env = await temper.environments.create({ end_user_id: "alice" }, { idempotencyKey: `signup-${userId}` });env = temper.environments.create(end_user_id="alice", idempotency_key=f"signup-{user_id}")env, err := client.Environments.Create(ctx, temper.CreateEnvironmentParams{EndUserID: "alice", IdempotencyKey: "signup-" + userID})Configuration: environment variables and files
Give an environment non-secret configuration when you create it, or replace it later, with env (name to value) and files (name to
content). Inside the VM, the variables are in /run/temper/env, which the agent service, every exec and login shells read; they
override the defaults in your agent package. Files appear read-only at /run/temper/files/<name>. Both are in place before the agent
first starts.
await temper.environments.create({
end_user_id: "alice",
env: { APP_REGION: "eu", FEATURE_X: "on" },
files: { "settings.json": { content: JSON.stringify({ theme: "dark" }) } },
});
await temper.environments.setConfig(envId, { env: { APP_REGION: "us" } }); // replaces the whole configtemper.environments.create(
end_user_id="alice",
env={"APP_REGION": "eu", "FEATURE_X": "on"},
files={"settings.json": '{"theme": "dark"}'}, # str, bytes, or {"content", "encoding"}
)
temper.environments.set_config(env_id, env={"APP_REGION": "us"}) # replaces the whole config_, err := client.Environments.Create(ctx, temper.CreateEnvironmentParams{
EndUserID: "alice",
Env: map[string]string{"APP_REGION": "eu", "FEATURE_X": "on"},
Files: map[string]temper.ConfigFile{"settings.json": {Content: `{"theme":"dark"}`}},
})
// SetConfig replaces the whole config; temper.FileBytes(b) wraps binary content.
_, err = client.Environments.SetConfig(ctx, envID, temper.EnvironmentConfig{Env: map[string]string{"APP_REGION": "us"}})PUT /v1/environments/{id}/config(setConfig) replaces the whole configuration;{}clears it. It doesn't wake the environment or replace the VM. Files update within about 10 seconds; new variables apply to processes started afterwards (the next exec, the agent's next start), so an agent that needs a new value right away has to restart or reread the file.- Limits: up to 100 variables (32 KiB in total) and 20 files (256 KiB in total). Variable names are letters, digits and underscores;
values can't contain single quotes or control characters.
HOME,USER,LOGNAME, names starting withTEMPER_and the platform's proxy and certificate variables are reserved. File names are letters, digits,.,_and-, with no directories. - Configuration is not encrypted the way secrets are, and the agent can read it. Keep API keys and tokens in secrets.
See Get the configuration and the other environment endpoints.
Creating in batches
To onboard many users at once, send 1 to 200 environments in one call with createBatch (Python create_batch, Go CreateBatch).
Temper creates them one by one, and one failure does not stop the others. The response is 200 with one result per item, in request
order: either {environment} or {error, message}. An item for an end user who already has an environment fails with
environment_exists and carries that environment's environment_id, so a batch is safe to retry. If the request as a whole is malformed (for example, one item has an unknown field),
you get 422 invalid_body and nothing is created.
See Create environments in a batch.
Desired state and actual state
An environment has two states:
| Field | Set by | Values |
|---|---|---|
desired_state | Your API calls and the idle ladder | running, suspended, stopped, destroyed |
state | The host that runs the VM, reported back to Temper | pending, running, suspended, stopped, destroyed, failed |
Your calls change desired_state and return straight away. The host then brings the VM to that state and reports it in state.
A new environment starts as desired_state: running, state: pending, and becomes running once it has been placed on a host and the VM
is up. If no host has capacity, it stays pending.
State changes are idempotent: asking for the state an environment is already in succeeds and returns it unchanged.
| Call | What it does |
|---|---|
suspend | Takes a memory snapshot. Only works when desired_state is running. If the environment is stopped or destroyed, it returns 409 invalid_state_transition. |
resume | Brings a suspended or stopped environment back to running. It does not count as activity. |
wake | Like resume, but also counts as activity. On an environment that is already running, it only records activity. Call it when a request for this user arrives. |
destroy | Sets desired_state: destroyed. The host deletes the VM, the data disk and its backups. Returns 200 with the environment. |
state: failed means the VM could not be started. The environment's events say why. Exec and file calls on a failed environment return
409 environment_failed.
Environment events
Every state change, idle step, wakeup, agent upgrade and agent action is recorded as an event. List events with events (Python
events / iter_events, Go Events / IterEvents). They come back oldest first and are paginated with an integer cursor. You can
filter them by kind, for example created, desired_suspended, running, idle_suspended, idle_stopped, keep_awake or
agent_upgrade. Events stay readable after the environment is destroyed. See List environment events.
The idle ladder
An environment that nobody is using steps down through three levels, so you don't pay for idle memory and the user's state is still kept:
- Running. The VM is up and the agent answers immediately. Idle memory is handed back to the host.
- Suspended (memory snapshot). After
suspend_after_secswithout activity, Temper snapshots the VM's memory and stops it. Running processes, open files and the agent's in-memory state are all kept, and the VM picks up where it left off when it wakes. - Powered off. After
stop_after_secswithout activity, the snapshot is dropped and only the data disk is kept. On the next wake, Temper boots a fresh VM, attaches the disk and starts your agent. Files on the disk are all there. Processes that were running before the suspend are not.
The defaults are suspend_after_secs: 21600 (6 hours) and stop_after_secs: 604800 (7 days). Both are counted from the
environment's last activity (last_active_at), not from the previous step. An environment is only powered off from the suspended
level, so if you turn suspension off, the environment never powers off either.
What counts as activity
last_active_at moves forward when:
- the VM is using CPU (so a task that is still working is never suspended mid-way);
- you call
wake,keepAwake, or any exec, file or process endpoint; - a scheduled wakeup or agent schedule fires.
Adjusting the thresholds
Set the thresholds when you create the environment (idle), or change them later with updateIdle (Python update_idle, Go
UpdateIdle). Each value is 0, which turns that step off, or between 60 seconds and 90 days. Only the fields you send are changed.
// Suspend after 1 hour idle; never power off.
await temper.environments.updateIdle(env.id, { suspend_after_secs: 3600, stop_after_secs: 0 });# Suspend after 1 hour idle; never power off.
temper.environments.update_idle(env.id, suspend_after_secs=3600, stop_after_secs=0)// Suspend after 1 hour idle; never power off.
_, err = client.Environments.UpdateIdle(ctx, env.ID, temper.IdleSettings{
SuspendAfterSecs: temper.Ptr(3600),
StopAfterSecs: temper.Ptr(0),
})See Update idle settings.
Requests wake the environment
You don't have to check the state before you use an environment. Exec, file and process calls wake an environment that is suspended or
powered off, wait until it is running, and then do their work. If it cannot be placed on a host or does not start in time, they return
503 (environment_not_placed or environment_not_running), and you can retry. When your own backend gets a message for the user
(for example, a chat message you are about to hand to the agent), call wake first so the VM is already coming up.
An environment that went over a usage limit is suspended and stays suspended: resume, wake and exec return 409 limit_exceeded
until the limit is raised or removed. See Usage and limits.
Keeping an environment awake
CPU activity already keeps a working environment running. Use keepAwake (Python keep_awake, Go KeepAwake) when a task is waiting
with an idle CPU, for example on an external callback or a slow upload. It holds off the idle ladder for 1 to 86,400 seconds from now,
and wakes the environment if it isn't running. It never shortens a later keep_awake_until that is already set. The current value is
in idle.keep_awake_until.
await temper.environments.keepAwake(env.id, 1800); // 30 minutestemper.environments.keep_awake(env.id, 1800) # 30 minutes_, err = client.Environments.KeepAwake(ctx, env.ID, 1800) // 30 minutesWakeups and schedules
A suspended VM can't run its own cron jobs. Instead, Temper wakes the environment for you a little before the time comes, so the task doesn't wait for a resume.
One-off wakeups (you)
Register a wakeup with createWakeup (Python create_wakeup, Go CreateWakeup). at is an RFC 3339 time between one minute ago
(fires right away) and 366 days ahead, and reason is an optional note. An environment can have up to 100 pending wakeups.
Going over returns 429 wakeup_limit_reached. For recurring work, register the next wakeup each time the task runs.
const wk = await temper.environments.createWakeup(env.id, {
at: new Date("2026-11-01T08:00:00Z"),
reason: "daily digest",
});
const pending = await temper.environments.listWakeups(env.id);
await temper.environments.cancelWakeup(env.id, wk.id);from datetime import datetime, timezone
wk = temper.environments.create_wakeup(
env.id, at=datetime(2026, 11, 1, 8, 0, tzinfo=timezone.utc), reason="daily digest"
)
pending = temper.environments.list_wakeups(env.id)
temper.environments.cancel_wakeup(env.id, wk.id)wk, err := client.Environments.CreateWakeup(ctx, env.ID,
time.Date(2026, 11, 1, 8, 0, 0, 0, time.UTC), "daily digest")
if err != nil {
log.Fatal(err)
}
pending, err := client.Environments.ListWakeups(ctx, env.ID)
err = client.Environments.CancelWakeup(ctx, env.ID, wk.ID)Cancelling a wakeup that already fired or was already cancelled returns 404.
Schedules the agent registers
Agents like Hermes have their own recurring jobs and heartbeats. Inside the VM, the agent registers them with Temper over a local HTTP
socket at /run/temper/control.sock instead of running its own cron:
# Inside the VM (the agent's code)
curl --unix-socket /run/temper/control.sock -X PUT http://temper/schedules/daily-digest \
-d '{"cron": "0 8 * * *", "tz": "Europe/Berlin"}'
curl --unix-socket /run/temper/control.sock -X POST http://temper/keep-awake -d '{"seconds": 3600}'- A schedule is either a 5-field cron expression with an IANA time zone, or a one-off
attime. Registering the same key again replaces it. Temper works out the next run itself, so a rule keeps firing even if one run crashes. - The agent can list (
GET /schedules) and delete (DELETE /schedules/<key>) only its own schedules. - A keep-awake the agent asks for lasts only as long as the process that asked for it, and ends when the VM is replaced.
- Temper identifies the environment from the VM the request came from. The agent can't act on another environment.
You can see every schedule, including the agent's (source: agent), with listSchedules (Python list_schedules, Go
ListSchedules), and cancel any of them with cancelSchedule. Each time the agent registers, deletes, keeps awake, or is refused,
Temper records an environment event with source: agent and sends you a signed webhook
(environment.agent_schedule_set, environment.agent_schedule_deleted, environment.agent_keep_awake,
environment.agent_keep_awake_released, environment.agent_request_rejected). See the schedules API.
Limits on what the agent can do
An agent that has been taken over by a prompt injection should not be able to keep the VM awake forever or wake it every minute. So what the agent registers is capped per environment. Your own API calls are not subject to these caps.
| Setting | Default | Meaning |
|---|---|---|
scheduling | true | false stops the agent from registering schedules or keeping the VM awake, and ends any keep-awake it currently holds. |
max_schedules | 20 | Most schedules the agent can have (0 to 1,000). |
min_interval_secs | 900 | Shortest gap allowed between two runs of a rule (60 to 86,400). |
keep_awake_max_secs | 7200 | Longest single keep-awake the agent can ask for (0 to 86,400). |
keep_awake_daily_secs | 21600 | Total keep-awake time the agent can use in any 24 hours (0 to 86,400). |
Change them with updateAgentSettings (Python update_agent_settings, Go UpdateAgentSettings). Only the fields you send change,
and the response is the full set, so an empty update reads the current values.
const settings = await temper.environments.updateAgentSettings(env.id, { max_schedules: 5, keep_awake_max_secs: 600 });settings = temper.environments.update_agent_settings(env.id, max_schedules=5, keep_awake_max_secs=600)settings, err := client.Environments.UpdateAgentSettings(ctx, env.ID, temper.AgentSettingsPatch{
MaxSchedules: temper.Ptr(5),
KeepAwakeMaxSecs: temper.Ptr(600),
})Resources
resources sets the size of the VM and its data disk. Every field is optional:
| Field | Default | Range |
|---|---|---|
cpus | 2 | 1 to 8 |
memory_mib | 4096 | 512 to 16,384. The most memory the agent can use. |
base_memory_mib | 1024 | 256 up to memory_mib. The VM's fixed memory. Memory above it is returned to the host before a suspend, which keeps the memory snapshot small. |
disk_gib | 10 | 1 to 200. The size of the data disk. |
memory_mib is a ceiling, not a reservation: memory the agent isn't using goes back to the host. Usage is metered on what the
environment actually uses (see Usage and limits).
Running commands
Once an environment exists you can run commands, read and write files, and manage background processes in it. These run as the same user as your agent. See the exec API and the quickstart.