SDK — Events I Consume
A biome consumes platform events through the backend service it ships. The service registers a set of typed consumed-event descriptors with @xemahq/events; the platform's event hub delivers every matching CloudEvent to that service, and a decorated handler method processes it.
There is no manifest field that subscribes a biome to events. Event consumption is a property of running code — a biome with no shipped service consumes no events. If your biome needs to react to platform events, it ships an API and registers consumers inside it.
CloudEvents 1.0 envelope
Every platform event is a CloudEvents 1.0 message delivered over the platform's event transport. The envelope carries:
1specversion = "1.0"
2type = "<domain>.<resource>.<verb>.v<major>" e.g. "xema.biome.installation.lifecycle.changed.v1"
3source = "<service-slug>" e.g. "biome-host-api"
4id = "<unique delivery id>"
5time = ISO-8601
6subject = "<resource ref>" e.g. "xema://store/biome/[email protected]"
7orgid = "<orgId>" extension attribute
8projectid = "<projectId>" extension attribute (when applicable)
9data = { ... } typed per the registered event descriptorEvent types are versioned in the vN suffix — same major-bump-only rule as capabilities. A consumer registered for xema.biome.installation.lifecycle.changed.v1 keeps receiving v1 events even after a v2 type ships; to receive v2 the consumer registers the v2 descriptor.
A biome declares the event types it publishes under the namespace prefix its manifest claims (see Delivery and namespaces). A biome can also consume types that other services publish.
Delivery and namespaces
- At-least-once delivery. An event may arrive more than once.
- Deduplicate by
sourceplusid. Consumers must tolerate repeats by remembering the pair. - Namespace claims. A manifest declares a prefix for the event types it publishes. The longest matching prefix wins. The reserved
xemaroot is for first-party biomes only. A manifest that claims a prefix it may not hold is refused at admission, with a typed reason.
Versioning, producer and schema
- Every event type is versioned by major. The
v<major>suffix is part of the type. A (type, major) pair must be registered before anything can publish it; publishing an unregistered (type, major) is refused with a typed reason. - Every event carries
dataschema. Publishers stamp it, and facts and index rows carry it too. For a registered event it has the formhttps://schemas.xema.dev/events/<type>/<major>. - Every event type names its producer. An index event declaration names the service that produces it.
Subscription grants
The event hub grants a subscription per exact (service, tenant, type, major). A grant for one major does not cover another, and a grant for one tenant does not cover another. Once given, a grant is sticky: it stays granted.
Finding events
The catalogue is the source of truth for which events exist. These docs do not keep their own list. The biome host serves a catalogue filtered to the calling organisation, so you only see types your organisation can see:
| Route | Returns |
|---|---|
GET /events/catalogue | One entry per event type and major: type, major, scope, holderBiomeId, dataschema, state, latestVersion, deprecated, versions, producers, consumers, latestMajor and consumersOnOlderMajors. |
GET /events/catalogue/asyncapi | The same catalogue as an AsyncAPI document, with messages keyed <type>.v<major>. |
GET /events/schemas/:type/:major | The schema for one event type and major. Answers 404 for a type your organisation cannot see. |
latestMajor and consumersOnOlderMajors show whether consumers still sit on an older major before you retire it.
How a biome consumes an event
Consumption is three pieces of code inside the biome's shipped service:
- A descriptor — declare the event you consume with
defineEventfrom@xemahq/events, giving itstype,source, and a Zod schema fordata. This is the runtime contract the transport validates delivered payloads against. - A consumed-events registry — collect the descriptors your service consumes into a
consumed-events.registry.tsarray. The service module registers this array so the event hub knows which types to deliver to this service. - A handler — a provider method in your service decorated with
@OnCloudEvent(<descriptor>)that receives the validated envelope and does the work.
1. Declare the descriptor
1// src/events/consumed/consumed-events.registry.ts
2import { defineEvent, type EventSource, VisibilityTier } from '@xemahq/events';
3import { z } from 'zod';
4
5const BIOME_HOST_SOURCE: EventSource = '/services/biome-host-api';
6
7export const BiomeInstallationLifecycleChangedConsumed = defineEvent({
8 type: 'xema.biome.installation.lifecycle.changed.v1',
9 source: BIOME_HOST_SOURCE,
10 schemaVersion: '1.0.0',
11 visibility: VisibilityTier.PROJECT,
12 description: 'A biome installation changed lifecycle — refresh the biome cache for the org.',
13 data: z.object({
14 installationId: z.string(),
15 biomeId: z.string(),
16 orgId: z.string(),
17 }),
18 hint: z.object({ installationId: z.string(), biomeId: z.string() }),
19});
20
21export const MY_BIOME_CONSUMED_EVENT_DESCRIPTORS = [
22 BiomeInstallationLifecycleChangedConsumed,
23] as const;2. Register the array in the service module
The service's @xemahq/events module registration takes the descriptor array so the event hub creates the subscription for exactly those types. An unknown or misspelled type never silently no-ops — the transport only delivers types that a registered descriptor declares.
3. Handle the delivered event
1import { OnCloudEvent } from '@xemahq/events';
2
3import { BiomeInstallationLifecycleChangedConsumed } from '../events/consumed/consumed-events.registry';
4
5@Injectable()
6export class BiomeInstallationConsumerService {
7 @OnCloudEvent(BiomeInstallationLifecycleChangedConsumed)
8 async onBiomeInstallationLifecycleChanged(
9 event: typeof BiomeInstallationLifecycleChangedConsumed.envelope,
10 ): Promise<void> {
11 // event.data.biomeId / event.data.orgId — validated against the descriptor schema
12 // event.orgid / event.projectid — envelope extension attributes
13 await this.refreshCache(event.data.orgId);
14 }
15}Only the Xema import is shown. @Injectable() comes from your service framework's own package — register the handler class as a provider in your module the usual way, and the SDK discovers its @OnCloudEvent methods at boot.
Rules:
- The handler runs inside the biome's own shipped service, under that service's identity and grants — every downstream call it makes is authorised exactly like any other call the service makes.
- The handler MUST be idempotent. The platform delivers events at-least-once; the deterministic CloudEvents
idlets handlers dedupe. datais validated against the descriptor's Zod schema before your handler runs. A payload that fails the schema is a delivery error, not a silent skip.- Long-running work belongs in a workflow run dispatched from the handler, not in the handler body.
When a biome ships no service
A contributions-only biome — one that ships agents, skills, workflows, or connector bindings but no ships.apis[] service — does not consume platform events directly. If it needs event-driven behaviour, either:
- react to workflow lifecycle inside a workflow definition (gates, follow-on jobs), or
- ship a small backend service (Backend I ship) whose only job is to register consumers.
Example event types
The catalogue (see Finding events) is the list. The rows below are one illustrative example of what a consumer might subscribe to, not a complete or current list.
| Event type | Source | Typical use |
|---|---|---|
xema.biome.installation.lifecycle.changed.v1 | biome-host-api | Cache invalidation, audit dashboards, per-installation roll-ups |
Related pages
- Backend I ship — the service a biome needs before it can consume events
- Capabilities — how the calls a handler makes are authorised
- Lifecycle Hooks — the biome's own lifecycle transitions, and why nothing fires on them yet
Previous: ← UI I contribute Next: Testing →