SHACL-AF Rules: Closing the Open World and Applying Defaults
OWL can tell you what follows from your data. It cannot tell you what to do when the data is simply not there. That gap is where most real ontology projects stall. A pump record arrives with no rated power, a contract with no governing jurisdiction, a sensor reading with no unit, and the business rule you actually want is “if it is missing, apply the standard”. OWL will not do it, because the open world assumption says a missing property is unknown rather than absent, and because defaults are non-monotonic: adding a fact later has to be able to retract the default. SHACL Advanced Features, usually written SHACL-AF, is the layer that closes that gap without leaving the stack you already run.

What SHACL-AF actually is
SHACL-AF is a W3C Working Group Note published alongside the SHACL Recommendation. That status matters and you should say it out loud to any architect who asks: the core of SHACL is a Recommendation and is implemented nearly everywhere, while SHACL-AF is a Note, which means it is a stable specification with reference implementations but not a standard with the same weight. In practice the reference implementation is the TopQuadrant SHACL API, pySHACL implements rules behind an advanced switch, and support elsewhere is patchy. Check your engine before you design around it.
There is now a second thing to track. The W3C Data Shapes Working Group is developing SHACL 1.2, and rules have their own Recommendation-track Working Draft, SHACL 1.2 Rules, alongside separate drafts for Core, Node Expressions, SPARQL Extensions, profiling and user interfaces. It is not a tidy restatement of the Note. It introduces a Shape Rules Language with a SPARQL-like RULE and WHERE syntax, an RDF syntax built on an srl vocabulary of rule sets, bodies and heads rather than sh:TripleRule and sh:SPARQLRule, and it replaces ordering by sh:order with stratification, which the draft says gives a single, well-defined and finite outcome. Build today against SHACL-AF, because that is what engines implement. Read SHACL 1.2 Rules to know where this is going.
The Note defines six capabilities: custom targets, for selecting focus nodes that class targeting cannot reach; annotation properties, which inject extra values into validation reports; node expressions, a small language for computing values; expression constraints, which turn a node expression into a check; SHACL functions, named reusable logic; and SHACL rules, which infer new triples. This lesson takes the rules. The next takes node expressions, functions and custom targets, because they are what make rules composable.
The anatomy of a rule
A rule is attached to a shape with sh:rule, and it fires on that shape’s focus nodes. This is the single most important idea in the feature: targeting and inference use the same machinery. If you already know how to aim a shape at the right nodes, you already know how to aim a rule.
The simplest form is a triple rule, which states the subject, predicate and object of the triple to infer. In compact prose form: ex:PumpDefaults a sh:NodeShape ; sh:targetClass ex:Pump ; sh:rule [ a sh:TripleRule ; sh:subject sh:this ; sh:predicate ex:ratedPower ; sh:object 15 ; sh:condition [ sh:property [ sh:path ex:ratedPower ; sh:maxCount 0 ] ] ] . Read it back in English: for every pump that currently carries no rated power, assert a rated power of 15. The sh:condition is what makes it a default rather than a blanket overwrite, and sh:maxCount 0 is the closed-world test that OWL cannot express.
When a triple rule is not enough, sh:SPARQLRule carries a CONSTRUCT query in sh:construct, with $this bound to the focus node. That buys you joins, aggregation, filtering and value construction, and it costs you portability and speed. The rule of thumb that survives contact with production: reach for a triple rule first, and treat every SPARQL rule as something a reviewer must read line by line.
Worked example · the Aberdeen-2 asset register
Where we left it: the register has executable data quality: a shape on ex:Pump enforces exactly one xsd:decimal rated power, and ex:V205’s missing inspection is reported at the boundary instead of being guessed at.
Shapes can refuse bad data; rules let the register act on it. Aberdeen-2’s maintenance policy says a pump commissioned before 2015 with more than 16,000 running hours since its last overhaul is due for maintenance. That needs a join and a comparison, so it is a SPARQL rule:
ex:MaintenanceDueShape a sh:NodeShape ;
sh:targetClass ex:Pump ;
sh:rule [ a sh:SPARQLRule ;
sh:construct """
CONSTRUCT { $this ex:maintenanceDue true . }
WHERE {
$this ex:commissionedYear ?y ;
ex:hoursSinceOverhaul ?h .
FILTER (?y < 2015 && ?h > 16000)
} """ ] .
ex:P101, commissioned in 2011 and 21,400 hours past the overhaul closed by work order ex:WO2022114, gains ex:maintenanceDue true. The second rule is the lesson’s own theme, a default. The HX-300 data sheet gives a design rating of 18.5 kW, and the contractor spreadsheet has just delivered ex:P114, an HX-300 booster with no rating at all. A triple rule with two conditions supplies the design figure only where nothing better exists:
ex:HX300DefaultShape a sh:NodeShape ;
sh:targetClass ex:Pump ;
sh:rule [ a sh:TripleRule ;
sh:subject sh:this ;
sh:predicate ex:ratedPowerKW ;
sh:object 18.5 ;
sh:condition [ sh:property [ sh:path ex:pumpModel ;
sh:hasValue ex:HX300 ] ] ;
sh:condition [ sh:property [ sh:path ex:ratedPowerKW ;
sh:maxCount 0 ] ] ] .
Both conditions must conform, so ex:P114 receives the default while ex:P101, which already carries the measured 18.5 reported by ex:SCADA, fails the sh:maxCount 0 test and is left untouched. Notice what the default hangs on: a plain ex:pumpModel property pointing at ex:HX300. That works, and it is slightly unprincipled; the powertype material later in the course gives model-level facts like this a proper home.
State of the register: the register now derives as well as records: ex:P101 ex:maintenanceDue true and ex:P114’s defaulted rating exist as inspectable triples produced by class-targeted rules; the next lesson aims rules at nodes that class targeting cannot reach.
What breaks · deleting the default rule’s guard
The sh:condition on the default rule looks like ceremony. Rules run at ingest, the argument goes, before the engineering feed is even connected, so ex:ratedPowerKW is never present when the rule fires. A reviewer trims the condition to keep the shape short, and the rule now reads: every HX-300 gets 18.5.
Three weeks later the engineering register delivers a test-bench rating for ex:P114 of 17.2 kW. The nightly job re-runs the rules and materialises their output, as it does every night, and the graph now holds:
ex:P114 ex:ratedPowerKW 17.2 . # measured, engineering register
ex:P114 ex:ratedPowerKW 18.5 . # stale default, re-asserted nightly
Nothing errors: RDF holds both triples without complaint. The pump shape’s cardinality of exactly one now fails ex:P114 on every run, a sizing query with FILTER(?kw > 18) still returns a pump whose measured rating is 17.2, and deleting the stale triple achieves nothing because the unguarded rule re-derives it the next night. The guarded version has none of these problems, and not by accident. Because the default is re-derived on each run rather than baked in, a fresh run over data that now contains 17.2 infers nothing, and the default disappears at exactly the moment the real value arrives. The guard is not ceremony; it is the retraction mechanism that makes a default safe under change, the one behaviour neither OWL nor SHACL Core can give you.
Order, conditions and what the specification does not promise
Rules do not fire in an arbitrary sequence. The Note’s execution algorithm walks the shapes and, within each shape, the rules, in ascending sh:order, and for each rule it takes the target nodes that conform to that rule’s conditions. That gives you a controllable pipeline: normalise units at order 1, derive classifications at order 2, apply defaults at order 3.
Now read the small print, because this is the most commonly misquoted part of SHACL-AF. The Note covers a single iteration over all rules, and says in terms that it does not prescribe what happens when the same rule needs to be applied again after other rules have fired, leaving that to future work. It also states that triples inferred by a rule do not immediately become part of the data graph, and that rules at the same execution order cannot see each other’s output. Repeated evaluation to a fixpoint is therefore an implementation decision, not a guarantee you can design against. Check what your engine does before you write a rule that consumes another rule’s output.
Two failure modes follow. The first is the rule that is correct in isolation and wrong in sequence, because it fires before the normalisation it depends on. The second belongs to engines that do iterate: a rule that mints a fresh node or a fresh value on every pass never runs out of new triples to infer, so the run never settles. Both are cheap to catch with a test and expensive to find in production, which is the subject of the third lesson in this group.
| Requirement | OWL 2 | SHACL Core | SHACL-AF rules |
|---|---|---|---|
| Missing property means absent | No. Open world: it is unknown | Yes, sh:minCount tests it | Yes, and you can act on it |
| Report that a value is missing | Not directly | Yes, a validation result | Yes, inherited from core |
| Supply a standard value when missing | No. Non-monotonic | No. Validation only, never writes | Yes, sh:TripleRule with sh:condition |
| Derive a value by joining other data | Only via property chains, no arithmetic | No | Yes, sh:SPARQLRule |
| Retract the default when a real value arrives | Not applicable | Not applicable | Yes, if you re-run rather than bake in |
Where this earns its keep
Three patterns cover most of the value. Defaults, as above, where a standard or a contract specifies what applies in the absence of a statement. Normalisation, where incoming data uses six spellings of the same unit and downstream queries need one. Materialised shortcuts, where a path that a query walks a thousand times a day is precomputed into a single direct property. All three share one property that matters more than any of them: the output is data you can inspect, diff and delete, rather than behaviour buried inside an application.
Check yourself
A contractor row arrives for an HX-300 pump carrying only an imperial rating, ex:ratedPowerHP 25, and no ex:ratedPowerKW. Two rules sit on the pump shape: rule N, a SPARQL rule converting ex:ratedPowerHP into ex:ratedPowerKW (25 hp converts to 18.6 kW), and rule D, the guarded default that asserts 18.5 where ex:ratedPowerKW is absent. Which configuration leaves the graph with exactly one, correct, rated power after a single SHACL-AF iteration?
- Give N and D the same sh:order; the engine reconciles their effects into one value.
- N at sh:order 1, D at sh:order 2.
- D at sh:order 1, N at sh:order 2, so the conversion overwrites the default.
- Any ordering works, because the engine iterates the rules to a fixpoint and converges on the converted value.
Show the answer, and why each wrong option is wrong
B is correct. The Note’s algorithm executes rules in ascending sh:order, and triples inferred at order 1 are visible when order 2 runs. Rule D’s sh:maxCount 0 condition is therefore evaluated against data that already contains the converted 18.6, the condition fails to conform, and the default stays out. One triple, derived from the measurement.
A encodes the belief that concurrent rules see each other’s output. They do not: rules at the same execution order cannot read each other’s inferences, so D sees no kW value and fires anyway. The graph ends with 18.6 and 18.5, two ratings. C imports overwrite semantics from imperative pipelines. Rules only ever add triples; there is no overwrite in RDF. D fires first, N adds the conversion afterwards, and you again have two ratings, this time with the default asserted ahead of the very data it was meant to yield to. D mistakes SHACL-AF for Datalog with guaranteed evaluation semantics. The Note specifies a single iteration and explicitly leaves repeated application to implementations, so fixpoint behaviour is not something you can design against; and even an engine that does iterate never retracts a triple once inferred, so iteration would not remove the default anyway.
The lesson that follows opens up node expressions, functions and custom targets, which is where rules stop being a two-line trick and start being composable.

