Skip to main content

orkestr Python SDK

EU-hosted sandbox VMs and managed agents. Type-safe Python client for the orkestr sandboxes and Agent Runtime APIs (api.orkestr.eu/v1).

Status: 0.x. The client surface may still have breaking changes within the 0.x line; 1.0.0 will signal API stability.

Install

pip install orkestr

Requires Python 3.10+.

Authenticate

Mint an API token in the orkestr console with the sandboxes:write scope.

export ORKESTR_API_KEY="ork_..."

The SDK picks the key up from ORKESTR_API_KEY or you pass it explicitly.

Quick start

One-shot execution inside a fresh sandbox. The with block auto-terminates the sandbox on exit, preventing runaway costs if the caller crashes.

from orkestr import Sandbox

with Sandbox.create(template="python-3.12") as sbx:
    sbx.files.write("/workspace/main.py", "print(sum(range(1_000_000)))")
    result = sbx.exec("python /workspace/main.py")
    print(result.stdout)        # 499999500000
    print(result.duration_ms)   # ~120

API

Create a sandbox

sbx = Sandbox.create(
    template="python-3.12",          # one of the templates listed below
    size="small",                    # "small" | "medium" | "large" (tier-capped)
    network="off",                   # "off" | "restricted" | "open"
    timeout_seconds=600,             # auto-terminate after this many seconds
    env={"OPENAI_API_KEY": "sk-..."},
    metadata={"agent_run": "r_123"},
    region="fsn1",                   # region code (Sandbox.limits().regions) | None for auto
    api_key=None,                    # falls back to ORKESTR_API_KEY
)
print(sbx.id)             # "sbx_01HXYZ..."
print(sbx.status)         # "running"

Custom egress allowlist (restricted sandboxes)

A restricted sandbox reaches a default set of HTTPS hosts (package registries, GitHub, major LLM APIs). Pass allow_domains to point it at your own hosts instead - an internal API, a private package mirror, a vendor endpoint. The list replaces the default entirely (still HTTPS-only and proxy-mediated), so include any registries you still need. Sandbox.limits().default_egress_domains returns the default set to start from.

sbx = Sandbox.create(
    template="python-3.12",
    network="restricted",
    allow_domains=["pypi.org", "pythonhosted.org", "api.internal.example.com"],
)

Templates

Template Description
python-3.12 CPython 3.12 with pip and common libs
python-3.12-bare CPython 3.12 only, faster start
node-22 Node 22 with npm
debian-12 Real Debian bookworm - general-purpose base where apt-get works

Sizes

size picks from a fixed menu. Allowed sizes depend on your sandbox access tier (free = email verified, payg = card on file, enterprise).

Size vCPU RAM Tiers
small 1 1 GB free, payg, enterprise
medium 2 4 GB payg, enterprise
large 4 8 GB payg, enterprise

Check your tier limits

Sandbox.limits() reports the sizes and caps available to your API key's access tier — pick a size up front instead of discovering the limit from a PlanLimitError. Handy when the same code runs under keys on different tiers (e.g. a reseller provisioning per customer).

limits = Sandbox.limits()
limits.plan                        # "free"  (your sandbox access tier)
limits.allowed_sizes               # ["small"]
"medium" in limits.allowed_sizes   # False
limits.max_concurrent              # 1
limits.trial_credit_eur            # 5.0   (one-time trial credit, EUR; payg gets 25)
limits.trial_credit_used_eur       # 1.25  (EUR of the credit consumed)
for s in limits.sizes:             # full menu, each with .allowed
    print(s.size, s.cpu, s.memory_mb, s.allowed)

Sandbox compute is a one-time EUR trial credit that does not reset: compute is metered at the public pay-as-you-go rates and drawn against the credit. When trial_credit_used_eur >= trial_credit_eur, Sandbox.create() raises PlanLimitError with code="trial_exhausted" (free hits a hard wall; paid switches to pay-as-you-go). Metering rolls up actual cgroup CPU + working-set RAM per second, so an idle sandbox accrues only what it really uses.

Live metrics

sbx.metrics() returns this sandbox's live CPU and memory - the latest reading, a rolling ~60s sample window for sparklines, and its lifetime totals. Use it to watch a workload for saturation or memory pressure without instrumenting the workload itself.

