Skip to main content
The Python SDK (PyPI package 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

The import name is modulex, not modulex-python:
import
ModuleX install extras such as [all] or per-integration extras are currently empty. Installing them emits a pip warning and pulls in only the core package, so omit extras until they ship. Integrations are loaded server-side and do not require client-side extras. See Installing & using integrations and Known limitations.

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 no X-Authorization header.
  • 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).
Create an API key in the app under settings, then construct the client with it. The same auth scheme applies to cURL and the JavaScript SDK — see API authentication and the auth model for the full picture.
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 shared httpx.AsyncClient connection pool. Use it as an async context manager so the pool is closed for you on exit.
The context manager methods are __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
Models are configured with 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_headers are merged before the auth and content-type keys, so they can override User-Agent but never Authorization or Content-Type.
  • X-Organization-ID is added only when an organization resolves. Precedence: per-request organization_id= argument → client default → omitted.
  • Content-Type is dropped on GET-style SSE streams (no body) and on multipart uploads (so httpx sets the multipart boundary).
  • Idempotency-Key is attached only when you pass idempotency_key= to a mutating method (currently executions.run).
Idempotency-Key is not a run-deduplication mechanism. The backend run route assigns its own run identifier, so a caller-supplied key does not de-duplicate runs. It is also not used by the automatic retry path, which only retries GET/HEAD. See Errors & retries.

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
Running a workflow goes through the live billing gate, so this operation can return the 402/403/429 DenialEnvelope described in Errors. Calling the run route requires the owner or admin role; the member role has been retired.
executions.get_run(run_pk) takes the id field on a list_runs item — not the run_id field that executions.run returns. Both fields appear on the same list item, so it is easy to confuse them. The run_id is for streaming, status, and resume; the id is for history lookups. See Workflows & runs.

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") → base BillingError
This is the same 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.
PermissionError and TimeoutError shadow the Python builtins of the same name. After from modulex import TimeoutError, an except TimeoutError clause catches the ModuleX exception, not asyncio.TimeoutError. Import them explicitly and be deliberate about which one a handler should catch.
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.