Skip to main content
Besides video and audio, a model and its clients talk to each other with structured data. A client sends a command to the model. The model answers, and it can also send a message to the client at any time: a status, a result, an event the client should react to. Both directions are declared in your class. @event marks a method as a command handler, and a ModelMessage subclass declares a message. The runtime turns those declarations into the model’s schema, which is what typed client SDKs are generated from. The examples on this page come from the runtime’s Waypoint example, a world model that starts from an uploaded image.

Defining a command

The state gives you one set_<field> command per field, for values a client sets and the model keeps reading. For anything else, write a handler with @event. The method’s parameters are the data the client sends:
waypoint.py
The client calls it as set_image with an image parameter. The runtime validates the payload against the signature before your method runs, and it runs between steps, so the state is never changed while generate() is reading it. A handler can be async def or a plain def. A command does not need parameters. This one restarts the world from the image the model already has:
waypoint.py

Constraining a parameter

InputField on a parameter carries the same rules as on a state field, and the runtime checks them before your handler runs:
A value outside the rules is refused with invalid_command and your handler is not called. The constraints also travel into the schema, so a client sees the accepted range before it sends anything.

Replying to a command

A client that sends a command usually wants to know what happened. Return a ModelMessage from the handler and the runtime delivers it to that client as the reply to that exact command. A client awaiting the call receives it as the result:
waypoint.py
Return None when there is nothing to say. Reply when the client should confirm what took effect: a clamped value, a resolved default, the state after a change. The return annotation has to be one ModelMessage subclass or None. A union such as WaypointStatus | None is rejected when the class is declared, because the schema publishes one reply shape per command.

Reporting a failure

When a command cannot be carried out, raise CommandError with a code and a message. Every model refuses a command this way:
waypoint.py
The code is a short snake_case token, and clients branch on it. Keep each code the same from one release to the next. Name each code in the command’s description, so the schema tells a client what can fail. The message crosses the wire unchanged. Write it for the client, not for a log. A raise has three effects:
  • The caller learns on its own call. The runtime answers the command’s request with a failure that carries the code and the message. In Python, send_command() raises a ReactorError. In JavaScript, sendCommand() resolves undefined and reports the failure on the error event.
  • Only the caller gets the failure. The other clients in the session do not see a refusal of a command they did not send.
  • The runtime records that the command failed. Some sessions start with a step count. In such a session, a raise from a starting command ends the session and fails the steps that remain.

Never send an error and return

Do not refuse a command with await self.send(...) followed by a bare return. The runtime then sees a success and answers the caller with an empty acknowledgement. A JavaScript caller resolves exactly as it does on success. Every other client receives a message about a command it did not send. A session started with a step count keeps running with nothing to do. Do not return a different message type to report a failure either. The return annotation is the reply the schema publishes, and a client reads that type’s fields from whatever you return. If your published schema already declares an error message that clients listen for, keep sending it just before the raise until your next major release. Removing a message breaks the clients that listen for it.

Other exceptions

The runtime treats any other exception as a bug. It logs the exception with its traceback and answers the client with internal_error and a fixed message. Paths, queries, or credentials in the exception text never leave the container. When a check raises its own exception, such as a ValueError from a decoder, catch it and raise a CommandError with your own code:
The runtime uses the same channel for its own refusals: invalid_command when a payload fails validation, and unresolved_upload when a command references a file that cannot be fetched.

Which clients receive the failure

Every SDK Reactor publishes receives a raised failure: JavaScript 3.x, Python, C++, Swift, and Java. The retired JavaScript SDK 2.x (@reactor-team/js-sdk before 3.0) does not. Its commands carry no request id, so the runtime cannot connect a failure to them.

Sending messages

A ModelMessage subclass declares a typed message. Send one with self.send() from anywhere in your class, and every connected client receives it:
waypoint.py
The client receives {"type": "waypoint_status", "data": {...}}. The wire type is the snake_case form of the class name. MessageField is optional; it attaches a description or a default that shows up in the schema, and the class docstring documents the message itself. A message with no fields is valid and is the right shape for a pure signal. A message sent from process_output() goes on the wire before that step’s frames, so a client that reads the status and then sees the frame gets them in that order.

Messages to one client

self.send() goes to everyone. To reach the client a handler or hook is running for, add a client: ClientInfo parameter. The runtime fills it in, and client.send() delivers to that client alone:
waypoint.py
This is how a client that joins a running session gets a snapshot without every other client receiving it too. Sessions & Clients covers the client handle and serving several clients at once.

File uploads

To receive a file with a command, annotate a parameter as UploadedFile. The runtime fetches the upload and hands your handler the bytes:
UploadedFile carries name, mime_type, data (the raw bytes), and size. Check mime_type before decoding, because a client can upload anything. A parameter can also be a list[UploadedFile], and an upload nested inside a dict or a dataclass field resolves the same way. A client can also upload a file on its own, outside any command. Declare a @file_uploaded hook to receive those:
The handler takes one parameter named uploaded_file, plus the optional client. Prefer putting a file on the command it belongs to, so the schema tells the client what the file is for. Use @file_uploaded for a drop zone or a client that uploads first and decides later.

Next

Sessions & Clients

Session and connection hooks, the client handle, and serving several clients.

Managing State

The state object and the commands it generates.