m = sbx.metrics()
m.cpu.usage_percent     # 47.0  (% of allocated cores; pegged 1-core = 100)
m.cpu.usage_cores       # 0.94  (cores in use of m.cpu.cores)
m.memory.usage_bytes    # 1879048192  (working set, excludes reclaimable cache)
m.memory.usage_percent  # 43.7  (% of m.memory.limit_bytes)
m.lifetime.cpu_seconds  # 1284.31  (on-CPU seconds since the sandbox started)
for s in m.samples:     # oldest first; s.t is a datetime
    print(s.t, s.cpu_percent, s.mem_bytes)

It is telemetry, not a state change: a paused or terminated sandbox returns a result with null live usage (check m.sandbox_status) and an empty samples window - lifetime is still populated. Poll no faster than m.sample_interval_seconds; pass since=<datetime> to fetch only samples newer than your last poll.

Expose a port (preview URLs)

sbx.get_host(port) returns a public hostname for an HTTP port a process in the sandbox is serving. Build the URL as f"https://{sbx.get_host(port)}" and open it in a browser or embed it in your app: a live preview of a dev server running inside the sandbox.

sbx = Sandbox.create(template="node-22", network="open")

# Start something that listens on a port (non-blocking, in the background):
sbx.exec("nohup python3 -m http.server 3000 >/tmp/srv.log 2>&1 &")
# ...or your framework's dev server, e.g. `npm run dev -- --port 3000`.

host = sbx.get_host(3000)          # "3000-01hxyz....sbx.orkestr.run"
url = f"https://{host}"            # open in a browser or embed it in your app

The URL is public - its only capability is the unguessable sandbox id in the hostname, since an embedded preview cannot send an Authorization header. It is stable for the sandbox's lifetime but only serves traffic while the sandbox is running (a paused or terminated sandbox returns nothing). WebSockets ride through, so dev-server HMR works.

get_host requires a card on file (paid tier) and a networked sandbox (network="restricted" or "open"); an off sandbox has no port to expose. It raises in either case. The sandbox GET/list payloads also carry preview_host_base (the port-less <id>.<base> suffix, or None) for building these URLs in a UI.

Run a command

result = sbx.exec("python /workspace/main.py", timeout_seconds=60)
result.stdout       # str
result.stderr       # str
result.exit_code    # int
result.duration_ms  # int

Stream a long command

for chunk in sbx.exec_stream("python long_task.py"):
    if chunk.stream == "stdout":
        print(chunk.data, end="", flush=True)
    else:
        print(chunk.data, end="", flush=True, file=sys.stderr)

Files

sbx.files.write("/workspace/data.json", '{"x": 1}')
sbx.files.write_bytes("/workspace/blob.bin", b"\x00\x01\x02")

content = sbx.files.read("/workspace/out.txt")       # returns str
raw = sbx.files.read_bytes("/workspace/blob.bin")     # returns bytes

for entry in sbx.files.list("/workspace"):
    print(entry.name, entry.is_dir, entry.size)

sbx.files.delete("/workspace/out.txt")

# Many files in one call (scaffolding a project without N round-trips).
# Per-file results: a bad path fails its own row, the rest still land.
results = sbx.files.write_batch({
    "/workspace/src/app.py": "print('hi')",
    "/workspace/README.md": "# my app",
})

# Large files: mint a pre-signed URL and move bytes directly - plain
# HTTP, no auth header, no base64, no 16 MiB cap. Reusable until expiry.
url = sbx.files.presign_upload("/workspace/dataset.csv", expires_in=600)
# requests.put(url.url, data=open("dataset.csv", "rb"))

url = sbx.files.presign_download("/workspace/out/report.pdf")
print(url.url)   # hand to a browser, curl, or another service

Persistent volumes

/workspace is fast scratch space, but it is RAM-backed and gone when the sandbox terminates. To keep data across sandboxes, attach a persistent volume - a named disk mounted at /persist. Anything written under /persist survives termination and comes back when a later sandbox attaches the same volume.

# First run: write results to the volume.
sbx = Sandbox.create(template="python-3.12", volume="my-cache")
sbx.run('echo "trained-weights-v1" > /persist/model.txt')
sbx.terminate()

