modulex-python, import name modulex) is the official async client for the ModuleX REST API. It is async-only: there is one client class, Modulex, backed by an httpx.AsyncClient, and every resource method is a coroutine you await. This page covers installation, client construction, authentication, environment-variable fallbacks, the request lifecycle, and the two facts that make the Python SDK different from the JavaScript SDK — its environment-variable fallbacks and its Python-only subscriptions resource.
For the full route-to-method coverage table and every cross-SDK naming difference, see the SDK parity matrix. For consuming run streams and answering human-in-the-loop prompts, see Streaming & HITL. For the exception tree and retry policy, see Errors & retries.
The Python SDK speaks snake_case on the wire in both directions — request bodies and query parameters are snake_case, and responses are snake_case Pydantic models. Unlike the JavaScript SDK, it does no camelCase-to-snake_case translation. Keys such as
workflow_id, top_k, and cron_expression are passed through verbatim.Requirements
The SDK is built on
asyncio. All examples below run inside an async def entered with asyncio.run(...).
Install
modulex, not modulex-python:
import
Authenticate
Every request authenticates with two headers, set automatically by the client from your constructor arguments or environment:Authorization: Bearer <mx_live_*>— your API key as a Bearer token. This is the auth header. There is noX-Authorizationheader.X-Organization-ID: <org_id>— the organization the request runs in. It is sent only when an organization is resolved (constructor, environment, or per-request override).
The backend also accepts
X-API-KEY as an alternative to Authorization: Bearer, but the Python SDK never sends it. You can add arbitrary headers through default_headers, but the SDK will not let you override Authorization or Content-Type (see Header construction).Construct the client
Modulex is the single entry point. Only api_key is positional; everything else is keyword-only.
signature
str | None
default:"None"
Your
mx_live_* API key, sent as Authorization: Bearer <api_key>. Required via this argument or the MODULEX_API_KEY environment variable. If neither is set, the constructor raises ValueError immediately: api_key is required: pass api_key=... or set the MODULEX_API_KEY environment variable.str | None
default:"None"
The default organization context, sent as the
X-Organization-ID header. Falls back to the MODULEX_ORGANIZATION_ID environment variable. When unset, the header is omitted; org-scoped routes then return 400 with X-Organization-ID header is required. Every org-scoped method also accepts a per-request organization_id= keyword that overrides this default for that one call.str | None
default:"https://api.modulex.dev"
The API base URL. Falls back to the
MODULEX_BASE_URL environment variable, then to the default. A trailing slash is stripped. See Base URLs & versioning — ModuleX has no /v1 path segment.float
default:"30.0"
Per-request httpx timeout in seconds, applied on every call. No environment fallback (constructor-only). The read timeout is disabled for SSE streams.
int
default:"3"
Retry budget for transient failures on idempotent (
GET/HEAD) requests. No environment fallback. See Retries, timeouts & backoff and Errors & retries.dict[str, str] | None
default:"empty"
Extra headers merged into every request. They are merged before the auth and content-type headers, so they can override
User-Agent but cannot override Authorization or Content-Type. No environment fallback.Environment-variable fallbacks
The Python SDK reads three environment variables. This is a deliberate difference from the JavaScript SDK, which reads none and requires every value in the constructor. The resolution order is explicit argument → environment variable → default.
With the three environment variables set, the client takes no arguments at all:
env-only
Only
api_key, organization_id, and base_url have environment fallbacks. timeout, max_retries, and default_headers are constructor-only. Cross-reference this divergence on the JavaScript SDK page and the parity matrix.Async client lifecycle
The client owns a sharedhttpx.AsyncClient connection pool. Use it as an async context manager so the pool is closed for you on exit.
__aenter__ (returns the client) and __aexit__ (calls close()). close() awaits httpx.AsyncClient.aclose() on the shared pool. Construct the client once per process and reuse it across calls; do not create a new client per request.
Wire conventions and typed responses
snake_case both ways
Request bodies and query parameters are built as snake_case dictionaries and sent verbatim — there is no casing-translation layer. Responses are snake_case as well. Pass and read keys exactly as the API defines them:workflow_id, knowledge_base_id, top_k, cron_expression, make_default.
Pydantic v2 models with a dict-compatibility shim
Every response is a Pydantic v2 model. You can read fields with typed attribute access or legacy dict access on the same object:access
extra="allow", so unknown fields returned by a newer backend are preserved (forward-compatible) and reachable through the same shim. to_dict() serializes with aliases applied and None values dropped. The model base type ModulexModel and the pagination wrapper AsyncPage are importable from modulex.types; SSEEvent is importable from the modulex package root.
Header construction
Headers are built once per request. The base set, in merge order, is:headers
default_headersare merged before the auth and content-type keys, so they can overrideUser-Agentbut neverAuthorizationorContent-Type.X-Organization-IDis added only when an organization resolves. Precedence: per-requestorganization_id=argument → client default → omitted.Content-Typeis dropped onGET-style SSE streams (no body) and on multipart uploads (so httpx sets the multipart boundary).Idempotency-Keyis attached only when you passidempotency_key=to a mutating method (currentlyexecutions.run).
Resource groups
The client exposes 17 resource groups as lazy properties (instantiated on first access). Each maps to a backend router; methods are async coroutines unless noted as SSE/paginator factories. The full route-to-method mapping is in the parity matrix.The subscriptions resource is Python-only
client.subscriptions exists only in the Python SDK. The JavaScript SDK has no subscriptions resource, so these four methods have no JavaScript counterpart — to read plans or open a Stripe checkout from JavaScript you call the REST routes directly. See the parity matrix and Subscriptions & Stripe.
str
required
The target plan slug, for example
"pro" or "max". Preferred over the legacy plan_id. See Plans & pricing.str
required
The billing interval, for example
"month" or "year".str | None
default:"None"
Legacy plan identifier, deprecated in favor of
plan_slug.str | None
default:"None"
Per-request organization override; defaults to the client/environment organization.
checkout_link and customer_portal send their parameters as query parameters, not a JSON body. Both return a CheckoutResponse whose url field is the link to open in a browser. The catalog PlanPrice model exposes the field amount; the billing BillingPlanPrice model exposes the field price — they are two different shapes for two different routes.Run a workflow
executions.run triggers a run and returns a RunResponse carrying the run_id and thread_id. Provide exactly one of workflow_id (a saved workflow), workflow (an ad-hoc definition), or system_workflow (a named system workflow). To consume the live event stream, pass run_id to executions.listen — covered in Streaming & HITL and SSE run streaming.
str | None
default:"None"
Run a saved workflow by id. Provide exactly one of
workflow_id, workflow, or system_workflow.dict | None
default:"None"
Run an ad-hoc workflow definition without saving it.
str | None
default:"None"
Run a named system workflow.
dict | None
default:"None"
Run inputs, resolved by the engine’s
{{node_id.field}} reference model. See Variables & references.dict | None
default:"None"
Per-run configuration overrides.
bool
default:"true"
Always sent. When
true, the run streams events you consume with executions.listen.bool
default:"false"
Always sent. When
true, the run is not persisted to run history.bool
default:"false"
Always sent. Scopes the run’s chat to you rather than the organization.
str | None
default:"None"
Links an ad-hoc run to a saved workflow’s Runs panel.
str | None
default:"None"
Sent as the
Idempotency-Key header. Note the no-op caveat in Header construction.str | None
default:"None"
Per-request organization override.
object
402/403/429 DenialEnvelope described in Errors. Calling the run route requires the owner or admin role; the member role has been retired.
Pagination
List methods come in two forms: single-page methods that return a typed*ListResponse model, and two auto-paginating helpers that return an AsyncPage[Model] you iterate with async for. The two auto-paginators are workflows.list_all (page style) and executions.iter_runs (offset style). AsyncPage validates each item into its Pydantic model lazily as you iterate. For cursor-paginated lists (composer.list, assistant.list), loop on next_cursor yourself. See Pagination.
Retries, timeouts and backoff
The SDK retries automatically, but only conservatively. Mutating requests are never retried, so they cannot double-execute.
See Errors & retries for the idempotency caveats and Rate limiting for the
429 headers.
Errors and billing denials
Any response with status>= 400 is mapped to a typed exception. All SDK exceptions inherit from ModulexError, which carries message, status_code, response, and body.
On
402/403/429, if the response body carries the flat denial envelope {code, layer, key, current, limit, reason} (top-level, under detail, or a bare {reason}), the SDK raises a BillingError subclass keyed by layer:
layer="credit"→CreditExhaustedError(402)layer="wallet"→WalletError(402)layer="quota"→QuotaExceededError(403)- otherwise (including
layer="rate") → baseBillingError
DenialEnvelope the usage gate returns on the run, composer, assistant, and managed-knowledge surfaces. A header-based 429 (the active rate-limit path) raises RateLimitError instead, so handle both 429 shapes. The unified envelope reference is on the Errors page.
error-handling
Streaming and human-in-the-loop
SSE methods (executions.listen, workflows.listen_changes, composer.listen, assistant.listen, chats.stream, credentials.bulk_modulex_keys_stream) return an EventSourceStream you use as an async iterator and async context manager. Each yielded SSEEvent exposes event, data, id, retry, and is_terminal. Terminal event types (done, error, cancelled, interrupted) stop iteration; heartbeats are filtered unless you opt in. An HTTP error on connect (for example a 404, billing denial, or rate limit) raises the same typed exception as a REST call rather than an opaque stream error.
For the full event taxonomy, resume contract, and worked HITL examples, see Streaming & HITL, SSE run streaming, and HITL resume.
Next steps
SDKs overview
How both SDKs map onto the REST surface, installed once and used across every operation.
SDK parity matrix
Route-to-method coverage and every cross-SDK naming difference, including the Python-only subscriptions resource.
Streaming & HITL
Consume SSE run streams and answer human-in-the-loop prompts.
Errors & retries
The exception tree, retry policy, and idempotency behavior in depth.