Module communication
Modules never import each other. They communicate through two declared mechanisms and order themselves through one reserved topic.
Declaring the interface#
provides: [my_module.metrics, my_module.analysis.completed]
consumes: [log.imported, discovery.completed]
optional_modules:
- id: performance
reason: Uses cycle times when performance is installed.
The platform checks these at startup, uses them to compute availability, and builds the precompute closure from them.
Event bus — fire-and-forget#
await ctx.bus.emit(
"my_module.analysis.completed",
{"user_id": ctx.user_id, "log_id": ctx.log_id, "summary": summary},
)
@on_event("my_module.analysis.completed")
async def react(self, ctx: ModuleContext, payload: dict) -> None:
ctx.logger.info("upstream_finished", summary=payload["summary"])
Include user_id on every user-scoped event; keep payloads small and JSON-serialisable; never rely on ordering between topics.
Capability registry — request/response#
if ctx.registry.has("performance.kpis"):
kpis = await ctx.registry.call("performance.kpis", log_id=ctx.log_id)
else:
ctx.logger.warning("performance_missing", fallback="computing locally")
kpis = await self._fallback_kpis(ctx)
Capabilities you publish go in provides; capabilities you call must be in consumes (hard) or optional_modules (soft). The platform refuses to mount a module that calls an undeclared capability. Guard optional calls with has(), and declare the dependency so the card reads Limited instead of failing.
Ordering#
@on_event("discovery.completed")
@job(progress=True, title="My overlay")
async def overlay(self, ctx: ModuleContext, payload: dict) -> None:
# discovery's precompute succeeded, so its cache entries are present
...
- The platform emits
<module_id>.completedwhen a module's precompute job succeeds; a failure or cancellation emits nothing. - Opening a dependent first runs its producers, then its own job. If a producer fails or is cancelled the dependent shows that error with a Retry instead of waiting forever. A module that only requires one (
requirements.modules) is gated on it the same way. - Steps in an import's plan are ordered topologically, then alphabetically by module id within a layer — deterministic enough to describe in progress copy.
| Need | Mechanism |
|---|---|
| "Something happened, react if you care" | Bus topic. |
| "I need a value from another module" | Capability call. |
| "Run after module X" | @on_event("X.completed") + @job. |
| "Reuse another module's widget" | useWidget(moduleId, widgetId) in the frontend. |