This section explains what happens after you emit an event.
Developers often understand how to declare a slot and how to call emit(...), but the most important operational details live in the dispatcher behavior:
- how listeners are resolved
- what arguments listeners receive
- what happens if the payload does not match the slot contract
- what happens if no listeners exist
- what happens when a listener raises an error
Understanding these details makes the difference between "I can fire an event" and "I can design a reliable module extension point".
Listener Registration¶
Listeners are registered through @event_listener(...), not through the Events facade.
Example:
@event_listener("auth.login.succeeded", priority=10)
async def on_login(user_id=None, role=None, session=None, module_sdk=None):
...The important rule is that listeners should target the fully qualified event name.
The base registration helper rejects unqualified names. Scoped decorator binding can qualify names in some module-authoring paths, but for public SDK documentation the safe rule is still:
- producers may work with relative names
- listeners should subscribe to the fully qualified key
How The Dispatcher Finds Listeners¶
When module_sdk.events.emit(...) forwards the event to the runtime dispatcher, the dispatcher:
- reads the event definition, if one exists
- loads the registered listeners for that exact event key
- builds an SDK instance for each listener’s module
- invokes each listener in registration order
- collects all listener return values into a list
Each listener runs in the context of its own module SDK, not the producer module’s SDK.
That point is easy to miss, but it is extremely important.
If auth emits an event and analytics listens to it, the listener receives an SDK bound to analytics, not to auth.
That is exactly what you want, because the listener should operate as its own module, using its own models, effects, and helpers.
What Parameters A Listener Can Receive¶
The dispatcher inspects the listener function signature and fills parameters by name.
The supported patterns are:
payloadreceives the full payload dictsessionreceives the event sessionsdk,module_sdk, ordemocrai.sdkreceives the listener module SDKevent_namereceives the fully qualified event key- any parameter whose name matches a payload key receives that payload value
That means all of these are valid styles:
@event_listener("auth.login.succeeded")
async def listener(payload=None, module_sdk=None):
...@event_listener("auth.login.succeeded")
async def listener(user_id=None, role=None, session=None):
...@event_listener("auth.login.succeeded")
async def listener(event_name=None, module_sdk=None, organization_id=None):
...Why This Matters¶
Listeners do not need rigid boilerplate signatures. They can accept only the fields they care about.
That keeps listeners small and focused.
Payload Validation Against Slots¶
If an emitted event has a declared slot, the dispatcher compares the payload against the declared params.
It warns when:
- declared params are missing from the emitted payload
- extra undeclared params are present in the emitted payload
These are warnings, not hard failures.
That behavior is deliberate: the runtime wants to surface contract drift without automatically crashing the producer flow.
What This Means For Module Authors¶
You should still treat slot params as the real contract.
If the dispatcher warns that your event payload does not match the slot declaration, that is usually a documentation or contract problem that should be fixed, not ignored indefinitely.
What Happens If No Slot Was Declared¶
If an event is emitted without a declared slot, the dispatcher logs a warning that the event was emitted without a declared public module API.
The event can still be delivered to listeners, but the platform is telling you that the event contract is under-specified.
In practical terms:
- undeclared events can work
- declared events are much easier to maintain
If an event is meant to be a stable extension point, declare the slot.
What Happens If No Listeners Exist¶
If no listeners are registered for the emitted event:
- the dispatcher returns an empty result list
- it logs a warning if the event was expected to have listeners
- it does not warn when the slot is marked
optional=True
This is why the optional flag on @event_slot(...) matters.
If your module is exposing an event as a hook that other modules may or may not implement, mark it optional.
If your architecture assumes that someone should always be listening, mark it non-optional so missing listeners become visible through warnings.
What Happens If A Listener Raises¶
If a listener throws an exception, the dispatcher logs an error and returns None for that listener result instead of crashing the whole emission loop.
That is an important operational behavior:
- one broken listener does not prevent the dispatcher from continuing through the rest of the listeners
- producer code does not automatically fail just because one subscriber broke
Why This Tradeoff Exists¶
The event system is designed for decoupled extensibility. In that kind of architecture, one subscriber should not usually be able to break the entire producer flow.
The cost of that design is that listener failures can become easier to miss if you are not watching logs or testing the flow carefully.
So the practical rule is:
- rely on the dispatcher to isolate listener failures
- still treat listener errors as real bugs that need to be fixed
Return Value Semantics¶
The dispatcher collects one result entry per listener execution.
That means the result from emit(...) is a list in listener order. Entries may be:
- concrete listener return values
Nonefor listeners that returned nothingNonefor listeners that failed and were swallowed after logging
This makes the result useful for tests, because you can assert that the expected listeners ran and inspect what they returned.
Practical Design Guidance¶
When you design a new event contract:
- declare a slot
- choose a stable, fully qualified business-oriented event name
- keep the payload compact and explicit
- mark the slot optional only when the architecture really supports "zero listeners"
- make listeners idempotent where possible
- do not treat event emission like synchronous RPC
That last point is especially important.
Events are best when they represent decoupled reactions to completed facts. If your producer strictly requires one specific listener to succeed, you may not actually want an event contract. You may want a direct call or another coordination mechanism.