On this page

Module communication

Modules never import each other. They communicate through two declared mechanisms and order themselves through one reserved topic.

Declaring the interface#

manifest.yaml
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#

Emitting and reacting
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#

Calling a capability, optionally
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#

Running after another module
@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>.completed when 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.
NeedMechanism
"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.