Launch a box and wait
The first thing every integration does: launch an environment ("box") from a template, wait until it's ready, do some work, and wind it down. This page walks that whole lifecycle with the SDK.
You'll need a client from the SDK quickstart, an orgId,
and the templateId of a template whose bundle has finished baking (its
status is ready — launching from an unbaked template is refused with
template_not_baked).
1. Launch — always with an idempotency key
environments.launch provisions a real EC2 instance in your connected AWS
account, so a duplicated launch is not just wasted API quota — it's a second
box on your AWS bill. Pass an idempotency key with every launch: if the
request is retried (by you, or by a flaky network), the API returns the
original box instead of provisioning a duplicate.
- TypeScript
- Python
const box = await slothbox.environments.launch(
{
orgId,
body: { templateId, name: "nightly-runner" },
},
// Sent as the Idempotency-Key header. Reuse the same key when you retry
// the same logical launch; use a fresh key for each new box you intend.
{ idempotencyKey: `nightly-${runId}` },
);
console.log(box.envId, box.status); // e.g. "env_01HXYZ…" "pending"
box = client.environments.launch(
org_id,
{"templateId": template_id, "name": "nightly-runner"},
# Sent as the Idempotency-Key header. Reuse the same key when you retry
# the same logical launch; use a fresh key for each new box you intend.
idempotency_key=f"nightly-{run_id}",
)
print(box.env_id, box.status) # e.g. "env_01HXYZ…" "pending"
The launch returns immediately with the new environment in a transitional
state — provisioning continues in the background. A box moves through
pending → provisioning → running; a launch that can't complete lands in
failed.
2. Wait until it's ready
The SDK ships lifecycle waiters that encode the status machine for you:
polling with backoff, an overall timeout (10 minutes by default — a real EC2
launch usually completes in 2–4 minutes), and terminal-state detection, so a box
that lands in a state it can't leave on its own throws a WaiterStateError
immediately instead of spinning until the deadline. Running out of time
throws a WaiterTimeoutError with the last status it saw.
- TypeScript
- Python
// Launch + wait in one call. launchAndWait ALWAYS sends an Idempotency-Key —
// the one you pass, or an auto-generated UUID — so the launch half is
// replay-safe; the waiter half resolves once the box is `running`.
const box = await slothbox.environments.launchAndWait(
{ orgId, body: { templateId, name: "nightly-runner" } },
// Pass your own stable key to deduplicate across process restarts:
{ idempotencyKey: `nightly-${runId}` },
);
// Or wait for a box you launched earlier:
await slothbox.environments.waitUntilReady({ orgId, envId: box.envId });
# Launch + wait in one call. launch_and_wait ALWAYS sends an Idempotency-Key —
# the one you pass, or an auto-generated UUID — so the launch half is
# replay-safe; the waiter half returns once the box is `running`.
box = client.environments.launch_and_wait(
org_id,
{"templateId": template_id, "name": "nightly-runner"},
# Pass your own stable key to deduplicate across process restarts:
idempotency_key=f"nightly-{run_id}",
)
# Or wait for a box you launched earlier:
client.environments.wait_until_ready(org_id, box.env_id)
All waiters take a timeout and poll-interval bounds — timeoutMs /
pollIntervalMs / maxPollIntervalMs and an abort signal in TypeScript;
timeout / poll_interval / max_poll_interval (seconds) in Python (see
Configuration). The same machinery
covers the other slow lifecycles: environments.waitUntilStopped /
wait_until_stopped and — for the bake that must finish before a template is
launchable at all — templates.waitUntilBaked / wait_until_baked.
If you'd rather poll yourself, the loop the waiters encapsulate looks like this:
- TypeScript
- Python
const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms));
async function waitUntilRunning(orgId: string, envId: string) {
const deadline = Date.now() + 10 * 60_000; // overall timeout
for (;;) {
const env = await slothbox.environments.get({ orgId, envId });
if (env.status === "running") return env;
// States a launching box can't leave on its own — fail fast. A `stopped`
// observation usually means auto-sleep or another actor stopped it.
if (env.status !== "pending" && env.status !== "provisioning") {
throw new Error(
`Box ${envId} reached terminal state ${env.status}: ${env.statusReason ?? ""}`,
);
}
if (Date.now() > deadline) throw new Error(`Timed out waiting for ${envId}`);
await sleep(7_000);
}
}
import time
def wait_until_running(org_id: str, env_id: str):
deadline = time.monotonic() + 10 * 60 # overall timeout
while True:
env = client.environments.get(org_id, env_id)
if env.status == "running":
return env
# States a launching box can't leave on its own — fail fast. A `stopped`
# observation usually means auto-sleep or another actor stopped it.
if env.status not in ("pending", "provisioning"):
raise RuntimeError(
f"Box {env_id} reached terminal state {env.status}: {env.status_reason or ''}"
)
if time.monotonic() > deadline:
raise TimeoutError(f"Timed out waiting for {env_id}")
time.sleep(7)
Pace your polling deliberately: launches and bakes sit in a tight org-wide rate tier (see Errors, retries & rate limits), and every status read does real work against your connected AWS account. The built-in waiters start at a 7-second interval and back off toward a 30-second ceiling for exactly that reason — there's nothing to gain from polling harder.
3. Ready isn't forever — auto-sleep
A running box does not stay running unconditionally. Organizations can have
auto-sleep policies — idle boxes are stopped automatically, and scheduled
sleep windows can stop them at set times — precisely so an unattended box
doesn't burn AWS spend overnight. So treat "ready" as a state you observed,
not a guarantee:
- Check before you reconnect. If time has passed since the box was ready,
read its status again — and
startit if it was put to sleep:
- TypeScript
- Python
let env = await slothbox.environments.get({ orgId, envId });
if (env.status === "stopped") {
env = await slothbox.environments.start({ orgId, envId });
env = await slothbox.environments.waitUntilReady({ orgId, envId });
}
env = client.environments.get(org_id, env_id)
if env.status == "stopped":
env = client.environments.start(org_id, env_id)
env = client.environments.wait_until_ready(org_id, env_id)
- Or let the events come to you. Subscribe a
webhook endpoint to
environment.stopped/environment.startedand your system observes sleep and wake as they happen, with no polling at all.
A stopped box keeps its disk and its identity — starting it again is fast and
returns the same envId.
4. Wind it down
Stop a box when you're done for now; terminate it when you're done for good:
- TypeScript
- Python
// Pause: the instance stops, the disk and envId survive. Frees up your
// seat's concurrent-box headroom too.
await slothbox.environments.stop({ orgId, envId });
// Gone for good: terminates the instance. This is irreversible — further
// lifecycle calls on the box are refused with `environment_terminated`.
await slothbox.environments.terminate({ orgId, envId });
# Pause: the instance stops, the disk and env_id survive. Frees up your
# seat's concurrent-box headroom too.
client.environments.stop(org_id, env_id)
# Gone for good: terminates the instance. This is irreversible — further
# lifecycle calls on the box are refused with `environment_terminated`.
client.environments.terminate(org_id, env_id)
Both return the environment with its new transitional status (stopping,
terminating); use waitUntilStopped / wait_until_stopped (or poll, as
above) if you need to block until the box has actually wound down.
Stopping promptly is more than tidiness: concurrent-box headroom frees up as soon as a box stops, and an idle box you forgot about is exactly what auto-sleep exists to catch.
Common launch failures
Launches are refused with a 409 and a machine-readable error.code when
the org isn't in a launchable state — seat_ceiling_exceeded,
no_active_aws_connection, template_not_baked. The
error taxonomy covers what
each one means and what to do about it.