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:

text
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 descriptor

Event 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 source plus id. 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 xema root 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 form https://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:

RouteReturns
GET /events/catalogueOne entry per event type and major: type, major, scope, holderBiomeId, dataschema, state, latestVersion, deprecated, versions, producers, consumers, latestMajor and consumersOnOlderMajors.
GET /events/catalogue/asyncapiThe same catalogue as an AsyncAPI document, with messages keyed <type>.v<major>.
GET /events/schemas/:type/:majorThe 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:

  1. A descriptor — declare the event you consume with defineEvent from @xemahq/events, giving its type, source, and a Zod schema for data. This is the runtime contract the transport validates delivered payloads against.
  2. A consumed-events registry — collect the descriptors your service consumes into a consumed-events.registry.ts array. The service module registers this array so the event hub knows which types to deliver to this service.
  3. 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

ts
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

ts
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 id lets handlers dedupe.
  • data is 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 typeSourceTypical use
xema.biome.installation.lifecycle.changed.v1biome-host-apiCache invalidation, audit dashboards, per-installation roll-ups

  • 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 →