SDK reference
Every object the Python SDK exposes — new here, start with the quick start.
# Client
Credentials, base URL, pooled HTTP transport: every other SDK object is reached through this synchronous entry point.
>>> nodus.Client(api_key=None, *, base_url=None, timeout=30.0, max_retries=2)
All four constructor parameters and their defaults
| Parameter | Default | Meaning |
|---|---|---|
api_key |
$NODUS_API_KEY |
Authorization: Bearer <api_key>; missing or unreadable at construction raises ConfigurationError. |
base_url |
$NODUS_BASE_URL, then https://api.nodus.run |
Control plane to talk to. |
timeout |
30.0 |
Per-attempt deadline in seconds (not summed across retries); exceeding it raises APITimeoutError. |
max_retries |
2 |
Transient failures only: network errors, 408/429/5xx. Other 4xx return first-attempt; retrying a rejected payload cannot change the answer. |
One client per process: the pooled transport amortizes TLS handshakes; per-request clients add one per submission. Thread-safe; the workload handles it returns are not.
| Method | Returns | Notes |
|---|---|---|
client.run(**brief) |
Workload |
Submits a brief: requirements and outcomes, never a machine. Returns once accepted; not yet placed. |
client.get(workload_id) |
Workload |
Fresh handle for an existing workload. |
client.list(limit=50, offset=0, status=None) |
list[Workload] |
One page, newest first. status: a WorkloadStatus, wire string, list of either, or presets "active"/"terminal". |
client.iter_workloads(page_size=50) |
Iterator[Workload] |
Pages lazily; whole history never in memory. |
client.cancel(workload_id) |
None |
Safe stop, not a kill: in-flight work may commit its checkpoint before CANCELLED settles. |
client.healthz() |
dict |
Unauthenticated liveness probe of the configured base_url. |
A context manager: exit closes the transport, releasing pooled sockets; with-form for short-lived processes, module-level for long-lived services.
Example — construct a client (Python)
>>> import nodus >>> # Omitted arguments: read from environment. >>> client = nodus.Client( ... api_key="nk_live_…", ... base_url="https://api.nodus.run", ... timeout=30.0, ... max_retries=2, ... ) >>> client.healthz() {'status': 'ok'} >>> # Transport closes on exit. >>> with nodus.Client() as client: ... for wl in client.iter_workloads(page_size=100): ... print(wl.id, wl.status, wl.spend_usd)
# Workload
The handle returned by client.run() and
client.get(): one brief's lifecycle state, chosen route,
spend, output.
Handles are mutable: refresh() and
wait() update the instance in place and return it, so
later reads cost nothing. Unsafe to share across threads or tasks; give each its own
via client.get().
All 11 Workload attributes
| Attribute | Type | Meaning |
|---|---|---|
id | str | Stable identifier, wl_…. |
status | WorkloadStatus | Current lifecycle state. |
route | Route | None | Chosen route; None until planning resolves. |
spend_usd | float | Committed spend so far, including recovery already performed. |
budget_usd | float | The brief's ceiling; cost to completion is planned against it, not an hourly rate. |
created_at | datetime | Acceptance time, timezone-aware UTC. |
updated_at | datetime | Last state transition. |
stages | list[StageRun] | One entry per stage; single-stage briefs have exactly one. |
error | str | None | Terminal failure reason; None otherwise. |
is_terminal | bool | True once the status can no longer change. |
succeeded | bool | True only for COMPLETED. Check this, not is_terminal (also true for failure and cancellation). |
All 7 Workload methods
| Method | Returns | Notes |
|---|---|---|
workload.refresh() |
Workload |
One read; updates this instance in place. |
workload.wait(poll_seconds=2.0, timeout_seconds=None) |
Workload |
Poll until is_terminal; raises APITimeoutError when timeout_seconds elapses. The workload keeps running: a client-side deadline is not a cancellation. |
workload.events(after=0) |
list[Event] |
Ordered lifecycle events; pass the last sequence seen as after to read only the new. |
workload.stream_events() |
Iterator[Event] |
Blocks, yielding events as they occur; stops at the terminal event. |
workload.artifacts() |
list[Artifact] |
One Artifact per committed checkpoint/output manifest: stage_id, generation, sequence, final. Digests: artifact.outputs[name].sha256, artifact.files[i].sha256; a manifest is written only after every digest verifies, so no per-row verified flag. |
workload.ledger() |
Ledger |
entries (id, entry type, debit, credit, currency, evidence, timestamp) plus settlement (status, total): what a spend number is defensible against. |
workload.cancel() |
None |
Same safe stop as client.cancel(id). |
StageRun carries id,
status, continuity_mode,
completed_units, total_units,
latest_manifest. Progress counts in units the stage
defines; a reclaim resuming from the last manifest reads as work retained, not lost.
Example — submit, wait, and read the result (Python)
>>> import nodus >>> >>> with nodus.Client() as client: ... wl = client.run( ... model="7B fine-tune", ... command=["python", "train.py", "--epochs", "3"], ... peak_memory_gb=38, ... expected_runtime_hours=6, ... budget=180.00, ... continuity="checkpointed", ... interrupt_tolerance="high", ... data_regions=["us"], ... ) ... wl.wait(poll_seconds=5.0, timeout_seconds=8 * 3600) ... ... print(wl.status, wl.succeeded) ... print(wl.route.sku, wl.route.compute_class) ... print(f"{wl.spend_usd:.2f} of {wl.budget_usd:.2f} budgeted") WorkloadStatus.COMPLETED True nodus:a100-40-us-east accelerator 164.20 of 180.00 budgeted
# Route
The placement decision, in the Nodus catalog alone:
None through
ACCEPTED and
PLANNING, set from
RESERVING on.
| Attribute | Type | Meaning |
|---|---|---|
sku | str | Catalog identifier, always nodus:… (e.g. nodus:a100-40-us-east). |
compute_class | str | "accelerator"/"vm". |
fit_class | str | Capability class the brief was fitted to, e.g. a100-40: memory, interconnect, throughput envelope. |
region | str | Where the work runs, consistent with the brief's data_regions. |
price_usd_hour | float | Route's rate, USD per hour. |
expected_cost_usd | float | Cost to completion: run plus expected recovery. |
expected_hours | float | Expected wall-clock hours of successful execution. |
interruptible | bool | Can the route be reclaimed underneath the workload. |
price_usd_hour ×
expected_hours plus expected recovery:
expected_cost_usd can exceed the naive product.
budget is checked against it, never against a plan that
only fits if nothing goes wrong.
No supplier field anywhere in the API, Route and
Workload included: the product contract.
You get a catalog route and an outcome; Nodus picks and moves placement, accountable
for deadline and budget.
# AsyncClient
nodus.AsyncClient mirrors
Client with the same constructor
arguments; run/get/list/cancel/healthz
are coroutines, iter_workloads an async iterator, the
client an async context manager.
Calls return an AsyncWorkload with the same attributes,
types, and in-place mutation as
Workload plus awaitable
refresh()/wait()/events()/artifacts()/ledger()/cancel()
and async for event in wl.stream_events().
wait() yields between polls; thousands of concurrent
waits cost a task each, not a thread each.
Example — submit workloads concurrently (Python)
import asyncio
import nodus
async def sweep(client, lr: float) -> nodus.AsyncWorkload:
wl = await client.run(
model=f"7B sweep lr={lr}",
command=["python", "train.py", "--lr", str(lr)],
peak_memory_gb=38,
expected_runtime_hours=4,
budget=120.00,
continuity="checkpointed",
)
await wl.wait(poll_seconds=5.0)
return wl
async def main() -> None:
async with nodus.AsyncClient() as client:
runs = await asyncio.gather(
*(sweep(client, lr) for lr in (1e-5, 3e-5, 1e-4)),
)
for wl in runs:
print(wl.id, wl.status, wl.route.sku, f"${wl.spend_usd:.2f}")
asyncio.run(main())
# Types
Enums compare equal to their wire strings, so
wl.status == "running" needs no imports; enum fields
accept strings too.
ComputeClass — accelerator vs vm, an output of fitting
ComputeClass is an output of fitting the brief, not
declared. A brief needing no accelerator routes to a VM class through identical
planning, pricing, recovery, settlement; multi-stage briefs commonly mix both.
| Member | Wire value | Meaning |
|---|---|---|
ComputeClass.ACCELERATOR | "accelerator" | Device-memory high-water mark: training, fine-tuning, batch inference. |
ComputeClass.VM | "vm" | Plain CPU and memory: data prep, eval harnesses, ETL, scoring. |
ContinuityMode is what must survive an interruption,
the brief's most consequential field; it decides what recovery may do.
| Member | Wire value | Meaning |
|---|---|---|
ContinuityMode.CHECKPOINTED | "checkpointed" | Default: state commits to verified manifests as work proceeds; a reclaim resumes from the last, repeating only work since. |
ContinuityMode.RESTARTABLE | "restartable" | No intermediate state worth keeping; a reclaim reruns the stage, beating checkpointing for short runs. |
ContinuityMode.EPHEMERAL | "ephemeral" | Not worth resuming or repeating; a reclaim ends the stage. |
InterruptTolerance — low, medium, high
InterruptTolerance is how much interruption the outcome
absorbs: higher widens feasible routes and lowers cost to completion; lower narrows
and raises it.
| Member | Wire value | Meaning |
|---|---|---|
InterruptTolerance.LOW | "low" | Prefer routes not reclaimed; for deadlines with no slack. |
InterruptTolerance.MEDIUM | "medium" | Interruption acceptable if the deadline holds. |
InterruptTolerance.HIGH | "high" | Interruption routine; with CHECKPOINTED the lowest cost to completion. |
WorkloadStatus — all 9 lifecycle states, in order
WorkloadStatus is the lifecycle in order; workloads can
bounce between RECOVERING and
RUNNING any number of times before settling.
| Member | Wire value | Meaning |
|---|---|---|
WorkloadStatus.ACCEPTED | "accepted" | Brief validated, durable; no route yet. |
WorkloadStatus.PLANNING | "planning" | Fitting requirements to a capability class; pricing completion against budget and deadline. |
WorkloadStatus.RESERVING | "reserving" | Holding capacity; route set from here on. |
WorkloadStatus.PROVISIONING | "provisioning" | Environment building, inputs staging. |
WorkloadStatus.RUNNING | "running" | Executing; spend_usd and stage progress advance. |
WorkloadStatus.RECOVERING | "recovering" | Capacity reclaimed or environment failed; Nodus re-places the work under its continuity mode. Not an error, not your move. |
WorkloadStatus.COMPLETED | "completed" | Terminal; outcome produced, artifacts verified; only here is succeeded true. |
WorkloadStatus.FAILED | "failed" | Terminal; recovery could not deliver within the brief; error says why. |
WorkloadStatus.CANCELLED | "cancelled" | Terminal; stopped on request, after in-flight work could commit. |
COMPLETED/FAILED/CANCELLED is exactly the terminal
set, where is_terminal is true and
wait() returns. Everything else can still change; never
read RECOVERING as failure.
The SDK also exports brief models
Requirements/Outcome/Continuity/Policy/StageSpec;
pass kwargs to run() and never touch them, or construct
directly for pre-call validation.
Event — all 5 attributes
Event: yielded by
workload.events() and
stream_events().
| Attribute | Type | Meaning |
|---|---|---|
seq | int | Monotonic per workload; pass the last seen as after to resume without re-reading. |
id | str | Stable identifier; dedupe on it when processing one workload from several places. |
type | str | Lifecycle transitions and durability milestones: workload.running/checkpoint.committed/workload.completed. |
payload | dict | Event-specific detail; treat unknown keys as additive. |
created_at | datetime | When the control plane recorded it. |
# Errors
One except nodus.NodusError catches everything the SDK
raises, and nothing else: every error inherits it. Each carries
request_id where the control plane returned one; quote
it in support requests.
All 13 error classes — HTTP status and when each is raised
| Class | HTTP | When |
|---|---|---|
NodusError | — | Base class for everything below. |
ConfigurationError | — | Before any network call: no key resolved, malformed base URL, contradictory arguments. Nothing sent, nothing charged. |
AuthenticationError | 401/403 | Key missing, unknown, revoked, or expired. Never retry. |
NotFoundError | 404 | No such workload for this tenant; another tenant's id reads absent, not forbidden. |
ValidationError | 400/422 | Brief rejected; .code and .message identify the field. |
IdempotencyConflictError | 409 | Same key, different payload; change the key or resend the original to replay. |
RateLimitError | 429 | Too many requests; .retry_after gives the seconds. |
BudgetExceededError | 402 | Would breach a cap on the key; .payload carries cap, month-to-date, estimate. |
CapacityUnavailableError | 503 | No feasible route now for these requirements, deadline, residency, budget; retryable, likelier with a wider brief. |
SignatureError | 401 | Signature rejected: usually a stale timestamp, re-serialized body, or wrong secret. |
APIError | other 4xx/5xx | Any response with no more specific class. |
APIConnectionError | — | Never reached the control plane; retried up to max_retries. |
APITimeoutError | — | A deadline elapsed: timeout or wait(timeout_seconds=…). The workload keeps running. |
One distinction matters: can the condition change on its own? Time clears rate limits and capacity; asking for less clears budget caps; authentication and validation never clear.
Example — error handling pattern (Python)
import time
import nodus
def submit(client, brief: dict, attempts: int = 6) -> nodus.Workload:
brief = dict(brief)
for _ in range(attempts):
try:
return client.run(**brief)
except nodus.AuthenticationError:
raise
except nodus.ValidationError as exc:
raise RuntimeError(f"bad brief [{exc.code}] {exc.message}") from exc
except nodus.RateLimitError as exc:
time.sleep(exc.retry_after)
except nodus.BudgetExceededError as exc:
cap = exc.payload["monthly_spend_cap_usd"]
spent = exc.payload["month_to_date_usd"]
needed = exc.payload["estimated_cost_usd"]
headroom = cap - spent
if needed > headroom:
raise
brief["budget"] = headroom
except nodus.CapacityUnavailableError:
brief["interrupt_tolerance"] = "high"
brief["continuity"] = "checkpointed"
brief.pop("finish_by", None)
time.sleep(30)
raise RuntimeError("no feasible route after widening the brief")
Widening an infeasible brief; stable keys for retry loops
Feasibility is a function of the brief, not capacity alone: raising
interrupt_tolerance and dropping
finish_by admits interruptible routes the deadline
excluded; continuity="checkpointed" bounds the trade to
work since the last checkpoint. Relaxing
data_regions would widen further; residency is
compliance, not preference; leave it alone.
run() mints a fresh
Idempotency-Key covering only that call's internal
retries; an application retry loop can create a second paid workload if the
submission landed and the response was lost. Pass a key derived from the thing being
run, stable across retries:
client.run(**brief, idempotency_key=f"nightly-eval-{run_date}");
full rule in retries & idempotency.
# HTTP surface
The SDK is a thin client over this surface. Tenant-scoped throughout: another tenant’s workload id reads as absent, not forbidden. Submission rules: retries & idempotency.
All 36 HTTP endpoints
| Endpoint | Contract |
|---|---|
POST /v1/workloads | Submit a brief; Idempotency-Key required. 202 with id and revision; replays repeat the original response and set Idempotent-Replayed: true. |
GET /v1/workloads | List for the authenticated tenant: limit (default 50, max 100), offset and status (presets active/terminal or comma-separated statuses); next_offset when more remain. |
GET /v1/workloads/{id} | Status, the nodus:… customer route, spend, per-stage progress |
GET /v1/workloads/{id}/events | Ordered lifecycle events; after reads only the new; max 100 per call |
GET /v1/workloads/{id}/ledger | Customer-safe ledger evidence + settlement |
POST /v1/workloads/{id}/cancel | Safe stop |
GET /v1/workloads/{id}/artifacts | Verified checkpoint/output manifests |
GET /v1/workloads/{id}/outputs | Files completed stages published: name/stage_id/sha256/bytes/download path. No storage keys; the path is the handle. |
GET /v1/workloads/{id}/outputs/{name} | Stream one output; X-Nodus-SHA256 is the digest verified on commit. Pass stage when two stages publish the same name; otherwise 409, not a guess. |
GET /v1/workloads/{id}/logs | Process stdout/stderr. Optional stage/generation params; default: latest attempt that recorded anything, named in X-Nodus-Stage-Id/X-Nodus-Generation. Recorded alongside checkpoints; failed stages have logs too. |
PUT /v1/webhooks | Register signed webhook endpoint (https, public hosts only) |
GET /v1/webhooks | Webhook config, secret omitted |
DELETE /v1/webhooks | Remove webhook |
POST /v1/console/signup | Invite → tenant, console account, first API key, session; tenant id assigned, never chosen. |
POST /v1/console/login | Email+password → session token; one answer for every failure, no account enumeration. |
POST /v1/console/logout | Revoke the current session |
GET /v1/console/session | Whoami for the current session |
GET /v1/console/keys | Key metadata, never a secret; session only, an API key refused. |
POST /v1/console/keys | Issue a key; raw value in this response and nowhere else. |
POST /v1/console/keys/{id}/revoke | Revoke a key in your tenant; another tenant's id reads absent. |
GET /v1/console/members | Roster and open invitations; readable by any member. |
POST /v1/console/invites | Admin only. One-time invitation token, returned once; Nodus does not email it. |
POST /v1/console/invite/accept | Invitation → account + session; email from the invite, never the request. |
POST /v1/console/members/{id}/role | Admin only; 409 rather than strand an account with no admin. |
POST /v1/console/members/{id}/remove | Admin only; signs them out everywhere by cascade. |
POST /v1/console/password | Change your own password; signs out every other session. |
POST /v1/console/members/{id}/reset | Admin only; one-time reset token for a locked-out teammate. |
GET/PUT /v1/console/limits | Monthly spend cap: read, or set (admin only); null means unbounded. |
GET /v1/console/usage | Daily spend series, month-to-date, the cap |
GET /v1/console/audit | Console actions with actor: keys, members, invitations, limits |
POST /v1/signup | Legacy: invite → tenant + key, no console account. Prefer /v1/console/signup. |
PUT /v1/billing/profile | Set the billing contact |
GET /v1/billing/invoices | List invoices |
POST /v1/billing/invoices | Invoice unbilled charges for one workload |
GET /healthz | Liveness; no auth, no database read |
GET /readyz | Readiness; 503 while draining or the database is unreachable |