Omnigraph
Concepts

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:

  1. What kinds of things exist? Node types (node Person, node Company).
  2. How can they be connected? Edge types (edge WorksAt: Person -> Company).
  3. What properties do they carry? Typed fields (name: String, age: I32?).
  4. 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 -> Agent

Use 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 -> Decision

Now 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 -> Company

Detail 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 -> Company

This approach lets you evolve the schema without breaking existing queries. New properties are nullable (?) by default, so existing data remains valid.

On this page