Events
Modules communicate through a versioned event bus rather than calling each other directly. Your module declares what it publishes and subscribes to in the manifest; the platform validates those declarations before you ship.
Declare in the manifest
"events": {
"publishes": [
{ "name": "cafm.work-order.created", "version": "1.0.0" }
],
"subscribes": [
{ "name": "building.alarm.triggered", "version": "1.0.0" }
]
}Where events come from
Events have two sources, and which one produces the event you consume determines the tool you reach for below:
- Platform events (
building.*) are published by the platform itself. They are versioned together with the SDK in its catalog snapshot (tv-events.snapshot.json) — the full list with payload schemas is Platform events. - Module events (
cafm.*,service-desk.*, …) are published by individual modules. By design there is no central catalog of them: a module's published events are declared in its manifest (events.publishes) and nowhere else, so there is no registry you have to get your own event added to. The module catalog shows which modules publish and subscribe.
Validate your subscriptions — check-pact
check-pact verifies that every contract in your events.subscribes actually fits the producer's schema:
# Subscribing only to first-party platform events:
npx @tv/extension-sdk check-pact module-manifest.json
# Subscribing to an event published by another MODULE — federate its
# manifest in, because module events aren't in the first-party catalog:
npx @tv/extension-sdk check-pact module-manifest.json \
--producer=../tv-module-cafm/module-manifest.jsonPass one --producer per producing module. Without it, a subscription to a module event fails as "unknown event" — which is the tool working correctly, not a bug.
Guard your own published events — check-events
If you publish events, snapshot their schemas and commit the snapshot. check-events compares the current schemas against it and fails CI on any breaking change — before it reaches your consumers:
npx @tv/extension-sdk snapshot-events ./tv-events.snapshot.json # regenerate
npx @tv/extension-sdk check-events ./tv-events.snapshot.json # verify in CIBoth commands take the snapshot path as an argument.
Publish + subscribe at runtime
The bus is reached through eventBus on the platform context:
import { usePlatformContext } from '@tv/extension-sdk/react';
import { useEffect } from 'react';
function useWorkOrderEvents() {
const { eventBus } = usePlatformContext();
// publish — (eventName, payload)
const announce = (wo: WorkOrder) =>
eventBus.publish('cafm.work-order.created', {
id: wo.id,
buildingId: wo.buildingId,
});
// subscribe — returns an unsubscribe function; call it on unmount
useEffect(() => {
return eventBus.subscribe<AlarmEvent>('building.alarm.triggered', (event) => {
// react to the alarm
});
}, [eventBus]);
return { announce };
}eventBus, not events
The context field is eventBus. publish takes the payload directly as its second argument — the event's version lives in the manifest declaration, not in each call.
Transport
EventBusClient is a stable interface over a moving transport: it currently proxies to the platform WebSocket gateway and is being migrated to NATS JetStream. Your code doesn't change either way — that's the point of the interface.
Need a response? That's not an event
publish() is fire-and-forget. It returns Promise<void>, which resolves when the bus accepts the event — not when (or whether) any consumer has processed it. The bus has no request/response mode.
When your code needs a result, the request/response pair is an HTTP call via ctx.api to the module that owns the data; the event is how everyone else finds out:
const { api, eventBus } = usePlatformContext();
// The API call IS the request/response — you get the created entity back.
const wo = await api.post<WorkOrder>(
`/api/v1/buildings/${building.id}/work-orders`,
dto,
);
// The event is the announcement for other modules; nobody "replies" to it.
await eventBus.publish('cafm.work-order.created', {
id: wo.id,
buildingId: wo.buildingId,
});Rule of thumb: API for questions, events for announcements. If you catch yourself publishing an event and then waiting for a "reply" event, replace the pair with one API call.
Versioning
Event names are namespaced (<module>.<entity>.<action>) and carry a semver version. Bumping a payload's shape in a breaking way means a new major on that event — consumers pin the version they understand, so you can evolve without breaking them.
→ Next: Testing