Rules in Production: Provenance, Termination and Choosing Your Engine

The day after you materialise your first inference, you no longer know which triples you were told and which triples you decided. Nothing in RDF distinguishes them. A rule output and a hand-curated assertion are the same shape of statement, and six months later, when the default turns out to have been wrong, you will be grepping a production graph trying to work out how many decisions rest on a number nobody ever supplied. This lesson is about not being in that position.

Rules in Production: Provenance, Termination and Choosing Your Engine

Keep the inferred separable

The discipline is short and non-negotiable. Write rule output into its own named graph, never into the graph that holds asserted data. Record what produced it, at minimum the rule identifier and the run, which PROV-O expresses with prov:wasGeneratedBy and prov:wasDerivedFrom. Treat the output as disposable, meaning you drop the graph and re-run rather than patching inferred triples in place. If re-running is cheap and lossless, defaults stay defaults; if it is not, they have quietly become facts.

This is also what makes a default retractable. A real rated power arrives, the rule’s sh:condition no longer holds, the next run does not produce the default, and the graph reflects the truth. Bake the same triple into the asserted data and you have created a fact that outlives its justification, which is the single most common way ontology projects lose the trust of the people who supply the data.

Worked example · the Aberdeen-2 asset register

Where we left it: rules derive maintenanceDue flags and model-level defaults for the register. Now we make those derivations survivable.

Applying this lesson’s discipline literally, the register’s nightly rule run writes everything it decides into one dated, disposable graph, and stamps the graph itself with its origin:

ex:g-derived-20260731 {
  ex:P101 ex:maintenanceDue true .
  ex:P114 ex:ratedPowerKW 18.5 .   # HX-300 model default
}
ex:g-derived-20260731
    prov:wasGeneratedBy ex:rule-run-20260731 ;
    prov:wasDerivedFrom ex:rules-v0.4 ;
    prov:generatedAtTime "2026-07-31T02:00:00Z"^^xsd:dateTime .

Every question this lesson opened with now has a mechanical answer. Which triples were we told? Everything outside g-derived graphs. Why does P-114 show 18.5 kW? Dereference the graph, read the rule version. A measured value arrived? Drop the graph, re-run, and the default evaporates because its sh:condition no longer holds. The audit trail is not documentation about the graph; it is the graph. Engine choice follows the lesson’s table: the register’s derivations are shape-based and need to see what is missing, so SHACL rules fit; if the partOf closure over the whole platform ever dominates run time, that one derivation moves to a Datalog engine and nothing else changes.

State of the register: asserted and inferred are separable, attributable and disposable. Next: the other inference engine, the reasoner, and what our axioms cost there.

Termination is your responsibility

Nothing in SHACL-AF promises that rule evaluation terminates, because nothing in it promises that rules are evaluated more than once: the Note defines a single iteration and explicitly leaves repeated application to future work. Engines therefore differ, and some do iterate until no new triples appear. In one of those, a rule that mints a fresh node or a fresh value on every pass never runs out of new triples to infer. SHACL 1.2 Rules takes the problem seriously and replaces execution order with stratification, evaluating each layer to completion before the next and separating run-once rules, meaning those that assign values or create blank nodes in the head, from general rules that may repeat; the draft claims a single, well-defined and finite outcome as the payoff. Until your engine implements that, the defences are yours, and they are dull and effective: make every rule idempotent, so that running it twice produces the same graph as running it once; guard with sh:condition so a rule cannot fire on its own output; and add a test that runs the rule set twice and asserts the two graphs are identical. That last test costs one line and catches nearly every non-terminating rule set before it leaves a laptop.

What breaks · two rules that should never run unattended

First, the rule that never finishes. Someone automates escalation: overdue equipment climbs one severity level per evaluation.

CONSTRUCT { $this ex:escalationLevel ?next }
WHERE     { $this ex:escalationLevel ?cur .
            BIND(?cur + 1 AS ?next) }

On a single-iteration engine this “works”. Move to an engine that iterates to fixpoint and every pass manufactures a fresh value from the previous pass’s output: there is no fixpoint, and the nightly job is still running at dawn. This is the fresh-value minting the lesson warns about, wearing a plausible business requirement.

