Oxagen Docs

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:

  1. Ontology events — every node, edge, and source change emits a typed event onto a Redis Streams bus.
  2. Workspace agent triggers — user-defined subscriptions that match an event filter and enqueue prompted agent runs.
  3. 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 kindEmitted byFields
node.createdOntology service on insertnode_id · type · name · properties · source
node.updatedOntology service on property changenode_id · type · changed_fields
node.deletedSoft- or hard-deletenode_id · type
node.referencedQuestion Answerer on every cited node; any worker that reads while processingnode_id · type · ref_source · ref_timestamp
edge.createdOntology service on insertedge_id · type · source_node_id · target_node_id · properties
edge.updatedOntology service on property changeedge_id · type · changed_fields
edge.deletedSoft- or hard-deleteedge_id · type
type.discoveredFirst INSERT of a previously-unseen (type_name, type_kind) pairtype_name · type_kind (node · edge)
source.startedA connector / note / file / prompt began emitting eventssource_kind · source_id · source_label
source.progressMid-run progress from a connector with per-item granularitysource_kind · source_id · processed · total · last_label
source.completedSource 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.

ColumnMeaning
nameDisplay label.
enabledToggle without deleting the row.
event_def_idFK to the OntologyEventDef — owns the trigger_kind (node.created etc.) and the JSON filter ({"type": "deal", "properties.stage": "closed_won"}).
promptInstructions for the dispatched agent. Plain text; ≤ 16 KB.
context_refs_jsonOptional structured @mentions of nodes / docs the agent should pull into context before running.
agent_kindThe agent flavour to dispatch (e.g. question_answerer, worker).
model_tierBranded tier persisted for audit / compliance.
execution_modeimmediate (run on every match) or batched (group matches and run on a cron).
batch_cron / batch_max_sizeWhen batched.
persist_run_memoryWhen true the worker signals memory linkage after a successful run.
tool_policy_jsonPer-trigger tool allowlist / denylist override.

Run lifecycle

When the resolver matches an event:

  1. Resolve. Filter the event against event_def_id's JSON predicate. Miss → skip.
  2. Snapshot. Copy the trigger's prompt, agent kind, model tier, and tool policy into a fresh WorkspaceAgentTriggerRun row. The snapshot is immutable even if the trigger row is later edited — the run records what executed, not what the trigger currently says.
  3. Dispatch. Hand off to the agent runtime in agent_platform.executor. The runtime materialises an Execution + one or more Run rows.
  4. Step. Each tool call writes a row to run.step with the hash chained against the previous step in the same run.
  5. Terminate. When the run finishes, status flips to completed or failed, tokens / cost / output_text / feedback land on the row, and a terminal audit.event lands with action='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:

ColumnMeaning
idUUID.
tsUTC timestamp.
tenant_id · workspace_id · project_idScope.
chain_idtenant:<tenant_id>:workspace:<workspace_id> — one chain per workspace inside a tenant. Avoids a global write bottleneck.
chain_seqMonotonic integer inside the chain. UNIQUE(chain_id, chain_seq).
actor_kinduser · agent_run · api_key · service.
actor_user_id / actor_agent_run_id / actor_api_key_idThe id corresponding to actor_kind.
ip_address · user_agentNetwork attribution when present.
entity_kind · entity_idWhat was mutated.
actionTyped action string (agent.created, brand_kit.activated, trigger.fired, document.created, …).
before · afterJSONB snapshots of the row before and after the mutation.
event_metadataFree-form context (request id, idempotency key, etc.).
prev_hash · this_hashThe 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_attestation row 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>.json with 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.event chains via chain_id + a ts filter.
  • run.step chains, one per terminal Run whose ended_at date matches covers_date. Only succeeded / failed / cancelled runs 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, and source.completed events plus thousands of node.created / edge.created events per sync.
  • Security — workspace isolation and the encryption posture.

Get started free · Agent Overview · Security

On this page