# Hours or days later, a brand-new sandbox re-attaches the same volume:
sbx = Sandbox.create(template="python-3.12", volume="my-cache")
print(sbx.run("cat /persist/model.txt").stdout)   # -> trained-weights-v1
print(sbx.volume)                                   # -> "my-cache"

A volume is created on first reference (default 10 GB), so you usually just name it in create(). Notes:

  • A volume can back one running sandbox at a time - attaching a volume that's already in use raises a 409 (volume_in_use); terminate the holder first.
  • A volume lives in one region, and a sandbox attaching it runs there. A new volume homes wherever its first sandbox lands (so a region on that first Sandbox.create places both); after that, passing a different region is a clear 400 - omit it, or move the volume first.
  • Manage volumes explicitly to pick a size or region up front, relocate one, or clean up:
from orkestr import Volume

vol = Volume.create("datasets", size_gb=50, region="rbx")
for v in Volume.list():
    print(v.name, v.size_mb, v.status, v.region)
Volume.move(vol.id, "hel1")   # detached only; data follows via its checkpoint
Volume.delete(vol.id)     # rejected while attached to a running sandbox

# Auto-destroy a temporary volume: days (1-3650) or hours (1-24), one or the
# other. The platform deletes it and its data past the deadline, so a failed
# cleanup of your own does not leave storage billing forever.
job = Volume.create("run-42", size_gb=2, max_lifetime_hours=6)
Volume.update(job.id, max_lifetime_hours=2)     # change it
Volume.update(job.id, max_lifetime_days=None)   # or keep it after all

Valid region ids come from Sandbox.limits().regions - the live fleet, not a fixed list.

Lifecycle webhooks

Get POSTed instead of polling: register an endpoint and orkestr delivers a signed JSON payload on lifecycle events (sandbox.created / .paused / .resumed / .terminated / .expired, template.build_finished, volume.moved). The URL must be publicly reachable; the signing secret is returned only by create - store it then.

from orkestr import Webhook, verify_webhook_signature

whk = Webhook.create(
    "https://example.com/hooks/orkestr",
    event_types=["sandbox.paused", "sandbox.expired"],  # omit for all events
)
save(whk.secret)  # whsec_..., shown only here

# In your receiver, verify the RAW body before parsing:
if not verify_webhook_signature(secret, raw_body, sig_header):
    return 401  # X-Orkestr-Signature did not match

Webhook.list()           # secret never included
Webhook.enable(whk.id)   # re-arm after auto-disable (20 consecutive failures)
Webhook.delete(whk.id)

Deliveries carry X-Orkestr-Event, X-Orkestr-Delivery (stable across retries - deduplicate on it) and X-Orkestr-Signature (sha256= + HMAC-SHA256 hex of the body). Failed deliveries retry 3 times, 10 s apart.

Pause + resume

Pausing snapshots the sandbox memory + disk and stops the compute meter. Resume restores it on the same or a different host. pause() returns the sandbox id; persist it across processes and pass to Sandbox.resume.

sbx = Sandbox.create(template="node-22", network="restricted")
sandbox_id = sbx.pause()
# ... minutes or hours later, from any worker:
sbx = Sandbox.resume(sandbox_id)

Snapshot retention is tier-capped (free: 1, payg: 10, enterprise: 50). Calling pause() over the cap raises SnapshotCapReached.

Snapshot fork

Capture a running sandbox into a named snapshot, then boot any number of independent children from it - agent forking, parallel exploration of candidate fixes, or fan-out from a warmed-up environment. The source keeps running.

from orkestr import Sandbox, Snapshot

base = Sandbox.create(template="python-3.12")
base.exec("pip install -q pandas && echo ready > /workspace/state")

snap = base.snapshot(name="warmed")          # base keeps running
a = Sandbox.create(snapshot=snap.id)          # fork 1
b = Sandbox.create(snapshot=snap.id)          # fork 2 - independent of a

Snapshot.list()                               # your snapshots
Snapshot.delete(snap.id)

Each fork inherits the sandbox's filesystem and memory at snapshot time, then diverges - a write in one fork is invisible to the others and to the source. Named snapshots are tier-capped (Sandbox.limits().max_named_snapshots). v1 supports off-network, volume-less sandboxes.

Terminate

Context-manager exit calls terminate(). Outside with:

sbx.terminate()

After terminate() the sandbox row stays in your account history but the VM and any in-memory state are gone.