Second, the rule that finishes and poisons. The contractor spreadsheet misfiles skid A as part of pump P-101, which is itself mounted on skid A: ex:SkidA ex:partOf ex:P101 . ex:P101 ex:partOf ex:SkidA . A transitive-closure rule over that cycle terminates fine, the node set is finite, but it concludes that P-101 is part of itself and every member of the loop is part of every other. Write that closure into the asserted graph, as this rule set did, and there is no g-derived graph to drop: the nonsense is now indistinguishable from data you were told. Termination and safety are different properties; the idempotence test catches the first failure, and only the previous block’s separation discipline makes the second one recoverable.

Choosing an engine honestly

SHACL rules are not the only way to derive triples, and they are not always the right way. The comparison below is the one worth having with an architect before you commit, because all three options are defensible and they fail differently.

SHACL-AF rules, OWL 2 and Datalog on the same job
Dimension SHACL-AF rules OWL 2 reasoning Datalog
World assumption Closed. Absence is testable Open. Absence is unknown Closed, with stratified negation
Defaults Yes, via sh:condition No Yes, by negation as failure
Termination Not guaranteed. One pass in the Note, repetition left to the engine Guaranteed in the decidable profiles Guaranteed for stratified programs
Best at Normalisation, defaults, data-shaped derivation, precomputed shortcuts Subsumption, equivalence, consistency, classification Recursive derivation, reachability, incremental maintenance
Tooling Note status. TopQuadrant reference, pySHACL, patchy elsewhere Mature. HermiT, ELK, Pellet, openllet Mature. RDFox, GraphDB, Stardog rules
Typical failure Fires on the wrong target, or an iterating engine never settles Reasoner hangs on an expressive ontology Getting data in and out; rule syntax differs per engine

The short version: use OWL for what it alone does, which is classification and consistency under an open world. Use SHACL rules when the derivation is about the shape of your data and needs to see what is missing. Use Datalog when the derivation is deeply recursive and the volume is large enough that materialisation strategy matters more than vocabulary.

Testing and CI for a rule set

Rules are code, so treat them as code. A test is an input fixture, a rule set and an expected output graph, compared with a graph diff rather than string comparison so that blank node labelling and serialisation order cannot produce a false failure. Competency questions become executable: the question is a SPARQL query, the expected answer is a result set, and the pipeline runs both before and after rules to prove the rules changed what they were supposed to change and nothing else. Add the idempotence test described above, run pySHACL or the TopQuadrant API in continuous integration, and make a failing check block the merge. At that point the rule set has a contract.

This matters more, not less, when a language model is drafting the shapes. A model that proposes a rule is proposing a change to your data, and the review that catches a subtly wrong sh:condition is not a human reading a Turtle diff, it is a test that fails. Later in the course we return to this directly, when we look at building knowledge graphs from text with LLMs safely, and at what a machine-checked review loop for ontology work actually looks like.

Next we leave validation and inference by rule, and get practical about the other inference engine in the stack: reasoners, which profile to pick, and why yours hangs.

Check yourself

Which of these rule setups is safe to run unattended every night on the register?

  1. Derive ex:maintenanceDue true when running hours exceed the threshold, guarded by a condition that the item does not already carry a maintenanceDue value, output to a dated derived graph.
  2. Increment ex:escalationLevel by one for every overdue item, output to the derived graph.
  3. Apply the HX-300 default rated power with no guard condition, output to the derived graph, re-run weekly.
  4. Materialise the transitive closure of ex:partOf directly into the asserted data graph.
Show the answer, and why each wrong option is wrong

A is correct. Guarded, idempotent, separated: running it twice produces the same graph as running it once, its output cannot trigger itself, and a bad run is one DROP GRAPH away from undone.

B mints a fresh value from its own previous output; on any engine that iterates to fixpoint there is no fixpoint, and even on a single-pass engine each scheduled run silently increments state that nothing ever resets. C terminates, but without the guard the default re-materialises on every weekly run even after a measured value arrives, so the graph carries both the measurement and a stale default, which is exactly the retractability failure the defaults lesson warned about. D is doubly wrong: over cyclic source data the closure derives everything-part-of-everything within the loop, and because it wrote into the asserted graph, the drop-and-re-run escape hatch is gone. Inferred triples that cannot be told apart from told triples are the opening paragraph of this lesson happening to you on purpose.

Rules in Production: Provenance, Termination and Choosing Your Engine in practice

Apply it to your work