Skip to content

Reasoning

OWLGraph performs reasoning at write time, not query time. Every mutation is intercepted by the materialization engine, which applies inference rules and appends additional edges before the data is committed.

Client mutation
Parse JSON/RDF → Directed edges
OWLGraph Materializer
├── 0. Delete cascades
├── 1. Type hierarchy (subClassOf)
├── 2. Domain inference
├── 3. Range inference
├── 4. Property hierarchy (subPropertyOf)
├── 5. Inverse properties
├── 6. Symmetric properties
├── 7. Property chains
├── 8. Transitive closure
└── 9. Disjointness validation
Validation, indexing, commit to storage

The property-hierarchy rule runs before inverse, symmetric, and chain rules so that materialized parent-property edges can themselves participate in those downstream rules. Transitive closure runs last so it sees the full enriched edge set.

When a type is removed from a node, all ancestor types are also removed:

Remove: <0x1> dgraph.type "GoldenRetriever"
Effect: <0x1> dgraph.type "Dog" (removed)
<0x1> dgraph.type "Mammal" (removed)
<0x1> dgraph.type "Animal" (removed)

When dgraph.type is set, all superclass types are added:

Input: <0x1> dgraph.type "GoldenRetriever"
Inferred: <0x1> dgraph.type "Dog" (owl.inferred=true)
<0x1> dgraph.type "Mammal" (owl.inferred=true)
<0x1> dgraph.type "Animal" (owl.inferred=true)

Using a property with rdfs:domain infers the domain type on the subject:

Ontology: hasOwner rdfs:domain Animal
Input: <0x1> hasOwner <0x2>
Inferred: <0x1> dgraph.type "Animal" (owl.inferred=true)

Using a property with rdfs:range infers the range type on the object:

Ontology: hasOwner rdfs:range Person
Input: <0x1> hasOwner <0x2>
Inferred: <0x2> dgraph.type "Person" (owl.inferred=true)

When an edge is written on a property declared as rdfs:subPropertyOf of one or more parent properties, an edge is materialized for every transitive ancestor property:

Ontology: quotes rdfs:subPropertyOf intertextuallyRelatedTo
intertextuallyRelatedTo rdfs:subPropertyOf semanticLink
Input: <0x1> quotes <0x2>
Inferred: <0x1> intertextuallyRelatedTo <0x2> (owl.inferred=true)
<0x1> semanticLink <0x2> (owl.inferred=true)

The materialized parent edges are then themselves fed into the inverse, symmetric, chain, and transitive rules, so a single sub-property write can produce a substantial fan-out when the parent properties are richly characterized.

Ontology: isOwnerOf owl:inverseOf hasOwner
Input: <0x1> hasOwner <0x2>
Inferred: <0x2> isOwnerOf <0x1> (owl.inferred=true)
Ontology: friendOf a owl:SymmetricProperty
Input: <0x1> friendOf <0x2>
Inferred: <0x2> friendOf <0x1> (owl.inferred=true)
Ontology: hasGrandparent = hasParent o hasParent
Input: <0x3> hasParent <0x2>
<0x2> hasParent <0x1>
Inferred: <0x3> hasGrandparent <0x1> (owl.inferred=true)

For any property declared as owl:TransitiveProperty, the engine computes the closure of all reachable pairs and materializes any missing direct edges:

Ontology: precedesEvent a owl:TransitiveProperty
Input: <Flood> precedesEvent <Fall>
<Fall> precedesEvent <Creation>
Inferred: <Flood> precedesEvent <Creation> (owl.inferred=true)

Closure spans transactions: when the new edge connects to a chain that is already persisted, the engine reads the existing graph (using the predicate’s forward and @reverse indexes) and emits the additional links the new edge implies. As a result, transitive writes cost slightly more than non-transitive ones — the engine performs forward and backward BFS from each new edge, bounded by the circuit breaker.

If you bulk-load a large historical graph and then declare a property transitive after the fact, run the retroactive reasoner to catch up; new writes alone will not back-fill arbitrary historical chains.

After all inference, the engine checks for disjoint type violations. The check considers types from the current mutation, types inferred by upstream rules, and types already persisted on the entity, expanded with the full ancestor closure of every type. This means a violation is caught even when the conflicting types were written in separate transactions, and even when disjointness is declared on a superclass of the asserted type.

Ontology: Dog owl:disjointWith Cat
Mutation A (committed): <0x1> dgraph.type "Dog"
Mutation B (rejected): <0x1> dgraph.type "Cat"
Error: "disjointness violation: type Dog is disjoint with type Cat"

All materialized edges carry owl.inferred=true as a facet. Query it to distinguish asserted from inferred facts:

{
q(func: type(Dog)) {
name
dgraph.type @facets(owl.inferred)
}
}

If a single mutation generates more than 10,000 inferred edges, materialization is aborted and the mutation is rejected. This prevents runaway inference in pathological ontologies.

When an ontology is loaded into a cluster with existing data, a background process scans existing nodes and adds missing ancestor type edges. This runs asynchronously — the ontology load response returns immediately.