Skip to main content
Communication runs both ways. Clients send commands, declared with @event. Your model sends messages, declared as ModelMessage subclasses. Both are typed, and both end up in the schema that generates your clients’ SDKs.

Commands

@event marks a method as the handler for a named command. The method’s parameters become the command’s payload:
The client sends {"type": "set_style", "data": {"style": "anime"}} and the runtime validates the payload against the signature before calling you. Handlers can be async def or plain def. A command taking no parameters is fine, and useful for actions rather than settings:

Constraining a parameter

InputField carries the constraints the runtime checks before your handler runs:
A value outside the constraint is rejected, and the client is answered with invalid_command. The runtime checks your defaults too, when the class is declared, so a default that violates its own constraint fails at startup rather than on the first request. Constraints also travel into the schema, so a generated SDK shows a client the range before it sends anything.

Replying to a command

A handler that returns a ModelMessage sends it back as that command’s correlated reply, so a client awaiting the command resolves with the result:
Return None when the client needs no answer. Use a reply when the client should confirm what actually took effect — clamped values, resolved defaults, or the full state after a change. The return annotation must be a single ModelMessage subclass or None. A union — including StyleChanged | None — is rejected when the class is declared, because the schema publishes one response shape and a client generated from it would expect no body and receive one.

Reporting a failure

Raise CommandError to answer a command with a failure the client can act on. The code and message cross the wire unchanged, correlated with the command, so an awaiting caller rejects with a reason instead of waiting for a reply that never comes:
Write the message for the client, not for a log. Any other exception is a fault the client cannot act on: the runtime logs it with its traceback and answers with a generic internal_error, keeping the detail — which can name paths, queries, or credentials — out of the reply. The runtime sends a few codes of its own on the same channel: invalid_command when a payload fails the model’s contract and no handler runs, and unresolved_upload when a command references an upload that cannot be fetched.

Outbound messages

A ModelMessage subclass declares a typed payload your model can send at any time. Fields are declared like a dataclass:
The client receives {"type": "progress", "data": {"step": 3, "total": 100}}. The wire type is the snake_case form of the class name. MessageField is optional — it exists to attach a default or a description that shows up in the generated schema. The class docstring documents the message itself. Send one with self.send():
A message with no fields is valid and is the right shape for a pure signal, like GenerationComplete.

Per-client messages

self.send() broadcasts to every client in the session. To reach one client, take a client: ClientInfo parameter and use client.send(). The runtime injects the client that triggered the handler, so it never appears in your schema.
client is a handle you can keep. Store it in @connected and message that client later from anywhere:
For a single-client model, self.send() covers everything. Reach for client.send() when a session has an audience and the message is meant for one of them.

File uploads

To receive a binary file with a command, annotate a parameter as UploadedFile. The runtime resolves the upload and hands your handler the bytes:
UploadedFile carries name, mime_type, data (the raw bytes), and size (their length). Check mime_type before decoding — a client can upload anything. A file that arrives with a command reaches that handler alone. The runtime resolves it first, so a handler runs with the bytes in hand, and an upload it cannot fetch answers the client with an unresolved_upload failure.

Uploads outside a command

A client can also send a file on its own. @file_uploaded receives those:
The handler takes exactly one parameter named uploaded_file, plus the optional client. The runtime fetches an upload’s bytes only when the model declares this hook, so it is what turns a bare notification into a file. Reach for it when a file has no natural command to ride along with — a drag-and-drop surface, or a client that uploads first and decides what to do with the result later. When the file belongs to an action, put it on that action’s @event instead, so the schema shows a client what the file is for.

Putting it together

From the client’s side, this model:
  • accepts set_prompt and replies with the applied prompt_changed,
  • accepts get_prompt and answers the asking client alone,
  • broadcasts a progress message on every frame.

Next

Managing State

One event loop, shared by your loop and your handlers.

Media Input

Read the client’s camera and microphone.