---
title: "Use platform events"
description: "Connect chat channels, inventory changes and granted plugin results."
canonical_url: "https://docs.oz0.ai/build/parts/event-sources/platform-events"
---
# Use platform events

> Connect chat channels, inventory changes and granted plugin results.

For: plugin authors.

Connect chat channels, inventory changes and granted plugin results.

## Chat channels

A source whose events carry chat messages can be a channel: mentions and slash commands start agents, answers, questions and approvals go to the thread. See [Chat channels](https://docs.oz0.ai/build/apps/chat-channels/build#build-a-channel) for the `channels:` block, the message an event carries and the post function, and the verified plugins [Slack](https://docs.oz0.ai/build/plugins/verified/slack), [Discord](https://docs.oz0.ai/build/plugins/verified/discord) and [Microsoft Teams](https://docs.oz0.ai/build/plugins/verified/teams) for three complete ones.

A chat system that names where replies go in each delivery (Teams' `serviceUrl`) declares the channel's `endpoint`: the
claim of the source's `jwt` signature it must equal, and the hosts it may be on. The edge keeps a message's `endpoint`
only when both hold, refuses the message otherwise (`channel.refused`), and hands it to the post function with each
post of the thread:

```yaml [oz0-plugin.yaml]
channels:
  - name: teams
    events: teams
    post: oz0_teams.post
    endpoint:
      claim: serviceurl
      hosts: [smba.trafficmanager.net]
```

## The inventory's changes

An event source is built in: `inventory`, the changes of the tenant's [inventory](https://docs.oz0.ai/govern/inventory). Its events fire only the flows and routines of a plugin that declares [`reads: [inventory]`](https://docs.oz0.ai/reference/plugin-manifest/data#reads) and that a tenant admin granted it: a flow with this trigger in another plugin fails lint, a routine whose agent or flow is another plugin's gets a warning when it is saved and is never fired, and after a revocation nothing fires. No job can wait for these events, and no plugin may name a source of its own `inventory`.

```yaml [flows/sync.yaml]
name: registry-sync
trigger:
  event: { source: inventory, types: [inventory.changed] }
steps:
  - id: sync
    tool: oz0_registry.sync
    input: { name: "${{ trigger.event.attributes.name }}" }
```

Each event's `type` is `inventory.changed`, its `action` one of `created`, `updated`, `retired`, `unretired` and `deleted`, its `subject` `agent/<name>`, and its `attributes` `kind`, `name`, `plugin`, `state`, `flow` and `previous_state`. It carries no text. Since the platform writes these events about its own records, the jobs they start are not untrusted from their start, as an outside delivery's are ([taint](https://docs.oz0.ai/govern/taint-and-labels#what-taints-a-job)): a sync flow's first call to a directory runs without the taint-default rule's ask, and what the directory answers taints the job as any open-world result does.

The edge looks at the inventory of each tenant with a grant every ten seconds, so a change arrives within about that, once however many edges run; at most 100 changes of a tenant are routed per look, and the rest on the next ones.

The first look after a grant routes nothing: it is what the inventory was when the grant began, which the plugin reads with its first call, so a plugin that keeps another system in step also runs a full pass now and then (a daily routine), which catches anything an event missed.

## The results of granted plugins

The second built-in source is `results`: what happens to the plugins whose results a plugin was granted ([`reads: [results]`](https://docs.oz0.ai/reference/plugin-manifest/data#reads), for named targets). It fires only the flows and routines of that plugin, and only for its targets: a flow with this trigger in a plugin that does not declare the kind fails lint, a routine whose agent or flow is of a plugin that was not granted it, or that reads no target yet, gets a warning when it is saved and is never fired, and after a revocation nothing fires.

No job can wait for these events, and no plugin may name a source of its own `results`.

```yaml [routines/on-findings.yaml]
name: improve-on-findings
agent: improver
triggers:
  - name: new-finding
    event:
      source: results
      types: [finding.opened]
      filter: event.attributes.kind == "eval_regression"
```

Two types:

- `finding.opened`: a finding was opened about a target. Its `action` is `opened`, its `subject` `finding/<id>`, and its `attributes` `plugin` (the target), `kind`, `agent` and `finding` (the finding's ID, which [`oz.evidence`](https://docs.oz0.ai/improve/evidence-for-agents) reads with the rest).
- `model.changed`: a model change reaches the target's agents: an alias they ask for was set or deleted, or a model mapping rule that covers them was set or deleted. Its `action` is `set` or `delete`, its `subject` `alias/<name>` or `mapping_rule/<name>`, and its `attributes` `plugin`, `change` (`alias` or `mapping_rule`), `name` and `agents` (the target's agents it reaches).

The events carry no text, and the jobs they start are not untrusted from their start, as the inventory's are. The edge looks every 15 seconds at each tenant with a target, and routes each event once however many edges run; at most 100 new findings and, separately, 100 model changes a tenant per look, the rest on the next ones. Only what happened after a target was granted is routed, and only within the hour before a look, so an edge that was down longer routes nothing it missed.

While the person who granted a target no longer holds `content.read`, has left the tenant or is disabled, nothing of that target is routed, nothing that happened meanwhile is routed later, and an `evidence_grant_lapsed` finding says so until a person who may read content grants the target again.


## Sitemap

See the full [sitemap](https://docs.oz0.ai/sitemap.md) for all pages.
