@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 oneset_<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
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:
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 aModelMessage 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
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, raiseCommandError with a code and a message. Every model
refuses a command this way:
waypoint.py
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 aReactorError. In JavaScript,sendCommand()resolvesundefinedand reports the failure on theerrorevent. - 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 withawait 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 withinternal_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:
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
AModelMessage subclass declares a typed message. Send one with self.send() from anywhere in
your class, and every connected client receives it:
waypoint.py
{"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
File uploads
To receive a file with a command, annotate a parameter asUploadedFile. 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:
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.