Skip to content

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

json
"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:

bash
# 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.json

Pass 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:

bash
npx @tv/extension-sdk snapshot-events ./tv-events.snapshot.json   # regenerate
npx @tv/extension-sdk check-events ./tv-events.snapshot.json      # verify in CI

Both commands take the snapshot path as an argument.

Publish + subscribe at runtime

The bus is reached through eventBus on the platform context:

ts
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:

ts
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

Built on the Tango Vision platform. Questions? developers@tango.vision