temperDocs
Menu

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":
        raise
import 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 config
temper.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 with TEMPER_ 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:

FieldSet byValues
desired_stateYour API calls and the idle ladderrunning, suspended, stopped, destroyed
stateThe host that runs the VM, reported back to Temperpending, 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.

CallWhat it does
suspendTakes a memory snapshot. Only works when desired_state is running. If the environment is stopped or destroyed, it returns 409 invalid_state_transition.
resumeBrings a suspended or stopped environment back to running. It does not count as activity.
wakeLike 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.
destroySets 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:

  1. Running. The VM is up and the agent answers immediately. Idle memory is handed back to the host.
  2. Suspended (memory snapshot). After suspend_after_secs without 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.
  3. Powered off. After stop_after_secs without 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 minutes
temper.environments.keep_awake(env.id, 1800)  # 30 minutes
_, err = client.Environments.KeepAwake(ctx, env.ID, 1800) // 30 minutes

Wakeups 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:

bash
# 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 at time. 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.

SettingDefaultMeaning
schedulingtruefalse stops the agent from registering schedules or keeping the VM awake, and ends any keep-awake it currently holds.
max_schedules20Most schedules the agent can have (0 to 1,000).
min_interval_secs900Shortest gap allowed between two runs of a rule (60 to 86,400).
keep_awake_max_secs7200Longest single keep-awake the agent can ask for (0 to 86,400).
keep_awake_daily_secs21600Total 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),
})

See Update agent settings.

Resources

resources sets the size of the VM and its data disk. Every field is optional:

FieldDefaultRange
cpus21 to 8
memory_mib4096512 to 16,384. The most memory the agent can use.
base_memory_mib1024256 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_gib101 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.