Giving AI Agents a Shared Vocabulary with OWL and SHACL
Once you have more than one agent touching the same data, you run into a quiet kind of bug. The sales agent writes a record with a customer field that points at a person. The billing agent reads customer and assumes it’s an account. Both tool calls pass validation, because both payloads are perfectly well formed JSON. Nothing crashes. The data just slowly stops meaning what anyone thinks it means.
The schemas we hand to agents today are almost entirely about shape. In the 2026-07-28 revision of the MCP spec, for example, a tool describes its arguments with an inputSchema written in JSON Schema and can optionally describe its result with an outputSchema. That’s genuinely useful, and it catches a missing field or a string where a number should be. What it can’t tell a model is that a customer and a billing account are different kinds of thing, or that a customer only ever has one account manager. That’s the job of an ontology, and the W3C has had a mature set of standards for it for well over a decade.
What an ontology actually is
The OWL 2 Primer has a nicely down to earth definition. It treats an ontology as a document of precise statements about some subject area, and it splits the contents into two kinds of knowledge. Terminological knowledge is the vocabulary and how the terms relate to each other. Assertional knowledge is facts about concrete things. If you squint, that’s your schema and your rows, which is exactly the comparison the Primer makes before explaining why the analogy breaks down.
Underneath OWL sits RDF, which stores everything as triples of subject, predicate and object. The detail that matters most for agents is that the names in those triples are IRIs, and the RDF 1.1 spec says IRIs have global scope, so the same IRI means the same resource wherever it shows up. A JSON key called customer means whatever the last developer thought it meant. The IRI https://example.com/ont#Customer means one thing, defined in one place, for every agent and every system that uses it.
RDF Schema adds classes, subclasses, and the rdfs:domain and rdfs:range of a property. OWL builds on that with more precise tools, like saying two classes are disjoint, two classes are equivalent, or a property is functional (it can have at most one value). Here’s a tiny ontology in Turtle for the example above:
@prefix ex: <https://example.com/ont#> .
@prefix owl: <http://www.w3.org/2002/07/owl#> .
@prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> .
ex:Customer a owl:Class .
ex:BillingAccount a owl:Class .
ex:Customer owl:disjointWith ex:BillingAccount .
ex:accountManager a owl:ObjectProperty, owl:FunctionalProperty ;
rdfs:domain ex:Customer .
Now “customer” and “billing account” are distinct by definition, and a reasoner can flag anything that claims to be both.
The part that surprises everyone
Here’s where people coming from databases and JSON Schema get tripped up, and it matters a lot when the thing writing your data is a language model.
OWL is not a validation language. The Primer says so plainly: it isn’t a schema language for syntax conformance, and it has no way to require that a piece of information be present. It also works under the open world assumption. In a database a missing fact is usually treated as false, while in OWL a missing fact might simply be unknown. On top of that, OWL doesn’t assume that two different names refer to two different things.
Put those together and look at what happens when an agent makes a mistake. Say it asserts that customer 42 has two account managers, jim and james. You might expect the functional property to reject that. It doesn’t. The Primer walks through this exact pattern: with a functional property and two different values, a reasoner concludes the two names must refer to the same individual. RDFS domains behave the same way. The RDF Schema spec defines rdfs:domain as a statement that anything with the property is an instance of the class, so if an agent attaches accountManager to a billing account, the reasoner infers that the account is also a customer. Only because I declared the two classes disjoint does that one turn into an inconsistency.
For humans modelling a domain carefully, that behavior is a feature. For an agent that sometimes gets things wrong, it can be a trap, because the reasoner will happily explain away the error instead of refusing it. Feed this example to a rule based OWL reasoner and you get exactly that: a brand new owl:sameAs link between the two staff members, and no complaint.
SHACL is the bouncer
The fix is a second W3C standard that was designed for this job. SHACL, which became a W3C Recommendation on July 20, 2017, validates an RDF data graph against a shapes graph full of conditions. Where OWL asks what else must be true, SHACL asks whether this data meets the rules. It’s much closer to what JSON Schema does, except it works on the meaning layer.
@prefix ex: <https://example.com/ont#> .
@prefix sh: <http://www.w3.org/ns/shacl#> .
ex:CustomerShape a sh:NodeShape ;
sh:targetClass ex:Customer ;
sh:property [
sh:path ex:accountManager ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:nodeKind sh:IRI ;
sh:message "A customer needs exactly one account manager." ;
] .
Validation produces a report. It carries sh:conforms, and each problem comes with the focus node that failed, the property path, and a severity. If the shape has an sh:message, the spec says it gets copied into every result as sh:resultMessage. That last detail is great for agents, because you can write messages aimed at the model and feed the report straight back as the reason its write was rejected.
One gotcha is worth knowing before you rely on class based targets. SHACL uses the RDF and RDFS vocabularies but doesn’t require RDFS inferencing. When it decides whether a node is an instance of a class, it follows rdfs:subClassOf triples, but those triples have to be in the data graph being validated. If your class hierarchy lives only in a separate ontology file, a shape targeting a parent class can quietly skip nodes that only have a subclass type. The spec allows inferred triples to be computed before or during validation, and the example below uses that to close the gap.
JSON-LD is the bridge
Agents produce JSON, not Turtle, and you don’t need to change that. JSON-LD 1.1 lets a @context map ordinary JSON keys to IRIs, and setting @vocab sends any plain key to your ontology’s namespace. So the agent’s tool output can look like normal JSON and still turn into proper triples on the way in.
In Python, RDFLib parses JSON-LD out of the box, and pySHACL (0.40.1 came out on July 28) does the validation. Its validate function returns a tuple of the conformance flag, the report as a graph, and the report as readable text. This is the whole gate:
from rdflib import Graph
from pyshacl import validate
agent_output = """{
"@context": {"@vocab": "https://example.com/ont#"},
"@id": "https://example.com/data/customer/42",
"@type": "Customer",
"accountManager": [
{"@id": "https://example.com/data/staff/jim"},
{"@id": "https://example.com/data/staff/james"}
]
}"""
data = Graph().parse(data=agent_output, format="json-ld")
conforms, report_graph, report_text = validate(
data, shacl_graph="shapes.ttl", ont_graph="ontology.ttl", inference="rdfs"
)
if not conforms:
print(report_text) # hand this back to the agent instead of saving
Run with the two files above, that prints a max count violation on customer 42 along with the custom message. The ont_graph argument mixes the ontology’s RDFS and OWL definitions into the data before validation, and inference="rdfs" turns on RDFS expansion, which the pySHACL README notes defaults to off. Mixing in the ontology is what takes care of the subclass gotcha from the last section, since the class hierarchy ends up inside the graph being validated.
Cost and safety
Reasoning isn’t free, and OWL gives you explicit tradeoffs. OWL 2 Full is undecidable, which is why OWL 2 DL exists as a restricted version that reasoners can fully support. The OWL 2 Profiles spec goes further with three trimmed down profiles. OWL 2 EL handles ontologies with huge numbers of classes in polynomial time. OWL 2 QL is built for lots of instance data and can answer queries by rewriting them for an ordinary relational database. OWL 2 RL targets rule engines and keeps its main reasoning tasks polynomial. If you expect agents to write a lot of data, my suggestion is to start with RDFS plus SHACL and add an OWL profile only when you have a real inference you need.
On the security side, remember that the graph becomes shared memory for every agent that reads it, so a bad write isn’t a local mistake anymore. Validate every write, and keep reads separate from writes. SPARQL makes that easy because SPARQL 1.1 Update is its own language, apart from SPARQL Query. Giving an agent a tool that runs queries but never updates, with timeouts and result limits on the endpoint, is a sensible default in my opinion.
Where the standards are heading
The stack is still moving. RDF 1.2 reached Candidate Recommendation in April 2026, and its headline addition is triple terms, which let a triple appear as the object of another triple. Being able to say which agent asserted a fact, and when, without awkward workarounds is a pretty natural fit for multi agent systems. The W3C Data Shapes Working Group also published its latest SHACL 1.2 Core working draft on August 28, so validation is getting attention too.
If your agents only ever call a couple of tools, JSON Schema is plenty. Once several agents share a memory or a knowledge graph, though, it’s worth writing down what your words mean in a form every agent and validator can read. Keep the ontology small, let SHACL guard the writes, and send the violations back to the model so it can fix its own mistakes.