Events, triggers, and audits
How Oxagen turns ontology changes into observable events, how those events fire user-defined agent triggers, and how every agent run is recorded in a hash-chained, Merkle-attested audit log.
Oxagen has three coupled control-plane systems:
- Ontology events — every node, edge, and source change emits a typed event onto a Redis Streams bus.
- Workspace agent triggers — user-defined subscriptions that match an event filter and enqueue prompted agent runs.
- Audit chain — every config-plane mutation and every runtime step lands in an append-only, hash-chained table that the daily attestation job rolls up into a Merkle root.
This page is the reference for all three. None of the surfaces below are part of a separate "observability" or "compliance" add-on — they ship in the platform.
Ontology events
Every mutation against the workspace ontology emits a typed
event on a Redis Streams bus. The wire format is one Redis
stream entry per event, keyed {"payload": "<json>"}, with the
event class registered on import.
Event kind | Emitted by | Fields |
|---|---|---|
node.created | Ontology service on insert | node_id · type · name · properties · source |
node.updated | Ontology service on property change | node_id · type · changed_fields |
node.deleted | Soft- or hard-delete | node_id · type |
node.referenced | Question Answerer on every cited node; any worker that reads while processing | node_id · type · ref_source · ref_timestamp |
edge.created | Ontology service on insert | edge_id · type · source_node_id · target_node_id · properties |
edge.updated | Ontology service on property change | edge_id · type · changed_fields |
edge.deleted | Soft- or hard-delete | edge_id · type |
type.discovered | First INSERT of a previously-unseen (type_name, type_kind) pair | type_name · type_kind (node · edge) |
source.started | A connector / note / file / prompt began emitting events | source_kind · source_id · source_label |
source.progress | Mid-run progress from a connector with per-item granularity | source_kind · source_id · processed · total · last_label |
source.completed | Source finished (success or error) | source_kind · source_id · source_label · entities_added · edges_added · types_discovered · status · error |
Every event inherits four base fields: workspace_id (routing
key), event_id (dedup), occurred_at, and kind. The bus is
workspace-scoped — consumers subscribe per workspace and
never see cross-tenant traffic.
Publishing
Publishing happens through oxagen.ontology_events.get_event_bus().publish(event).
Bus failures are non-fatal: the calling sync logs the publish
failure and continues. The ontology mutation is the source of
truth; the event is a downstream signal.
Consuming
Consumers go through XREADGROUP on a workspace-scoped consumer
group. The Workspace Agent Trigger resolver is the
canonical consumer; downstream analytics consumers read the
same stream with their own group name.
Workspace agent triggers
A WorkspaceAgentTrigger row is a user-defined ontology event
→ prompted agent run binding. The user opens
Settings → Automations in the dashboard, picks an
OntologyEventDef (a typed event subscription with a filter),
writes a prompt, picks an agent flavour and model tier, and
saves.
| Column | Meaning |
|---|---|
name | Display label. |
enabled | Toggle without deleting the row. |
event_def_id | FK to the OntologyEventDef — owns the trigger_kind (node.created etc.) and the JSON filter ({"type": "deal", "properties.stage": "closed_won"}). |
prompt | Instructions for the dispatched agent. Plain text; ≤ 16 KB. |
context_refs_json | Optional structured @mentions of nodes / docs the agent should pull into context before running. |
agent_kind | The agent flavour to dispatch (e.g. question_answerer, worker). |
model_tier | Branded tier persisted for audit / compliance. |
execution_mode | immediate (run on every match) or batched (group matches and run on a cron). |
batch_cron / batch_max_size | When batched. |
persist_run_memory | When true the worker signals memory linkage after a successful run. |
tool_policy_json | Per-trigger tool allowlist / denylist override. |
Run lifecycle
When the resolver matches an event:
- Resolve. Filter the event against
event_def_id's JSON predicate. Miss → skip. - Snapshot. Copy the trigger's prompt, agent kind, model
tier, and tool policy into a fresh
WorkspaceAgentTriggerRunrow. The snapshot is immutable even if the trigger row is later edited — the run records what executed, not what the trigger currently says. - Dispatch. Hand off to the agent runtime in
agent_platform.executor. The runtime materialises anExecution+ one or moreRunrows. - Step. Each tool call writes a row to
run.stepwith the hash chained against the previous step in the same run. - Terminate. When the run finishes, status flips to
completedorfailed, tokens / cost / output_text / feedback land on the row, and a terminalaudit.eventlands withaction='agent.run.completed'.
Index posture
The trigger table is indexed on
(workspace_id, enabled, is_deleted) so the per-event resolver
sweep is a single index scan. The run-history table indexes
(workspace_agent_trigger_id, started_at) for trigger-specific
timelines and (workspace_id, started_at) for the
workspace-wide automations view.
Audit chain
Every config-plane mutation (agent created / playbook published /
brand kit activated / trigger edited / human decision recorded)
and every runtime step (tool call / approval / artifact created)
writes an append-only row. The audit Postgres schema revokes
UPDATE and DELETE on the oxagen role — once written, rows are
read-only.
audit.event
The config-plane + human-decision table. Keyed:
| Column | Meaning |
|---|---|
id | UUID. |
ts | UTC timestamp. |
tenant_id · workspace_id · project_id | Scope. |
chain_id | tenant:<tenant_id>:workspace:<workspace_id> — one chain per workspace inside a tenant. Avoids a global write bottleneck. |
chain_seq | Monotonic integer inside the chain. UNIQUE(chain_id, chain_seq). |
actor_kind | user · agent_run · api_key · service. |
actor_user_id / actor_agent_run_id / actor_api_key_id | The id corresponding to actor_kind. |
ip_address · user_agent | Network attribution when present. |
entity_kind · entity_id | What was mutated. |
action | Typed action string (agent.created, brand_kit.activated, trigger.fired, document.created, …). |
before · after | JSONB snapshots of the row before and after the mutation. |
event_metadata | Free-form context (request id, idempotency key, etc.). |
prev_hash · this_hash | The chain. this_hash is computed over prev_hash + payload. Replay-verifiable. |
run.step
The runtime-step table. Same shape but rooted at a Run rather
than at a workspace — one chain per run, identified by
f"run:{run_id}". Tool calls, approval requests, error frames,
artifact creations all land here.
Verification
oxagen.domains.agent_platform.hash_chain.verify_chain(session, chain_id)
re-computes every row's this_hash and asserts continuity.
Operators run it ad-hoc; the daily attestation job runs it
implicitly before sealing the day's root.
Merkle attestation
A daily Cloud Run Job enumerates every chain with activity on
covers_date, fetches the per-row hashes, builds a Merkle tree,
and writes:
- One
audit.merkle_attestationrow per chain per day, carrying(chain_id, covers_date, first_seq, last_seq, event_count, merkle_root, gcs_uri, attested_at). - One immutable JSON blob to GCS under
gs://oxagen-audit-attestations/<covers_date>/<chain_id>.jsonwith the full per-row hash list and the root. The bucket is configured with object lock so the blob is tamper-evident even against compromised database credentials.
Both sources are enumerated:
audit.eventchains viachain_id+ atsfilter.run.stepchains, one per terminalRunwhoseended_atdate matchescovers_date. Onlysucceeded/failed/cancelledruns contribute — open runs are still accruing hashes.
Customer security reviews and SOC 2 audits read
audit.merkle_attestation for the in-database root and verify
against the GCS blob; an external auditor with read access to
the GCS prefix can independently reconstruct the day's root.
Webhooks
Inbound webhooks (Stripe events, GitHub push, Zoom webhook,
Linear webhook, etc.) land in webhooks and emit ontology
events the same way connector syncs do. Signature verification
happens at the boundary; the body is validated before any
ontology mutation. Inbound webhook deliveries themselves are
recorded in audit.event with entity_kind='webhook' so an
operator can answer "did GitHub deliver this push?" without
leaving the audit surface.
Notifications
notify.dispatch is the outbound side. An agent calls it
with a structured payload plus a channel selector (webhook /
Slack / email). Each dispatch is recorded in audit.event with
action='notify.dispatched' and the channel + idempotency key
in event_metadata. Repeated calls with the same
dedupe_key collapse to one delivery.
Where to go next
- Agent Tools — every capability that
shows up in
audit.event. - Agent Memory — how memory layers consume the ontology bus.
- Linear — a connector that
publishes
source.started,source.progress, andsource.completedevents plus thousands ofnode.created/edge.createdevents per sync. - Security — workspace isolation and the encryption posture.
GitHub App (private repositories)
Install and configure the Oxagen GitHub App so your workspace can ingest source code from private and organization-owned repositories.
Connectors
First-party data-source connectors that ingest your real-world data — meetings, email, calendars, files, code, transactions, warehouses — into a typed, queryable knowledge graph your AI agents plug into.