Chapter 10: Automatic Alerts
It was 8 PM. Dr. Portbridge was still at the clinic, manually cross-referencing vaccination records with appointment dates to find overdue animals. “There has to be a way to automate this,” she muttered. She needed the system to reason, to look at data and draw conclusions.
The problem
So far, our ontology describes things but doesn’t infer anything. If an animal hasn’t been vaccinated in over a year, a human has to notice. If an appointment is marked as an emergency but assigned to an intern, nobody catches it. We want the system to derive new facts from existing ones.
Rules
A rule defines an if-then inference: if certain patterns hold in the data, then new facts follow.
rule flag_unvaccinated:
match:
?animal a Animal
count ?shot as ?shots
?animal vaccinations ?shot
?shots [ = 0 ]
then:
?animal a UnvaccinatedAnimal
Note: This is a simplified example. Real vaccine-overdue logic would compare the
givenOndate andvalidForduration from Chapter 9, date arithmetic. The important thing here is the pattern: match conditions, then assert conclusions.
Let’s unpack the syntax.
Variables
Variables start with ?. They bind to values during pattern matching:
?animal: binds to any Animal instance
Patterns
Each line in the match: block is a pattern:
| Pattern | Meaning |
|---|---|
?animal a Animal | ?animal is an instance of Animal |
count ?shot as ?shots | ?shots is the number of ?shot values the indented lines below it find for this ?animal (0 when there are none) |
?animal vaccinations ?shot | (indented, inside the count) ?shot is one of ?animal’s vaccinations |
?shots [ = 0 ] | that number is 0 |
Patterns are combined with implicit AND. All must hold simultaneously.
Assertions
Each line in the then: block is an assertion, a new fact to create:
| Assertion | Meaning |
|---|---|
?animal a UnvaccinatedAnimal | Classify ?animal as an UnvaccinatedAnimal |
A more practical example
Let’s flag emergency appointments that are assigned to interns:
concept UnsafeAssignment
rule flag_intern_emergency:
match:
?appt a Appointment
?appt urgency Emergency
?appt animal [ treatedBy [ a Intern ] ]
then:
?appt a UnsafeAssignment
This says: “If an appointment is an Emergency, and the treating vet is an Intern, flag it as an UnsafeAssignment.”
Inferring new relationships
Rules don’t just classify, they can create relationships:
rule assign_primary_vet:
match:
?animal a Animal
?animal owner [ preferredVet ?vet ]
then:
?animal treatedBy ?vet
“If an animal’s owner has a preferred vet, assign that vet to the animal.”
(This requires adding preferredVet to Owner, we’ll do that below.)
To follow a chain of relationships of any length (parentOf+), see
property paths.
Weight-based alerts
Here’s a rule using numeric comparison:
concept OverweightAnimal
rule flag_overweight_dog:
match:
?dog a Dog
?dog weight [ > 40.0 ]
then:
?dog a OverweightAnimal
Worked example
Suppose the clinic records two dogs:
fact rex a Dog
name "Rex"
species Dog
weight 45.0
neutered true
fact buddy a Dog
name "Buddy"
species Dog
weight 22.0
neutered true
When the rules run, the reasoner walks every Dog, checks the weight constraint, and derives one new fact:
rex a OverweightAnimal # 45.0 > 40.0 → flagged
# buddy is left untouched # 22.0 > 40.0 → does not match
The derived rex a OverweightAnimal triple is added to the graph alongside the data you wrote. Nothing in the original facts changes. A rule only ever adds facts.
Classifying by an exact value
Comparisons aren’t the only test. A pattern can match an attribute against a literal value directly (a boolean, an enum member, a string, a date, or a date-time):
concept NeedsNeutering
rule flag_unneutered_dog:
match:
?dog a Dog
?dog neutered false
then:
?dog a NeedsNeutering
“Any dog whose neutered flag is false is tagged NeedsNeutering.”
Where rules run
These rules aren’t just documentation. When a Dolfin package is deployed in Agrafe, its rules are compiled and handed to the reasoner (retox). Every time data is written, the reasoner forward-chains the rules to a fixpoint, and the derived triples (rex a OverweightAnimal, …) become queryable through the deployment’s SPARQL API right next to the data you wrote.
Counting, summing, averaging
A rule can aggregate a group of values, as flag_unvaccinated above counts vaccinations. An aggregate line (count, count distinct, sum, average, min or max) names the aggregated variable and the result, and its indented lines say what to aggregate for each match of the lines above it. The result is used on later lines and in then::
concept BusyOwner:
sub Owner
has animal_count: int
rule flag_busy_owner:
match:
?owner a Owner
count ?pet as ?n
?pet owner ?owner
?n [ > 5 ]
then:
?owner a BusyOwner
?owner animal_count ?n
“An owner of more than 5 animals is a BusyOwner, with the count recorded.” A count over no value is 0 (an owner with no animal has ?n = 0); sum, average, min and max over no value have no result, so the rule does not fire. A rule may aggregate over what another rule infers (?pet a UnvaccinatedAnimal): it runs once that rule has finished. Two rules aggregating over each other’s conclusions in a loop have no answer and are refused, naming the rules.
How each backend runs aggregate rules:
| Backend | count, count distinct | sum, average, min, max |
|---|---|---|
SHACL-AF (radeau validate, the validation of an Agrafe deployment) | run | run |
N3 output (radeau build --emit n3-rules) | written as N3 (log:collectAllIn + list:length) | written as a # rule <name>: aggregate, runs as SHACL-AF only comment |
| retox (the reasoner) | run, after the rules deriving their input | not run (reported, see above) |
| Agrafe deployment (SPARQL queries) | inferred triples are queryable | listed among the deployment’s skipped constructs: queries do not see them |
A plain count whose indented lines bind another new variable (?pet owner ?owner then ?pet weight ?w) counts a value once per row, which N3 cannot say: like sum, it runs as SHACL-AF only (write count distinct to count each value once). The editor warns about every rule that runs as SHACL-AF only (S020).
The story so far
# package.dlf
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "0.1.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
# clinic.dlf
concept Species:
one of:
Dog
Cat
Bird
Rabbit
Reptile
Other
concept Urgency:
one of:
Routine
Urgent
Emergency
concept AppointmentStatus:
one of:
Scheduled
InProgress
Completed
Cancelled
concept Owner:
has firstName: one string
has lastName: one string
has phoneNumbers: at least 1 string
has email: optional string
has address: optional string
has preferredVet: optional Veterinarian
concept Veterinarian:
has name: one string
has licenseNumber: one string
has specialization: optional string
concept Surgeon:
sub Veterinarian
has surgeryCount: one int
has certifiedProcedures: at least 1 string
concept Dentist:
sub Veterinarian
has dentalCertification: one string
concept Intern:
sub Veterinarian
has university: one string
has year: one int
concept Vaccination:
has vaccineName: one string
has dateAdministered: one string
has batchNumber: optional string
concept Animal:
has name: one string
has species: one Species
has age: optional int
has weight: optional float
has owner: optional Owner
has vaccinations: Vaccination
has allergies: string
concept Dog:
sub Animal
has breed: optional string
has neutered: one boolean
concept Cat:
sub Animal
has indoor: one boolean
concept Bird:
sub Animal
has wingspan: optional float
has canFly: one boolean
concept Appointment:
has animal: one Animal
has scheduledFor: one date_time
has reason: one string
has urgency: one Urgency
has status: one AppointmentStatus
has diagnosis: optional string
has treatments: string
has notes: optional string
property treatedBy: Animal -> Veterinarian
# Derived concepts (created by rules)
concept UnvaccinatedAnimal
concept UnsafeAssignment
concept OverweightAnimal
# Rules
rule flag_unvaccinated:
match:
?animal a Animal
count ?shot as ?shots
?animal vaccinations ?shot
?shots [ = 0 ]
then:
?animal a UnvaccinatedAnimal
rule flag_intern_emergency:
match:
?appt a Appointment
?appt urgency Emergency
?appt animal [ treatedBy [ a Intern ] ]
then:
?appt a UnsafeAssignment
rule flag_overweight_dog:
match:
?dog a Dog
?dog weight [ > 40.0 ]
then:
?dog a OverweightAnimal
rule assign_primary_vet:
match:
?animal a Animal
?animal owner [ preferredVet ?vet ]
then:
?animal treatedBy ?vet
Try it
Write a rule that classifies a Cat as a SeniorCat if its age is greater than or equal to 10:
concept SeniorCat
rule flag_senior_cat:
match:
# your patterns here
then:
# your assertion here
The alerts were a revelation. The system caught an intern assigned to an emergency before it became a problem. It flagged three overweight dogs whose owners hadn’t noticed the gradual change. But when Dr. Portbridge looked closely at that last rule,
weight [ > 40.0 ], a doubt crept in. Forty what? A number with no unit was a number she couldn’t trust, and one threshold couldn’t possibly fit a parakeet and a mastiff alike. Before she added any more rules, the weights themselves needed to mean something.