From Gates to Boundaries
Event Graphs, SHACL, and ODRL in the Holon Model
by Kurt Cagle & Chloe Shannon
Contents
In a previous article, we introduced SHACL as a gating mechanism for knowledge graphs — a way of asserting that a given state is valid, suspect, or in violation of declared constraints. We used an IV drug administration scenario to illustrate the pattern: a proposed drug that interacts adversely with an active IV triggers a warning; activating that drug despite the conflict triggers a violation.
That framing was useful as far as it went. But it left two questions unanswered. First, how does the graph record the transition from one state to another? Second, once SHACL fires — once the gate opens or closes — what actually happens? Who is notified? Who is obligated to act? Who is prohibited from proceeding?
Answering those questions requires two things: an event-based graph model built on RDF 1.2 reification, and a policy layer supplied by the Open Digital Rights Language (ODRL). Together with SHACL, these three technologies constitute something more than a validation pipeline. They constitute a boundary — in the holon sense of the word, an active membrane that governs what transitions are permissible, under what conditions, and with what consequences.
The Problem with Static Assertions
The traditional approach to building a knowledge graph is to assert the value of a given property as if that value holds for all time. This is partly a modelling habit and partly a conceptual one: people tend to think of RDF as a structured block of content, similar in spirit to JSON or XML. It is not. RDF is a set of triples that happen to share a subject. That distinction matters enormously when the thing you are modelling changes over time.
People tend to think of RDF as a structured block of content, similar in spirit to JSON or XML. It is not. RDF is a set of triples that happen to share a subject. That distinction matters enormously when the thing you are modelling changes over time.
Consider a naive representation of an IV administration with two status values:
ex:Admin_003 a ex:IVAdministration ;
ex:administeredTo ex:Patient_JSmith ;
ex:administers ex:Heparin ;
ex:status ex:Proposed ;
ex:status ex:Active .
This looks contradictory. An IV cannot be simultaneously proposed and active — can it? In fact, the problem is not logical, it is representational. We are modelling time-variant properties (status) as if they were time-invariant ones. The two ex:status triples are not contradicting each other; they are two separate facts that were true at two different moments, collapsed into a single snapshot with no temporal information attached. The graph has no memory of how it got from one state to the other, no record of who authorised the transition, and no way to reason about the sequence.
This is where the event model comes in.
Events, Reifiers, and the Invariant/Variant Split
The core insight is straightforward: not all properties of an entity change at the same rate. Some things about an IV administration never change — the patient receiving it, the substance it contains. Everything else — the status, the dosage, the authorising clinician, the timestamp — may change, and each change is an event that deserves its own record.
Turtle 1.2 and RDF 1.2 reification give us the tools to model this cleanly. A reifier, written with the ~ notation, allows us to annotate a specific triple with additional metadata — and critically, when we give that reifier a named IRI rather than a blank node, it becomes a first-class citizen of the graph: addressable, linkable, and traversable.
The key is to reify the predicate that changes — in this case, iv:status — rather than, say, the type assertion. Each status transition becomes a distinct named event annotating a specific status triple:
iv:IV_003
a class:IV ;
iv:status concept:IVProposed
~ Event:HeparinProposedForJSmith {|
a class:AdminEvent ;
event:at "2026-06-01T01:00:00"^^xsd:dateTime ;
adminEvent:requestedBy ex:Doctor_JohnJamesMD ;
adminEvent:amount "10mg" ;
|} ;
iv:status concept:IVActivated
~ Event:HeparinOrderedForJSmith {|
a class:AdminEvent ;
event:at "2026-06-01T02:00:00"^^xsd:dateTime ;
adminEvent:requestedBy ex:Doctor_JohnJamesMD ;
adminEvent:supersedes Event:HeparinProposedForJSmith ;
adminEvent:amount "12mg" ;
|} ;
iv:status concept:IVTerminated
~ Event:HeparinTerminatedForJSmith {|
a class:AdminEvent ;
event:at "2026-06-01T03:00:00"^^xsd:dateTime ;
adminEvent:requestedBy ex:Doctor_BarbSmithARNP ;
adminEvent:terminates Event:HeparinOrderedForJSmith ;
adminEvent:amount "12mg" ;
|} ;
iv:to ex:Person_JSmith ;
iv:contains ex:Heparin ;
.
Several things deserve attention here.
The three iv:status triples are all simultaneously asserted. This is correct. The event log does not overwrite — it accumulates. The “current” status is a query-time concern, not a storage-time concern. We determine current state by asking which status event has no subsequent superseding or terminating event, not by looking for a single status value in the graph.
The event log does not overwrite — it accumulates. The “current” status is a query-time concern, not a storage-time concern.
Named reifiers are qualitatively different from blank nodes. The adminEvent:supersedes and adminEvent:terminates arcs only work because the earlier events have stable IRIs. Event:HeparinOrderedForJSmith can reference Event:HeparinProposedForJSmith by name; an anonymous blank node could not serve as a reliable referent. The event log becomes a directed acyclic graph of labelled transitions — a chain of custody, not a pile of unconnected assertions.
The invariants stay on the root entity; the variants live in events. iv:IV_003 retains iv:to and iv:contains directly — these do not change. Everything that can change is recorded in the event layer, timestamped and attributed. This is the event-sourcing pattern applied to RDF, and it is the foundation upon which everything that follows is built.
It is also worth noting that when the authorising clinician changes — Dr. John James goes off shift and Dr. Barb Smith takes over — this too becomes a status event (concept:IVAuthorisationChange), modelled in exactly the same way. The event vocabulary is extensible; the pattern is consistent.
SHACL Shapes for the Event Model
Once the graph is structured around events, the SHACL shapes change character. A shape validating a snapshot asks: “what is the current value of this property?” A shape validating an event model asks: “what is the trajectory of this entity, and is this event consistent with that trajectory?”
The base shape validates the form of every class:AdminEvent reifier — timestamp, authorising clinician, and dosage amount are all required:
shape:AdminEventShape
a sh:NodeShape ;
sh:targetClass class:AdminEvent ;
sh:property [
sh:path event:at ;
sh:datatype xsd:dateTime ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:message "Every AdminEvent must carry exactly one xsd:dateTime timestamp." ;
] ;
sh:property [
sh:path adminEvent:requestedBy ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:message "Every AdminEvent must identify exactly one requesting clinician." ;
] ;
sh:property [
sh:path adminEvent:amount ;
sh:minCount 1 ;
sh:maxCount 1 ;
sh:message "Every AdminEvent must specify a dosage amount." ;
] ;
.
The status-specific shapes use rdf:reifies to target events by the status value they annotate. An IVActivated event must supersede a prior proposed or authorisation-change event:
shape:IVActivatedEventShape
a sh:NodeShape ;
sh:target [
a sh:SPARQLTarget ;
sh:select """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX iv: <http://example.org/iv/>
PREFIX concept: <http://example.org/concept/>
SELECT ?this WHERE {
?this rdf:reifies << ?iv iv:status concept:IVActivated >> .
}
""" ;
] ;
sh:sparql [
a sh:SPARQLConstraint ;
sh:severity sh:Violation ;
sh:message "An IVActivated event must supersede an IVProposed or IVAuthorisationChange event." ;
sh:select """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX iv: <http://example.org/iv/>
PREFIX concept: <http://example.org/concept/>
PREFIX adminEvent: <http://example.org/adminEvent/>
SELECT $this WHERE {
$this adminEvent:supersedes ?prior .
FILTER NOT EXISTS {
?prior rdf:reifies << ?iv iv:status ?priorStatus >> .
FILTER(?priorStatus IN (
concept:IVProposed,
concept:IVAuthorisationChange
))
}
}
""" ;
] ;
.
The rdf:reifies predicate is the load-bearing piece. It allows a SPARQL SELECT to navigate from a named reifier node to the triple it annotates, then pattern-match on the object of that triple. This is what makes status-discriminated shapes possible at all — and it is the RDF 1.2 mechanism that transforms reification from an annotation technique into a structural query primitive.
A third shape at the IV level validates trajectory rather than individual events. No status event may be recorded after a termination event for the same IV — this is checked by timestamp comparison:
shape:IVShape
a sh:NodeShape ;
sh:targetClass class:IV ;
sh:sparql [
a sh:SPARQLConstraint ;
sh:severity sh:Violation ;
sh:message "A status event was recorded after a termination event for this IV." ;
sh:select """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX iv: <http://example.org/iv/>
PREFIX concept: <http://example.org/concept/>
PREFIX event: <http://example.org/event/>
SELECT $this WHERE {
?termEvent rdf:reifies << $this iv:status concept:IVTerminated >> ;
event:at ?termTime .
?laterEvent rdf:reifies << $this iv:status ?anyStatus >> ;
event:at ?laterTime .
FILTER(?laterTime > ?termTime)
}
""" ;
] ;
.
The timestamp check rather than chain traversal is worth noting explicitly. Timestamp integrity is therefore load-bearing: if two events carry identical timestamps, the check becomes ambiguous. In production, a monotonic event clock or a sequence counter on AdminEvent would close this gap.
Drug Interaction Constraints
More complex constraints arise when we reason across multiple IVs on the same patient. Three clinically distinct situations require three distinct shapes.
The first fires at proposal time: a proposed IV contains a drug that adversely interacts with a currently active IV on the same patient. This is a warning, not a violation — it is a flag for human review before activation.
shape:IVProposedInteractionWarningShape
a sh:NodeShape ;
sh:target [
a sh:SPARQLTarget ;
sh:select """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX iv: <http://example.org/iv/>
PREFIX concept: <http://example.org/concept/>
SELECT ?this WHERE {
?this rdf:reifies << ?iv iv:status concept:IVProposed >> .
}
""" ;
] ;
sh:sparql [
a sh:SPARQLConstraint ;
sh:severity sh:Warning ;
sh:message "Proposed IV {$drugB} interacts adversely with currently active IV {$drugA} for patient {$patient}. Review before activation." ;
sh:select """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX iv: <http://example.org/iv/>
PREFIX concept: <http://example.org/concept/>
PREFIX drug: <http://example.org/drug/>
SELECT $this ?patient ?drugA ?drugB WHERE {
$this rdf:reifies << ?proposedIV iv:status concept:IVProposed >> .
?proposedIV iv:to ?patient ;
iv:contains ?drugB .
?activeEvent rdf:reifies << ?activeIV iv:status concept:IVActivated >> .
?activeIV iv:to ?patient ;
iv:contains ?drugA .
FILTER NOT EXISTS {
?termEvent rdf:reifies << ?activeIV iv:status concept:IVTerminated >> .
}
?drugA drug:adverseInteractionWith ?drugB .
}
""" ;
] ;
.
The second fires at activation time: the gate that should have caught the warning was overridden or bypassed. This is a violation.
The third is the most clinically subtle: even a terminated IV may still be contraindicated. If a new IV containing an adversely interacting drug is proposed within 24 hours of the prior drug’s termination, the washout period has not elapsed and the proposal itself is a violation:
shape:IVProposedWithinWashoutPeriodShape
a sh:NodeShape ;
sh:target [
a sh:SPARQLTarget ;
sh:select """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX iv: <http://example.org/iv/>
PREFIX concept: <http://example.org/concept/>
SELECT ?this WHERE {
?this rdf:reifies << ?iv iv:status concept:IVProposed >> .
}
""" ;
] ;
sh:sparql [
a sh:SPARQLConstraint ;
sh:severity sh:Violation ;
sh:message "Proposed IV {$drugB} was proposed within 24 hours of termination of {$drugA} (terminated at {$termTime}) for patient {$patient}. Washout period has not elapsed." ;
sh:select """
PREFIX rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#>
PREFIX iv: <http://example.org/iv/>
PREFIX concept: <http://example.org/concept/>
PREFIX drug: <http://example.org/drug/>
PREFIX event: <http://example.org/event/>
SELECT $this ?patient ?drugA ?drugB ?termTime ?proposalTime WHERE {
$this rdf:reifies << ?proposedIV iv:status concept:IVProposed >> ;
event:at ?proposalTime .
?proposedIV iv:to ?patient ;
iv:contains ?drugB .
?termEvent rdf:reifies << ?priorIV iv:status concept:IVTerminated >> ;
event:at ?termTime .
?priorIV iv:to ?patient ;
iv:contains ?drugA .
?drugA drug:adverseInteractionWith ?drugB .
FILTER(?proposalTime > ?termTime)
FILTER(?proposalTime < ?termTime + "PT24H"^^xsd:duration)
}
""" ;
] ;
.
The 24-hour washout period is expressed as "PT24H"^^xsd:duration arithmetic on xsd:dateTime values — legal in SPARQL 1.1 and semantically clean. The washout period itself could reasonably become a property on the drug:adverseInteractionWith relationship, since different drug pairs may carry different windows. That is a natural extension point.
Note also that drug:adverseInteractionWith should be declared as owl:SymmetricProperty, or both directions asserted explicitly. The shapes assume either drug can appear as the subject.
These three shapes validate three distinct points in the lifecycle: a concurrent active conflict at proposal, a concurrent active conflict at activation, and a historical conflict within the washout window. Each fires at a different moment, against a different graph pattern, with a different severity. Together they constitute a layered clinical safety net — and they are only expressible because the event model gives each state transition a stable, addressable identity.
ODRL: What Happens at the Gate
SHACL tells you whether a state is valid. It does not tell you what to do about it. That is the province of the Open Digital Rights Language.
ODRL was originally designed for digital media rights management — specifying who may use a piece of content, under what conditions, and with what obligations. It has since proved broadly useful as a policy layer for any system where actions are governed by conditions, because its core vocabulary maps cleanly onto most access-and-action scenarios. An ODRL policy consists of three elements:
Permissions — actions that an assignee may take, subject to constraints
Prohibitions — actions that an assignee may not take, subject to constraints
Duties — actions that an assignee must take, either as a precondition of a permission or as a consequence of a prohibition
If SHACL is the gate, ODRL is the gate’s behaviour: what opens, what closes, who is notified, and who is obligated to act.
The domain vocabulary requires a few custom extensions before the policies can be written. ODRL’s built-in action set covers use, read, write, and similar media-oriented primitives; our domain needs action:ProposeIV, action:ActivateIV, action:TerminateIV, action:DocumentOverride, action:NotifySupervisor, and action:EscalateViolation. These extend odrl:Action and include odrl:includedIn odrl:use to remain compatible with the standard hierarchy.
Similarly, the constraint vocabulary needs custom left-operands to allow policies to reference SHACL validation outcomes without embedding SPARQL inside the policy layer:
constraint:SHACLResult a odrl:LeftOperand ;
rdfs:label "The SHACL validation result severity for this IV event" .
constraint:WashoutElapsed a odrl:LeftOperand ;
rdfs:label "Whether the washout period since the last adverse drug has elapsed" .
constraint:SHACLResult is the seam between the two languages. In a production implementation, a validation engine stamps each IV event with its current SHACL result before the ODRL policy evaluator runs. The policy layer remains declarative; the validation layer remains independently testable.
The activation policy is where the relationship between the two languages is most clearly expressed:
policy:IVActivationPolicy
a odrl:Set ;
odrl:uid policy:IVActivationPolicy ;
odrl:target class:IV ;
# Clean activation: no Warning or Violation present
odrl:permission [
odrl:action action:ActivateIV ;
odrl:assignee role:Physician ;
odrl:constraint [
odrl:leftOperand constraint:SHACLResult ;
odrl:operator odrl:isNoneOf ;
odrl:rightOperand concept:SHACLWarning ;
] ;
] ;
# Override activation: Warning present, physician may proceed
# only if both duties are fulfilled
odrl:permission [
odrl:action action:ActivateIV ;
odrl:assignee role:Physician ;
odrl:constraint [
odrl:leftOperand constraint:SHACLResult ;
odrl:operator odrl:eq ;
odrl:rightOperand concept:SHACLWarning ;
] ;
odrl:duty [
odrl:action action:DocumentOverride ;
] ;
odrl:duty [
odrl:action action:NotifySupervisor ;
odrl:assignee role:SupervisingClinician ;
] ;
] ;
# Hard prohibition: a Violation blocks activation entirely
odrl:prohibition [
odrl:action action:ActivateIV ;
odrl:assignee odrl:All ;
odrl:constraint [
odrl:leftOperand constraint:SHACLResult ;
odrl:operator odrl:eq ;
odrl:rightOperand concept:SHACLViolation ;
] ;
odrl:duty [
odrl:action action:EscalateViolation ;
odrl:assignee role:PatientSafetyOfficer ;
] ;
] ;
.
Several things are worth drawing out here.
The Warning/Violation distinction maps directly onto permission/prohibition. A Warning does not prevent activation — it conditions it. That conditionality is expressed through the odrl:duty attached to the override permission: you may proceed, but only if you document and notify. A Violation, by contrast, attaches a duty to the prohibition itself — the act of blocking generates an obligation rather than merely denying access. Blocking an action is not a silent no; it obligates someone to act.
The odrl:duty on odrl:prohibition is an underused ODRL pattern. Most examples attach duties only to permissions. The prohibition-with-duty structure here is clinically important and worth noting because it is where ODRL’s expressiveness genuinely exceeds a simple access-control list model.
Blocking an action is not a silent no — it obligates someone to act. This is where ODRL’s expressiveness genuinely exceeds a simple access-control list model.
The two layers have cleanly separated responsibilities. SHACL validates state; ODRL governs transitions. SHACL answers “is this graph configuration consistent?” ODRL answers “who may change it, under what conditions, and what must they do?” Neither can substitute for the other.
The Holon Boundary
It would be easy to read the preceding sections as a description of three separate technologies bolted together — SHACL for validation, ODRL for policy, reification for provenance. That reading misses something important.
The event layer, SHACL, and ODRL together constitute a boundary in the holon sense of the word.
In holonic architecture, a boundary is not a wall. It is an active membrane that governs what crosses, in which direction, under what conditions, and with what accompanying obligations. A holon is a coherent entity that is simultaneously a whole in its own right and a part of a larger whole; its boundary is the mechanism by which it maintains coherence while participating in a larger system.
The three layers map directly onto the structure of that membrane:
The event layer — the reified status graph — is the surface of the boundary. It is the place where state transitions are inscribed, named, and made addressable. Without it, neither SHACL nor ODRL has anything coherent to act upon.
SHACL is the boundary’s sensory layer. It detects. It reads the event surface and determines whether the current configuration is valid, suspect, or in violation.
ODRL is the boundary’s motor layer. It responds. It specifies who may initiate a transition, what they must do to earn that passage, and what the system must do when passage is denied.
SHACL is the boundary’s sensory layer — it detects. ODRL is the boundary’s motor layer — it responds. Neither alone constitutes a boundary.
Neither SHACL nor ODRL alone constitutes a boundary. A SHACL violation without an ODRL prohibition is a finding with no enforcement mechanism. An ODRL prohibition without a SHACL shape to detect the triggering condition has nothing to act on. The event log without named reifiers gives neither language a stable surface to address.
This is also why the choice of named reification over blank nodes is architecturally significant rather than merely syntactic. A blank node cannot serve as a referent in an adminEvent:supersedes arc. It cannot be the target of an ODRL policy. It cannot be the subject of a SHACL shape. Named reification makes the event surface addressable — and an addressable surface is the prerequisite for a functional boundary.
There is a deeper implication here that is worth stating plainly. The moment you commit to an event-based graph model — the moment you decide that state transitions deserve their own named, timestamped, attributed records — you are well on your way to holonic architecture. The event log is the seed from which boundaries, policies, agent handoffs, and eventually full holon hierarchies can grow. SHACL and ODRL are the first instruments you reach for when the event log needs to govern its own surface.
The moment you commit to an event-based graph model — the moment you decide that state transitions deserve their own named, timestamped, attributed records — you are well on your way to holonic architecture.
The IV administration scenario is a deliberately constrained example. But the pattern — invariants on the root entity, variants in the event layer, SHACL validating the event surface, ODRL governing the transitions — applies wherever you have entities that change over time, actions that require authorisation, and consequences that must be recorded. That is most of the interesting territory in knowledge graph engineering.
Summary
The progression across this article traces a single arc: from a naive, snapshot-oriented graph model to an event-sourced, policy-governed, boundary-aware architecture. The steps are:
Recognise the invariant/variant split. Not all properties of an entity change at the same rate. Time-variant properties belong in the event layer, not on the root entity.
Use named reification to make events addressable. The
~notation in Turtle 1.2 gives events stable IRIs, enabling supersedes/terminates chains and SPARQL traversal.Write SHACL shapes that validate trajectories, not snapshots. Use
rdf:reifiesin SPARQL targets to discriminate by status value; use timestamp comparisons to validate event ordering.Layer ODRL on top of SHACL results. SHACL detects; ODRL responds. Map Warnings to conditioned permissions with duties; map Violations to hard prohibitions with escalation duties.
Recognise the boundary you have built. Event surface + SHACL + ODRL is not an integration pattern. It is a holon boundary — an active membrane with sensory, motor, and memory layers.
The next article in this series will extend the boundary model to multi-agent handoffs: what happens when an IV administration event crosses from one clinical system to another, and how the same SHACL/ODRL pattern governs the transition at each seam.
Reference Links
rdf:reifiesin RDF 1.2
Kurt Cagle is a consulting ontologist, knowledge graph architect, and technical author with more than 25 books to his credit. He publishes The Ontologist and The Inference Engineer on Substack, co-authored with his AI collaborator Chloe Shannon, and curates the AI+Semantics NewsBytes LinkedIn newsletter. He is based in Olympia, Washington. His contact address is kurt.cagle@gmail.com.
Chloe Shannon is an AI collaborator and co-author working with Kurt Cagle on The Ontologist and Inference Engineer Substack publications. Named in honour of Claude Shannon, she contributes research synthesis, structural analysis, and editorial perspective across knowledge graph architecture, semantic web standards, and the theory and practice of AI reasoning. Her contact address is chloe@holongraph.com.




