Register hook handlers¶
Hooks are synchronous extension points: when a caller executes a hook by name, all registered handlers run in priority order inside the caller's task and return their results. This guide covers every way to register a handler and control its behavior.
Register a handler with the decorator¶
-
Import
hookfromuxok. -
Decorate a method on your
Pluginsubclass with@hook(name).class DataPlugin(Plugin): @hook("data.process") async def process(self, data: dict) -> dict: return {"processed": True, **data}The framework discovers
processat instantiation time and registers it with the hook system when the plugin starts. You do not callregister_hookyourself. -
Register the plugin with the core as usual.
Hook names are global. No prefix is added. Use dot-separated namespaces like "data.process" or "user.validate" to avoid collisions across plugins.
Set execution priority¶
-
Pass
priorityto@hook. Higher values run first; the default is0.class ValidationPlugin(Plugin): @hook("data.validate", priority=100) async def check_schema(self, data: dict) -> bool: return "id" in data @hook("data.validate", priority=50) async def check_business_rules(self, data: dict) -> bool: return data.get("status") in {"active", "pending"} @hook("data.validate", priority=10) async def check_permissions(self, data: dict) -> bool: return data.get("role") != "guest"With these registrations,
check_schemaruns first, thencheck_business_rules, thencheck_permissions. -
Use negative priority for fallback handlers — handlers that should run only when nothing else has handled the hook.
Use firstresult mode¶
-
Call the hook with
firstresult=Truefrom inside a plugin to stop execution at the first handler that returns a non-Nonevalue.The remaining handlers are skipped. This is the standard pattern for pluggable lookup chains where one provider is sufficient.
-
A handler that wants to be skipped returns
Noneexplicitly.
Implement conditional logic inside the handler¶
@hook accepts only name and priority. Conditional behavior belongs inside the handler body.
class ProcessingPlugin(Plugin):
@hook("data.process")
async def process_active(self, data: dict) -> dict | None:
if not data.get("active", True):
return None # Skip: signal no result from this handler
return {"processed": True, **data}
Returning None from a handler always means "no result from this handler." Callers collecting all results receive None in the list at that position; callers using firstresult=True skip it and continue to the next handler.
Register a handler dynamically¶
-
Define the handler as a regular method (no decorator).
-
Call
self.register_hookinsideon_start.class DynamicPlugin(Plugin): async def on_start(self) -> None: await self.register_hook("data.process", self._my_handler, priority=5) async def _my_handler(self, data: dict) -> dict: return {"dynamic": True, **data}register_hookis the method@hookdesugars to. Both paths bind the handler to this plugin instance: the kernel drains all of them automatically when the plugin stops or is reloaded. -
Register closures the same way when you need to capture local state.
Declare hooks consumed in metadata¶
Handlers registered on a hook name are the consuming side of a contract. Declare which hook names your plugin consumes so introspection tools and human readers know the dependency.
class ConsumerPlugin(Plugin):
def __init__(self) -> None:
super().__init__(
hooks_consumed={"data.process", "data.validate"},
)
@hook("data.process")
async def handle_process(self, data: dict) -> dict:
return {**data, "handled": True}
hooks_consumed is metadata only — it does not affect registration or execution.
See the hook system explanation for the execution model, priority semantics, and the firstresult contract in depth.