Packagenodus-sdk LanguagePython ≥ 3.10 Version1.0.0 APIv1

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.

signature ~/nodus
>>> nodus.Client(api_key=None, *, base_url=None, timeout=30.0, max_retries=2)
All four constructor parameters and their defaults
ParameterDefaultMeaning
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.

MethodReturnsNotes
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)
client · python ~/nodus
>>> 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
AttributeTypeMeaning
idstrStable identifier, wl_….
statusWorkloadStatusCurrent lifecycle state.
routeRoute | NoneChosen route; None until planning resolves.
spend_usdfloatCommitted spend so far, including recovery already performed.
budget_usdfloatThe brief's ceiling; cost to completion is planned against it, not an hourly rate.
created_atdatetimeAcceptance time, timezone-aware UTC.
updated_atdatetimeLast state transition.
stageslist[StageRun]One entry per stage; single-stage briefs have exactly one.
errorstr | NoneTerminal failure reason; None otherwise.
is_terminalboolTrue once the status can no longer change.
succeededboolTrue only for COMPLETED. Check this, not is_terminal (also true for failure and cancellation).
All 7 Workload methods
MethodReturnsNotes
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)
workload · python ~/nodus
>>> 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.

AttributeTypeMeaning
skustrCatalog identifier, always nodus:… (e.g. nodus:a100-40-us-east).
compute_classstr"accelerator"/"vm".
fit_classstrCapability class the brief was fitted to, e.g. a100-40: memory, interconnect, throughput envelope.
regionstrWhere the work runs, consistent with the brief's data_regions.
price_usd_hourfloatRoute's rate, USD per hour.
expected_cost_usdfloatCost to completion: run plus expected recovery.
expected_hoursfloatExpected wall-clock hours of successful execution.
interruptibleboolCan 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)
async · python ~/nodus
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.

MemberWire valueMeaning
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.

MemberWire valueMeaning
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.

MemberWire valueMeaning
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.

MemberWire valueMeaning
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().

AttributeTypeMeaning
seqintMonotonic per workload; pass the last seen as after to resume without re-reading.
idstrStable identifier; dedupe on it when processing one workload from several places.
typestrLifecycle transitions and durability milestones: workload.running/checkpoint.committed/workload.completed.
payloaddictEvent-specific detail; treat unknown keys as additive.
created_atdatetimeWhen 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
ClassHTTPWhen
NodusErrorBase class for everything below.
ConfigurationErrorBefore any network call: no key resolved, malformed base URL, contradictory arguments. Nothing sent, nothing charged.
AuthenticationError401/403Key missing, unknown, revoked, or expired. Never retry.
NotFoundError404No such workload for this tenant; another tenant's id reads absent, not forbidden.
ValidationError400/422Brief rejected; .code and .message identify the field.
IdempotencyConflictError409Same key, different payload; change the key or resend the original to replay.
RateLimitError429Too many requests; .retry_after gives the seconds.
BudgetExceededError402Would breach a cap on the key; .payload carries cap, month-to-date, estimate.
CapacityUnavailableError503No feasible route now for these requirements, deadline, residency, budget; retryable, likelier with a wider brief.
SignatureError401Signature rejected: usually a stale timestamp, re-serialized body, or wrong secret.
APIErrorother 4xx/5xxAny response with no more specific class.
APIConnectionErrorNever reached the control plane; retried up to max_retries.
APITimeoutErrorA 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)
errors · python ~/nodus
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
EndpointContract
POST /v1/workloadsSubmit a brief; Idempotency-Key required. 202 with id and revision; replays repeat the original response and set Idempotent-Replayed: true.
GET /v1/workloadsList 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}/eventsOrdered lifecycle events; after reads only the new; max 100 per call
GET /v1/workloads/{id}/ledgerCustomer-safe ledger evidence + settlement
POST /v1/workloads/{id}/cancelSafe stop
GET /v1/workloads/{id}/artifactsVerified checkpoint/output manifests
GET /v1/workloads/{id}/outputsFiles 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}/logsProcess 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/webhooksRegister signed webhook endpoint (https, public hosts only)
GET /v1/webhooksWebhook config, secret omitted
DELETE /v1/webhooksRemove webhook
POST /v1/console/signupInvite → tenant, console account, first API key, session; tenant id assigned, never chosen.
POST /v1/console/loginEmail+password → session token; one answer for every failure, no account enumeration.
POST /v1/console/logoutRevoke the current session
GET /v1/console/sessionWhoami for the current session
GET /v1/console/keysKey metadata, never a secret; session only, an API key refused.
POST /v1/console/keysIssue a key; raw value in this response and nowhere else.
POST /v1/console/keys/{id}/revokeRevoke a key in your tenant; another tenant's id reads absent.
GET /v1/console/membersRoster and open invitations; readable by any member.
POST /v1/console/invitesAdmin only. One-time invitation token, returned once; Nodus does not email it.
POST /v1/console/invite/acceptInvitation → account + session; email from the invite, never the request.
POST /v1/console/members/{id}/roleAdmin only; 409 rather than strand an account with no admin.
POST /v1/console/members/{id}/removeAdmin only; signs them out everywhere by cascade.
POST /v1/console/passwordChange your own password; signs out every other session.
POST /v1/console/members/{id}/resetAdmin only; one-time reset token for a locked-out teammate.
GET/PUT /v1/console/limitsMonthly spend cap: read, or set (admin only); null means unbounded.
GET /v1/console/usageDaily spend series, month-to-date, the cap
GET /v1/console/auditConsole actions with actor: keys, members, invitations, limits
POST /v1/signupLegacy: invite → tenant + key, no console account. Prefer /v1/console/signup.
PUT /v1/billing/profileSet the billing contact
GET /v1/billing/invoicesList invoices
POST /v1/billing/invoicesInvoice unbilled charges for one workload
GET /healthzLiveness; no auth, no database read
GET /readyzReadiness; 503 while draining or the database is unreachable