List your sandboxes

for sbx in Sandbox.list(status="running"):
    print(sbx.id, sbx.template, sbx.created_at)

Agents (private beta)

Invoke a managed agent you defined in the orkestr console. Each conversation (session) runs in its own isolated VM in the EU; orkestr does not store the conversation. Needs an API token with the agents:invoke scope (plus agents:read / agents:write for the management calls), and an account in the Agent Runtime beta.

from orkestr import Agent, AgentRun

agent = Agent.get("support-bot")                 # name or agt_ id
result = agent.invoke("Where is my order?", end_user_id="cust-42")
print(result.output)

# Continue the conversation (it lives in the session's VM):
agent.invoke("And when will it arrive?", session_id=result.session_id)

# Stream events as they happen:
for event in agent.stream("Summarise this repo", end_user_id="cust-42"):
    if event.type == "message":
        print(event.data["content"])

# A tool that needs approval pauses the run:
if result.status == "pending_approval":
    AgentRun.get(result.run_id).approve(result.approval.request_id)

Also on Agent: versions(), publish(config, promote=[...]), set_alias(name, version), end_session(id), usage(days), memory(end_user_id), clear_memory(...) and erase_end_user(id) (GDPR). Errors: AgentNotFound, AgentBusy, ApprovalPending, SessionEnded.

Errors

All SDK errors inherit from orkestr.OrkestrError.

Exception When
AuthError Missing / invalid / expired API key, or scope mismatch
RateLimitError Plan rate limit hit
PlanLimitError Sandbox limit, concurrent limit, snapshot cap
FleetFull Fleet momentarily at capacity (auto-retried first)
SandboxNotFound sandbox_id doesn't exist or isn't yours
SandboxNotReady Operation called on a paused / terminated sandbox
ExecTimeout exec() exceeded timeout_seconds
NetworkPolicyError Command tried to reach a host the policy blocks
OrkestrError Any other API-level error

When the fleet is momentarily full, create() and resume() back off and retry automatically (honouring the server's Retry-After), so a transient capacity blip doesn't kill a long-running agent. They only raise FleetFull once the retry budget is spent - tune it per call with max_retries / retry_max_seconds, or pass max_retries=0 to handle the backoff yourself.

Async support

The MVP SDK is sync only. Async (AsyncSandbox) lands in v0.2.0 if a design partner blocks on it; the wire format is identical.

Versioning

Follows Semantic Versioning. The SDK targets the /v1/sandboxes API; bumps to v1 of the API are non-breaking for SDK callers. v2 of the API will require an SDK major.

Metadata

Release files for orkestr 0.6.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for orkestr 0.6.0
File Size Uploaded
orkestr-0.6.0.tar.gz 61.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for orkestr 0.6.0
File Interpreter ABI Platform
orkestr-0.6.0-py3-none-any.whl Python 3 none any Details

Total release size: 104.2 kB

Release files / orkestr-0.6.0.tar.gz

Download URL orkestr-0.6.0.tar.gz
Size 61.0 kB
Tags Source
SHA-256 checksum
How to use checksums
e9b62b06ce2de7dd30c3c829ecb5cc710a4a4ede29cbd012ede634763e3dba07
BLAKE2b-256 checksum
How to use checksums
02bf1f9480bc60ef9a99cc448f5b15ca69e5c69f6eb548bd723c739a20d77115
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.13

Release files / orkestr-0.6.0-py3-none-any.whl

Download URL orkestr-0.6.0-py3-none-any.whl
Size 43.2 kB
Tags Python 3
SHA-256 checksum
How to use checksums
f14bb7e5751d6e99be1062634599b8712e1c7e7de37607b83c568930496cfbf3
BLAKE2b-256 checksum
How to use checksums
ba3c56de4e2b93bea42b0384c557df1e10c40743f9bf73670700019ce9e0a722
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.12.13

Release history Release notifications | RSS feed

0.6.3

2 release files

0.6.2

2 release files

0.6.1

2 release files

This release

0.6.0 This release

2 release files

0.5.1

2 release files

0.5.0

2 release files

0.4.2

2 release files

0.4.1

2 release files

0.4.0

2 release files

0.3.0

2 release files

0.2.2

2 release files

0.2.1

2 release files

0.2.0

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page