Runtime API Reference
The pyxle.runtime module provides decorators and error classes used in .pyxl files. The compiler auto-injects the ones you use in the idiomatic patterns, so you rarely import from pyxle.runtime at all:
- a file with a
@serverloader getsserverandLoaderError; - a file with an
@actiongetsaction,ActionError,ValidationActionError, andinvalidate_routes.
So raise LoaderError(...) / raise ActionError(...) work without an import. You can still import any of them explicitly (a name you define or import yourself always takes precedence over the auto-injection). pyxle check reports any name you genuinely left undefined.
@server
Marks an async function as a page data loader.
@server
async def load_page(request):
return {"key": "value"}Behaviour:
- Attaches
__pyxle_loader__ = Trueto the function (no wrapping) - The function must be
async(enforced at compile time) - Receives a Starlette
Requestobject - Must return a JSON-serializable
dict - Can return a
(dict, int)tuple to set the HTTP status code - Only one
@serverfunction is allowed per.pyxlfile
Request object properties:
| Property | Type | Description |
|---|---|---|
request.path_params |
dict |
URL path parameters from dynamic routes |
request.query_params |
QueryParams |
URL query string parameters |
request.headers |
Headers |
HTTP request headers |
request.cookies |
dict |
Request cookies |
request.url |
URL |
Full request URL |
request.method |
str |
HTTP method |
request.state |
State |
Mutable state for middleware to attach data |
request.state.request_id |
str |
The request's correlation id (also the X-Request-Id response header), unless observability is disabled |
@action
Marks an async function as a server action callable from React components.
@action
async def create_item(request):
body = await request.json()
return {"id": 1, "name": body["name"]}Behaviour:
- Attaches
__pyxle_action__ = Trueto the function (no wrapping) - The function must be
async - Receives a Starlette
Requestobject - Must return a JSON-serializable
dict - Multiple
@actionfunctions are allowed per.pyxlfile - Accessible via
POST /api/__actions/{page_path}/{action_name} - Protected by CSRF middleware by default
Validated body parameter (optional):
Annotate a parameter (after request) with a Pydantic BaseModel and Pyxle parses and validates the request body before the action runs, passing the typed model in:
from pydantic import BaseModel
class NewItem(BaseModel):
name: str
@action
async def create_item(request, body: NewItem):
return {"id": 1, "name": body.name}Requires the [pydantic] extra (pip install "pyxle-framework[pydantic]"). On a validation failure the action is not called and the client gets a 422 with a fields map. See Validating request bodies for the full pattern and the pyxle openapi command.
Client response format:
// Success
{ "ok": true, "id": 1, "name": "Item" }
// Error (from ActionError)
{ "ok": false, "error": "Error message" }
// Validation error (422, from a Pydantic body or ValidationActionError)
{ "ok": false, "error": "Validation failed", "fields": { "name": ["Field required"] } }LoaderError
Exception class for structured loader errors. Triggers the nearest error.pyxl boundary. Auto-injected in any file with a @server loader — no import needed.
@server
async def load_page(request):
raise LoaderError("Not found", status_code=404, data={"id": 42})Constructor:
LoaderError(message: str, status_code: int = 500, data: dict | None = None)| Parameter | Type | Default | Description |
|---|---|---|---|
message |
str |
(required) | Error message displayed in the error boundary |
status_code |
int |
500 |
HTTP response status code |
data |
dict | None |
None |
Additional JSON-serializable context |
Properties:
| Property | Type | Description |
|---|---|---|
.message |
str |
The error message |
.status_code |
int |
HTTP status code |
.data |
dict |
Additional context (empty dict if None was passed) |
ActionError
Exception class for structured action errors. Returns a JSON error response to the client.
@action
async def update_item(request):
raise ActionError("Validation failed", status_code=400, data={"field": "name"})Auto-imported since 0.3.0. Any
.pyxlfile that declares at least one@actiongetsfrom pyxle.runtime import ActionErrorinjected by the compiler, soraise ActionError(...)works without you remembering to import it. A user-definedActionErrorclass (or an existing import) is respected and takes precedence.
Constructor:
ActionError(message: str, status_code: int = 400, data: dict | None = None, *, fields: dict[str, list[str]] | None = None)| Parameter | Type | Default | Description |
|---|---|---|---|
message |
str |
(required) | Error message sent to the client |
status_code |
int |
400 |
HTTP response status code |
data |
dict | None |
None |
Additional JSON-serializable payload |
fields |
dict[str, list[str]] | None |
None |
Per-field validation messages (field path → messages), surfaced to the client as fields. Keyword-only |
Properties:
| Property | Type | Description |
|---|---|---|
.message |
str |
The error message |
.status_code |
int |
HTTP status code |
.data |
dict |
Additional data (empty dict if None was passed) |
.fields |
dict[str, list[str]] |
Per-field messages (empty dict if None was passed) |
ValidationActionError
A subclass of ActionError for request-body validation failures. Defaults to HTTP 422 and always carries fields. Pyxle raises it automatically when a Pydantic-typed @action body fails validation; raise it yourself for checks Pydantic can't express (uniqueness, cross-field rules):
from pyxle.runtime import ValidationActionError
@action
async def register(request, body: Signup):
if await db.email_taken(body.email):
raise ValidationActionError(fields={"email": ["That email is already registered."]})
...Auto-imported. Like
ActionError, any.pyxlfile that declares at least one@actiongetsValidationActionErrorinjected by the compiler. A user-defined class or existing import takes precedence.
Constructor:
ValidationActionError(message: str = "Validation failed", *, fields: dict[str, list[str]], status_code: int = 422, data: dict | None = None)| Parameter | Type | Default | Description |
|---|---|---|---|
message |
str |
"Validation failed" |
Top-level error message |
fields |
dict[str, list[str]] |
(required) | Field path → list of messages. Keyword-only |
status_code |
int |
422 |
HTTP response status code |
data |
dict | None |
None |
Additional JSON-serializable payload |
The client receives { "ok": false, "error": ..., "fields": { ... } }. The useAction hook exposes the map as result.fields; <Form> passes it as the second argument to onError. See Validating request bodies.
invalidate_routes(response, *urls)
Tell the client router to evict its cached navigation payloads for urls before the caller's next navigation. Used by @action handlers that mutate data the client has already cached elsewhere.
from pyxle.runtime import invalidate_routes
@action
async def delete_post(request):
...
# Drop the client's cached /posts so its next navigate() refetches.
return invalidate_routes({"ok": True}, "/posts", "/dashboard")The helper accepts either a plain dict (the usual @action return shape) or a Starlette Response / JSONResponse. In both cases Pyxle attaches an x-pyxle-invalidate: /posts, /dashboard header to the outgoing response. useAction and <Form> read this header automatically and call the client-side invalidate(url) for each listed URL — so mutation-driven cache invalidation works end-to-end with no extra wiring on the caller.
Parameters:
| Parameter | Type | Description |
|---|---|---|
response |
dict | Response |
The action's return value. Modified in place and returned. |
*urls |
str |
URLs to invalidate. Empty / duplicate values are filtered. |
Returns the response argument (possibly with a header or sentinel key added) so you can return invalidate_routes(...) in one line.
Background tasks — pyxle.tasks
Fire-and-forget background work that runs in-process on the app's event loop. See the Background Tasks guide.
enqueue(func, *args, **kwargs)
Schedule func(*args, **kwargs) on the app's bounded worker pool and return
immediately. Coroutine functions are awaited; plain callables run in a thread.
Usable from any loader or action while the app is running.
from pyxle.tasks import enqueue
@server
async def load_article(request):
enqueue(record_view, request.path_params["slug"]) # returns now
return {"article": ...}| Raises | When |
|---|---|
TaskQueueNotRunning |
Called outside a running Pyxle app (no active queue) |
TaskQueueFull |
The bounded queue is at capacity (sustained overload) |
A task that raises is logged (pyxle.tasks logger) and never takes a worker
down. The queue is per-process — under pyxle serve --workers N each worker
has its own, and queued work is lost on restart. For durable or cross-worker
jobs, use a real job queue (Celery / ARQ / Dramatiq) — see the guide.
Post-response tasks in an @action
Inside an @action, request.state.background is a Starlette
BackgroundTasks; add_task(func, *args)
runs after the response is sent. Returning {"background": [fn, *args]} from the
action is shorthand for a single such task.
WebSockets — pyxle.realtime
A page that exports a module-scope async def websocket(ws) also serves a
WebSocket at its path. pyxle.realtime provides the pub/sub and auth helpers
those handlers use. See the WebSockets guide for the
full walkthrough.
channel(ws, name, *, broker=None)
An async context manager that subscribes ws to a named channel for the life of
the block (unsubscribing on exit, including disconnect) and yields a handle with
.publish(message):
from pyxle.realtime import channel
async def websocket(ws):
await ws.accept()
async with channel(ws, f"room:{ws.path_params['room']}") as room:
async for message in ws.iter_text():
await room.publish(message) # reaches every subscriberA published dict/list is sent as a JSON frame, str as text, bytes as
binary. The broker defaults to the app-scoped InProcessBroker on
app.state.pyxle_broker (one per process — see the guide's multi-worker
caveat). Implement the Broker protocol (subscribe / unsubscribe /
publish) for a Redis/NATS backend.
authenticate_websocket(ws)
Resolve the signed-in pyxle-auth user for a
WebSocket upgrade, or None. The auth middleware never runs for WebSocket scope,
so a handler that needs the user must call this (it reads the session cookie off
the handshake). Returns None — zero database work — when the plugin isn't
installed or no cookie is present.
from pyxle.realtime import authenticate_websocket
async def websocket(ws):
user = await authenticate_websocket(ws)
if user is None:
await ws.close(code=4401)
return
await ws.accept()
...origin_allowed(ws, allowed_origins)
Whether the upgrade's Origin is permitted. CSRF doesn't apply to a WebSocket,
so checking the origin is the equivalent guard. An empty allowed_origins
allows all; a missing Origin header (same-origin / non-browser) is allowed.
AI accessibility hooks
Conventions Pyxle recognizes when the llms feature is enabled, so your app can serve a Markdown rendition of each page. None are imported — they're functions you define by name in your .pyxl server sections or in llms.py files. See the AI accessibility guide for the full model.
to_markdown(ctx)
Returns the Markdown for a page. Define it in a page's server section (scopes to that page) or in an llms.py file (scopes to that directory's subtree; nearest ancestor wins). Sync or async.
def to_markdown(ctx): # ctx: MarkdownContext
return f"# {title}\n\n{body}\n" # str -> served
# return None # -> decline, fall through to the next sourcellms_txt(ctx)
Generates /llms.txt. Define it in the root pages/llms.py. Sync or async. Return a string, or None to use the generated default. ctx is an LlmsTxtContext.
wrap_markdown(ctx, markdown)
Frames every resolved .md response with a header/footer. Define it in the root pages/llms.py. Sync or async. Return the wrapped string, or None to leave markdown unchanged. ctx is a MarkdownContext.
Context objects
MarkdownContext — passed to to_markdown and wrap_markdown:
| Member | Type | Description |
|---|---|---|
ctx.request |
starlette.requests.Request |
The request. ctx.request.path_params holds route params (e.g. slug). |
ctx.path |
str |
Canonical page path, always without .md (e.g. /docs/routing, /). Prefer this over request.url.path. |
await ctx.run_loader() |
Any |
Runs only the page's @server loader and returns its data (no render). {} if the page has no loader. |
await ctx.render_html() |
str |
Renders the page (loader + SSR) and returns the body HTML. Lazy; only call if you need it. |
LlmsTxtContext — passed to llms_txt:
| Member | Type | Description |
|---|---|---|
ctx.request |
Request |
The request. |
ctx.pages |
tuple[LlmsPageInfo, ...] |
The app's concrete pages. |
ctx.render_default() |
str |
The framework's generated /llms.txt. |
LlmsPageInfo — each entry in ctx.pages: path (e.g. /about), md_url (/about.md), title (humanized label).
Document <head> elements
Pyxle offers two ways to contribute elements to the document <head>: the <Head> component (recommended) and the HEAD Python variable (lower-level alternative). Both are merged with automatic deduplication.
<Head> component (recommended)
Imported from pyxle/client, usable anywhere in your JSX tree:
import { Head } from 'pyxle/client';
export default function Page({ data }) {
return (
<>
<Head>
<title>{data.title}</title>
<meta name="description" content={data.description} />
<link rel="canonical" href={data.canonicalUrl} />
</Head>
{/* ... */}
</>
);
}See Head Management for the full pattern (layouts, deduplication, reusable SEO components, etc.).
HEAD variable (lower-level alternative)
Not part of pyxle.runtime but available in every .pyxl file. The HEAD variable is extracted at compile time by the parser; use it for fully-static head metadata that doesn't need React context, or for pages without a JSX component.
Static form:
# String
HEAD = '<title>Page Title</title>'
# List of strings
HEAD = ['<title>Page Title</title>', '<meta name="description" content="..." />']Dynamic form (callable):
def HEAD(data):
return f'<title>{data["title"]}</title>'The callable receives the loader's return value. Must return a string or list of strings synchronously. For most dynamic head content, the <Head> component with normal JSX interpolation is simpler and more flexible.
Explicit imports
The compiler auto-injects the names you use in the idiomatic patterns (see the top of this page), so you usually import nothing. You can still import any of them explicitly — useful for type checking and IDE support, and a name you import or define yourself always takes precedence over the auto-injection:
from pyxle.runtime import server, action, LoaderError, ActionError, ValidationActionError, invalidate_routes