Ontologies
How typed schemas give your graph a formal domain model.
In Omnigraph, the .pg schema file is an ontology -- a formal description of what exists in your domain, how things relate, and what constraints apply. The schema compiler enforces this ontology at every write, so agents can trust the structure of the data they read.
Schema as ontology
An ontology answers four questions:
- What kinds of things exist? Node types (
node Person,node Company). - How can they be connected? Edge types (
edge WorksAt: Person -> Company). - What properties do they carry? Typed fields (
name: String,age: I32?). - What rules apply? Constraints (
@key,@unique,@index, enums, nullable markers).
In academic knowledge representation, these correspond to classes, relations, attributes, and axioms. In Omnigraph, they are all declared in a single .pg file and enforced by the database engine.
Why ontologies matter for agents
Without a typed schema, agents make up field names, store inconsistent data, and break each other's assumptions. A formal ontology gives agents:
- Discovery -- An agent can read the schema to learn what entities and relationships exist without being told in its prompt.
- Validation -- Mutations are rejected at write time if they violate the schema. No garbage in, no garbage out.
- Shared vocabulary -- Two agents working on the same graph use the same type names, edge names, and enum values. There is no ambiguity about what "status" means.
- Queryability -- The query compiler uses the schema to typecheck traversal paths. If an agent writes a query that doesn't match the schema, it fails at compile time, not at runtime.
Design patterns
Use enums for closed vocabularies
When a property has a fixed set of valid values, use an enum. This prevents agents from inventing new statuses, categories, or types.
node Ticket {
slug: String @key
status: enum(open, in_progress, resolved, closed)
priority: enum(low, medium, high, critical)
}Use edges instead of string references
If one entity references another, make it an edge. Edges are traversable, enforceable, and visible in queries.
# Instead of: assignee: String
edge AssignedTo: Ticket -> AgentUse interfaces for shared structure
When multiple node types share the same properties, define an interface.
interface Timestamped {
created_at: DateTime
updated_at: DateTime
}
node Ticket implements Timestamped {
slug: String @key
title: String
created_at: DateTime
updated_at: DateTime
}
node Message implements Timestamped {
body: String
created_at: DateTime
updated_at: DateTime
}Promote events to nodes
If an action matters for audit, analysis, or traversal, give it a node type instead of burying it in a log.
node Decision {
outcome: String
rationale: String
decided_at: DateTime
decided_by: String
}
edge HasDecision: Ticket -> DecisionNow you can query all decisions, find decisions without rationale, or traverse from a ticket to its decision history.
Layered ontology
Start with a core layer of stable entity types. Add detail layers as your understanding grows.
Core layer -- the entities and relationships that are unlikely to change:
node Person { name: String @key }
node Company { name: String @key }
edge WorksAt: Person -> CompanyDetail layer -- properties and edges added as requirements emerge:
node Person {
name: String @key
title: String?
bio: String? @index
embedding: Vector(1536) @index
}
edge Manages: Person -> Person
edge Founded: Person -> CompanyThis approach lets you evolve the schema without breaking existing queries. New properties are nullable (?) by default, so existing data remains valid.