Events you emit

Emitting events is a declaration, not a call — the trigger model bound to row changes, the two-segment name rule, payload locators, and realtime channels.

An event is a fact your app publishes so other apps can react to it without either app knowing about the other. Both directions are declarations, not API calls.

There is no publish API. platform.events.publish() and platform.events.on() do not exist. If you have seen them in an example, that example was wrong — no app calls anything like them, because there is nothing to call.

The declaration

manifest.json
"events": {
  "emits": [
    { "name": "serial.registered", "entity": "device_serials", "on": ["insert"] },
    { "name": "serial.updated", "entity": "device_serials", "on": ["update"],
      "payload": { "serial_id": "column:id", "status": "column:status" } },
    { "name": "serial.serviced", "entity": "service_events", "on": ["insert"], "channel": true },
    "serial.audited"
  ]
}
FieldRequiredDescription
nameYes<entity>.<action>, exactly two dot-separated segments. Published on the bus as <vendor>.<app>.<name>.
entityNoAn entity from your schema.json whose row changes emit this event.
onNoWhich row operations fire it: insert, update, delete.
payloadNoMap of output key to a column:<col> locator. Omit to send the changed row minus the injected columns.
sampleNoExample payload, shown in catalogs and used to prefill test runs.
channelNotrue marks the event pushable to a realtime channel for subscribed browsers. Default false.

The trigger model

With entity and on present, the platform generates the trigger that publishes the event when those row operations occur. That is the whole mechanism: a row goes in, the event goes out. You write no publish call.

Which leads to the thing worth internalising:

An emitted event is a property of a table, not a call site in your code.

If a row can change through a path you did not write — the Cockpit's own renderer, a mountCrud route, another capability, a bulk import — the event fires there too. That is usually exactly what you want, and it is the reason the model is declarative. But it means:

  • You cannot conditionally emit. There is no "only if the status really changed" — that logic belongs in the consumer.
  • You cannot emit something that is not a row change. A computation, a decision, a call that failed: none of those have a row, so none of them can be bound this way.
  • You cannot emit twice for one write, or suppress the event for a migration script's writes.

Design the consumer to be robust to all of that rather than trying to be clever with the trigger.

Catalog-only entries

An entry that is a bare string, or an object with no entity/on, is catalog-only: it declares the event's existence for anyone reading the catalog, and nothing publishes it.

manifest.json
"emits": ["serial.audited"]

That is useful for documenting an event you intend to exist, but be careful with it — a catalog entry nothing publishes looks exactly like a working event to somebody building a consumer against your catalog. Prefer binding it to an entity, or leaving it out until it does something.

The name rule

Event names must match ^[a-z][a-z0-9_]*\.[a-z][a-z0-9_]*$: exactly two dot-separated segments.

NameValid
serial.registeredYes
stock_level.adjustedYes
serial.service.registeredNo — three segments
registeredNo — one segment
Serial.RegisteredNo — lowercase only

The manifest schema catches this, and so does the scaffolder. It is also one of the rules that is checked again at publish, so a hand-edited manifest that slipped through locally still fails on the way out. See Validate.

On the bus, the name is prefixed with your vendor and app: serial.registered in the acme/serials app is published as acme.serials.serial.registered. So the two-segment rule applies to your half of the name, and the full name is unambiguous across every app on the platform.

payload locators

By default the event carries the changed row minus the injected columns. payload narrows and renames it:

manifest.json
{
  "name": "serial.updated",
  "entity": "device_serials",
  "on": ["update"],
  "payload": {
    "serial_id": "column:id",
    "serial": "column:serial",
    "status": "column:status"
  }
}

Each value is a column:<col> locator naming a column on that entity. The key is the name the consumer sees.

Declare a payload for anything you would not want in an audit trail forever, because that is effectively what an event bus is. The default — the whole row — is convenient and also broad: a column holding a note, an address or a price ends up in every consumer's inbox. Narrow it to the identifiers and the state a consumer needs, and let them call your capability for the rest.

A sample alongside it is cheap and pays for itself the first time somebody builds a consumer against your event without asking you what it looks like.

channel

manifest.json
{ "name": "serial.serviced", "entity": "service_events", "on": ["insert"], "channel": true }

channel: true marks the event pushable to a realtime channel, so a subscribed browser can receive it — the mechanism behind a Cockpit list that updates without a refresh. Default false.

Set it for events an operator would want to see land live. Leave it off for high-frequency internal churn: every channel event is a push to every subscribed browser.

What we are not claiming

Delivery semantics for the event bus — ordering, at-least-once versus at-most-once, retry policy, dead-letter handling, and the exact shape of the delivered envelope — are not settled enough to document. This page will not invent them.

What that means for you in practice:

  • Make consumers idempotent. Assume the same event may arrive twice, because that is the assumption you can afford to be wrong about in the safe direction.
  • Do not treat an event as a transaction. An event that has not arrived yet and an event that will never arrive look the same from the other side.
  • Use a capability call when you need an answer. Events are for something happened. If code needs a value before it can continue, it calls your capability.

Events, capabilities or a workflow?

UseWhen
A capability callThe caller needs an answer now — "is this in stock?", "what is this priced at?". Synchronous, and the caller declares a capability permission.
An eventSomething happened and other apps may want to know. Asynchronous, and you do not know or care who reacts.
An Integration Studio workflowThe reaction involves an external system, or needs durable execution with retries and visibility. See Integration Studio.

Next steps

Was this page helpful?