Introduction
What is Dolfin?
Dolfin is a language for modelling knowledge.
You use it to describe the concepts in a domain, the relationships between them, the constraints they must satisfy, and the rules that can be inferred from them. The result is an ontology: a precise, human-readable definition of what exists in your world and how the pieces fit together.
Dolfin is not tied to any particular storage backend or runtime. The model you write is independent of whether your data lives in a graph database, a relational database, a document store, or something else entirely. Dolfin describes what your data means; the implementation decides where and how it is stored and queried.
This separation is what makes ontologies powerful. The same model can drive validation in one system, feed a reasoning engine in another, and serve as shared vocabulary across a team, all without being rewritten.
What is an ontology?
The word ontology comes from philosophy. It’s the study of what exists. In software, an ontology is a formal description of the concepts in a domain and the relationships between them.
Think of it like a schema, but with more expressive power:
| A database schema can says things like … | A Dolfin ontology can also says… |
|---|---|
An animal has a name column | An animal must have exactly one name |
An appointment has a vet_id foreign key | An intern cannot treat an emergency |
| — | An unvaccinated animal is automatically flagged |
| — | Animal in this model is the same concept as fao:Animal in the global species registry |
Dolfin lets you express all of that in a clean, readable file.
What you will build
This tutorial follows Dr. Helen Portbridge as she designs the data model for her new veterinary clinic, Happy Paws. Over ten chapters, starting from a blank file, you will build a complete ontology that:
- Describes animals, owners, appointments, and veterinary staff
- Enforces rules like “a surgeon must be on call for any surgery”
- Automatically flags at-risk or overdue patients
- Connects to external registries using standard IRIs
By the end, you will have touched every major feature of the language.
What Dolfin looks like
Here is a small taste. Don’t worry about the details — each piece will be introduced step by step.
concept Animal:
has name: one string
has species: one Species
has owner: optional Owner
has vaccinations: Vaccination
concept Dog:
sub Animal
has breed: optional string
has neutered: one boolean
rule flag_unvaccinated:
match:
?animal a Animal
?animal vaccinations 0
then:
?animal a UnvaccinatedAnimal
Dolfin is designed to be readable without training. A domain expert, a developer, and a data architect can all look at the same file and understand it.
How to read this tutorial
The tutorial is structured as a story. Each chapter opens with a short scene from the clinic, poses a new modelling problem, introduces the Dolfin feature that solves it, and ends with a prompt for the next problem.
You can read it cover to cover, or use it as a reference — the Reference section at the end is a complete description of the language.
Ready? The clinic opens in Chapter 1.
Chapter 1: Opening Day
Chapter 1: Opening Day
Dr. Helen Portbridge had been dreaming of this day for years. The sign on the door read Happy Paws Veterinary Clinic, the smell of fresh paint still lingered, and the reception desk was empty except for a brand-new laptop. Before any patient walked in, she needed a system. A way to describe every animal, every owner, every appointment that would ever pass through these doors.
She opened a text editor and typed:
Every Dolfin project begins with a package. Think of a package as the identity card of your ontology: its name, its version, who made it, and what it’s for.
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "0.1.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
Let’s unpack this line by line.
The package name
package <http://happypaws.com/clinic>:
The name uses IRI-notation. If Dr. Portbridge later builds a separate ontology for her research lab, she could call it http://happypaws.com/research and there would be no collision.
The colon (:) at the end is important. It opens an indented block. Everything that belongs to this package declaration must be indented underneath it, exactly like Python.
Metadata
dolfin_version "1"
version "0.1.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
| Field | Required | What it does |
|---|---|---|
dolfin_version | ✅ | Which version of the Dolfin language to use |
version | ✅ | Your ontology’s own version (semver) |
author | ❌ | A human name |
description | ❌ | A sentence explaining the purpose |
dolfin_version "1" tells the parser which grammar to expect. Right now there is only version 1, but including it means your file will still work when the language evolves.
Try it
Change the author to your own name and hit Check:
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "0.1.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
Common mistakes
Forgetting the colon:
package <http://happypaws.com/clinic>
dolfin_version "1"
Dolfin will tell you: “Did you forget a : after the package name?”
Inconsistent indentation:
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "0.1.0" # ← too deep!
All lines inside a block must be at the same indentation level.
Dr. Portbridge saved the file as
package.dol. A package with no concepts is like a clinic with no exam rooms, technically it exists, but it can’t do anything yet. She needed to describe the things that would populate her world.
Chapter 2: Meet the Animals
The first patient arrived before the furniture did: a nervous-looking golden retriever named Biscuit, dragged in by an equally nervous owner. Dr. Portbridge grabbed a pen and a napkin and started writing down what she needed to know. Name? Biscuit. Owner? Some guy. Species? Dog. Age? Maybe five?
She looked at the napkin and thought: “I can do better than this.”
A concept is the core building block of any Dolfin ontology. It describes a category of things, not a specific individual, but the shape that all individuals of that kind share.
Your first concept
concept Animal:
has name: string
has species: string
has age: int
This says three things:
- There is a concept called
Animal. - An Animal can have a
name, which is text. - An Animal can have a
species(also text) and anage(a whole number).
The keyword has introduces an attribute, a piece of data that instances of this concept can carry. After has comes the attribute name, then a colon, then its type.
No constraints yet. Right now, every attribute has cardinality “any”: zero values, one value, or fifty values are all legal. That’s intentional for a first sketch, but it does mean nothing prevents an Animal with no name or three species. In a later chapter, we’ll learn how to say “exactly one name” or “at most one owner.”
Primitive types
Dolfin ships with four primitive types:
| Type | What it holds | Examples |
|---|---|---|
string | Text | "Biscuit", "cat" |
int | Whole numbers | 42, 0, -3 |
float | Decimal numbers | 3.14, 36.6 |
boolean | True or false | true, false |
Adding the owner
An animal doesn’t walk into a clinic alone (well, cats might). We need an owner:
concept Owner:
has first_name: string
has last_name: string
has phone: string
Concepts can reference other concepts
Here’s the interesting part. An attribute’s type doesn’t have to be a primitive, it can be another concept:
concept Animal:
has name: string
has species: string
has age: int
has owner: Owner
has owner: Owner means: an Animal can be linked to an Owner. Not to a string containing the owner’s name, to the actual Owner concept, with all its attributes. This is how you build a graph of interconnected data, not just flat tables.
The story so far
Here’s what we have after this chapter:
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "0.1.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
concept Animal:
has name: string
has species: string
has age: int
has owner: Owner
concept Owner:
has first_name: string
has last_name: string
has phone: string
Try it
Add a weight attribute (as a float) to the Animal concept:
concept Animal:
has name: string
has species: string
has age: int
has owner: Owner
concept Owner:
has first_name: string
has last_name: string
has phone: string
Dr. Portbridge looked at the model and felt a little proud. But as she started typing in Biscuit’s details, she realized something: an appointment isn’t just an animal and an owner. It has a date, a reason, a diagnosis. The animal and the owner exist before the appointment and after it. She needed something to represent the visit itself.
Chapter 3: The Appointment Book
By noon, Dr. Portbridge had seen three patients. She’d scribbled notes on separate napkins, but already she couldn’t remember whether the cat with the limp came before or after the parrot with the cough. She needed a concept for the visit itself, something that ties an animal, a date, and a reason together.
Modeling the appointment
concept Appointment:
has animal: Animal
has date: string
has reason: string
has diagnosis: string
has treatment: string
Notice that animal is of type Animal, a reference to the concept we defined earlier. This is how relationships emerge naturally in Dolfin. You don’t need a separate “relationship” syntax for simple ownership; has does the job.
Why is
datea string? Good catch. Dolfin 1.0 has four primitive types and doesn’t yet include a built-in date type. For now, we use a string like"2025-01-15". In a later chapter, we’ll see how prefixes and IRIs let you link to external type systems likexsd:date.
Standalone properties
So far, we’ve defined relationships inside concepts using has. But some relationships are important enough to deserve their own definition, especially when they could apply to multiple concepts or when you want to give them metadata.
A property is a standalone, reusable relationship:
property treatedBy:
Animal -> string
This reads: “treatedBy is a relationship from Animal to a string.” The arrow (->) separates the domain (what has the property) from the range (what values it takes).
That string is a placeholder. In reality, we’d want a Veterinarian concept. Let’s make one:
concept Veterinarian:
has name: string
has license_number: string
property treatedBy:
Animal -> Veterinarian
Now treatedBy is a first-class relationship between Animal and Veterinarian, defined separately from either concept.
When to use has vs property
| Use case | Use has | Use property |
|---|---|---|
| Simple attribute (name, age) | ✅ | ❌ |
| Core part of a concept’s identity | ✅ | ❌ |
| Relationship shared across concepts | ❌ | ✅ |
| Relationship you want to annotate or reason about | ❌ | ✅ |
Both compile to OWL properties. The difference is readability and intent.
The story so far
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "0.1.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
concept Animal:
has name: string
has species: string
has age: int
has owner: Owner
concept Owner:
has first_name: string
has last_name: string
has phone: string
concept Appointment:
has animal: Animal
has date: string
has reason: string
has diagnosis: string
has treatment: string
concept Veterinarian:
has name: string
has license_number: string
property treatedBy:
Animal -> Veterinarian
Try it
Add a standalone property owns that goes from Owner to Animal. Then add a property scheduledWith from Appointment to Veterinarian:
concept Animal:
has name: string
has species: string
has age: int
has owner: Owner
concept Owner:
has first_name: string
has last_name: string
has phone: string
concept Appointment:
has animal: Animal
has date: string
has reason: string
has diagnosis: string
has treatment: string
concept Veterinarian:
has name: string
has license_number: string
property treatedBy:
Animal -> Veterinarian
# Add your properties here
The appointment book was taking shape. But as Dr. Portbridge typed
species: stringfor the tenth time, she winced. “Dog” could be typed as “dog”, “Dog”, “DOG”, “canine”, or “golden retriever”. A free-text field was an invitation for chaos. She needed a fixed list.
Chapter 4: Species and Breeds
The receptionist had entered “dgo” as the species for a patient. Then “Feline (domestic shorthair)”. Then just “bird”. Dr. Portbridge realized that a
stringfield for species was a liability, it offered infinite freedom where she needed a closed list.
The problem with strings
When a field accepts any string, you get inconsistency:
"Dog"vs"dog"vs"Canine""Cat"vs"Feline"vs"feline (domestic)"
Queries break. Reports are nonsense. You need a controlled vocabulary.
Closed concepts
A closed concept defines a fixed, exhaustive set of allowed values:
concept Species:
one of:
Dog
Cat
Bird
Rabbit
Reptile
Other
Now replace the string in Animal:
concept Animal:
has name: string
has species: Species
has age: int
has weight: float
has owner: Owner
species can only be one of the six values listed in the enum. No typos, no ambiguity, no “dgo”.
Multiple closed concepts
The clinic also needs to categorize appointment urgency:
concept Urgency:
one of:
Routine
Urgent
Emergency
And appointment status:
concept AppointmentStatus:
one of:
Scheduled
InProgress
Completed
Cancelled
Now Appointment becomes:
concept Appointment:
has animal: Animal
has date: string
has reason: string
has urgency: Urgency
has status: AppointmentStatus
has diagnosis: string
has treatment: string
Enums vs concepts
Enums and concepts are very different things:
| Enum | Concept | |
|---|---|---|
| Instances | Fixed at design time | Created at runtime |
| Has attributes | ❌ | ✅ |
| Can be extended | ❌ (closed list) | ✅ (via sub) |
| Use case | Dropdowns, categories | Real-world entities |
Use enums for things that won’t change or change rarely (statuses, categories, units). Use concepts for things that live and breathe (patients, people, appointments).
The story so far
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "0.1.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
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 first_name: string
has last_name: string
has phone: string
concept Veterinarian:
has name: string
has license_number: string
concept Animal:
has name: string
has species: Species
has age: int
has weight: float
has owner: Owner
concept Appointment:
has animal: Animal
has date: string
has reason: string
has urgency: Urgency
has status: AppointmentStatus
has diagnosis: string
has treatment: string
property treatedBy:
Animal -> Veterinarian
Try it
Add an enum PaymentMethod with values Cash, Card, Insurance, and Pending. Then add a payment attribute to Appointment:
concept PaymentMethod:
one of:
Cash
concept Appointment:
has animal: Animal
has date: string
has reason: string
has urgency: Urgency
has status: AppointmentStatus
The enums solved the typo problem. Then a cardboard box appeared on the exam table. Inside was a tiny stray kitten, no collar, no owner, no known age. Dr. Portbridge named her Pixel on the spot and tried to enter her into the system: species
Cat, name “Pixel”, and… that was it. No owner to reference, no age to record. The system accepted Pixel without complaint. In fact, it would have accepted an Animal with no name at all, or one with five species. Every attribute had cardinality “any”: no minimum, no maximum, no constraints whatsoever. That was fine for a first sketch, but now she needed precision. “Every animal MUST have exactly one name. An owner might not have an email. A phone number is required.” She needed cardinality.
Chapter 5: Tightening the Rules
Dr. Portbridge had been entering patient data all morning. Then she noticed: the system had accepted an Animal with no name. Another had two species. An Appointment existed with zero dates. Nothing prevented nonsense because every attribute had cardinality “any”, zero or more, no questions asked. The sketch had been useful, but now she needed precision.
The problem
So far, every has declaration is unconstrained. The default cardinality is “any”, meaning zero to infinity. That’s great for rapid prototyping, but a real system needs rules:
- An animal must have exactly one name
- A species is required, and there’s only one
- An age might be unknown (strays, for instance)
- An owner is not always present (think of Pixel, the stray kitten)
Dolfin lets you tighten these rules with cardinality keywords placed between the colon and the type.
Cardinality keywords
| Keyword | Meaning | Example |
|---|---|---|
any (default) | Any (0 to ∞), the default | has task: any Task |
one | Exactly one (required) | has name: one string |
optional | Zero or one | has age: optional int |
The keyword goes after the colon and before the type:
has <attribute>: <cardinality> <Type>
Applying cardinality to Animal
Let’s revisit our Animal concept with proper constraints:
concept Animal:
has name: one string
has species: one Species
has age: optional int
has weight: optional float
has owner: optional Owner
Now:
nameandspeciesare required: exactly one value, always presentage,weight, andownerare optional: they can be absent
Choosing the right cardinality
This is a modeling decision, not a technical one. Ask yourself: “How many of this attribute can an entity reasonably have? And how many must it have?”
| Attribute | Cardinality | Reasoning |
|---|---|---|
name | one | Every animal must have a name, even “Unknown” |
species | one | You always know if it’s a dog or a cat |
age | optional | Strays often have unknown ages |
weight | optional | Not always measured on first visit |
owner | optional | Strays exist |
Applying cardinality to Owner and Appointment
Let’s apply the same thinking to Owner:
concept Owner:
has first_name: one string
has last_name: one string
has phone: one string
has email: optional string
has address: optional string
And to Appointment:
concept Appointment:
has animal: one Animal
has date: one string
has reason: one string
has urgency: one Urgency
has status: one AppointmentStatus
has diagnosis: optional string
has treatment: optional string
has notes: string
A diagnosis and treatment are unknown when the appointment is first scheduled: they only get filled in during or after the visit.
Leaving an attribute unconstrained
If you omit the cardinality keyword, you get the default: any (zero or more, no upper limit). This is useful for attributes where you genuinely don’t know the bounds yet, or where any number of values is acceptable:
concept Appointment:
has notes: string
Here notes has no constraint: an Appointment can have zero notes, one note, or a hundred. That’s sometimes exactly what you want.
The story so far
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "0.1.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
concept Species:
only values:
Dog
Cat
Bird
Rabbit
Reptile
Other
concept Urgency:
only values:
Routine
Urgent
Emergency
concept AppointmentStatus:
only values:
Scheduled
InProgress
Completed
Cancelled
concept Owner:
has first_name: one string
has last_name: one string
has phone: one string
has email: optional string
has address: optional string
concept Veterinarian:
has name: one string
has license_number: one string
concept Animal:
has name: one string
has species: one Species
has age: optional int
has weight: optional float
has owner: optional Owner
concept Appointment:
has animal: one Animal
has date: one string
has reason: one string
has urgency: one Urgency
has status: one AppointmentStatus
has diagnosis: optional string
has treatment: optional string
has notes: optional string
property treatedBy:
Animal -> Veterinarian
Try it
The Veterinarian concept currently requires name and license_number. Add an optional specialization attribute (as a string) and an optional phone:
concept Veterinarian:
has name: one string
has license_number: one string
The model finally rejected nonsense. An Animal without a name? Error. An Appointment with no date? Error. Pixel, the stray kitten, was registered with no owner and no known age, and the system accepted her gracefully.
A week later, the clinic ran a vaccination drive. Thirty animals in one day. Dr. Portbridge needed to record which vaccines each animal had received, not one, not two, but a variable number. She tried adding
has vaccine: one stringbut that only held one. She needed a list.
Chapter 6: How Many Vaccines?
Biscuit the golden retriever needed three vaccines: rabies, distemper, and bordetella. Dr. Portbridge tried
has vaccine: one string, but that could only hold one value. She tried adding three separate fields,vaccine1,vaccine2,vaccine3, and immediately hated herself for it. What about animals that need five? Or none?
The problem
Sometimes an attribute holds more than one value. A single has vaccine: one string gives you exactly one. The real world isn’t that tidy.
Multi-valued cardinality
In the previous chapter, you learned one and optional. But Dolfin has richer cardinality keywords for multi-valued attributes:
| Keyword | Meaning | Example |
|---|---|---|
at least N | N or more values | has phone: at least 1 string |
between N M | Between N and M (inclusive) | has ref: between 2 5 Owner |
at most N | Between 0 and N (inclusive) | has parents: at most 2 Person |
exactly N | Exactly N values | has coord: exactly 3 float |
| (none) | Any (0 to ∞) | has tag: string |
Combined with the keywords from Chapter 5, here’s the full cheat sheet:
Cardinality cheat sheet
| Syntax | Meaning | Example use case |
|---|---|---|
one Type | Exactly one (required) | has name: one string |
optional Type | Zero or one | has nickname: optional string |
Type | Any (0 to ∞, the default) | has tag: string |
at least N Type | N or more | has phone: at least 1 string |
at most N Type | 0 to N (inclusive) | has parents: at most 2 Person |
between N M Type | Between N and M (inclusive) | has references: between 2 5 Owner |
exactly N Type | Exactly N | has coordinates: exactly 3 float |
Applying cardinality to the clinic
Let’s think about what needs multi-valued cardinality:
Animals can have multiple vaccines, and might have multiple allergies:
concept Animal:
has name: one string
has species: one Species
has age: optional int
has weight: optional float
has owner: optional Owner
has vaccines: string
has allergies: string
Here vaccines and allergies use the default “any” that is zero or more strings, no upper limit.
Owners must have at least one phone number but could have several:
concept Owner:
has first_name: one string
has last_name: one string
has phone_numbers: at least 1 string
has email: optional string
has address: optional string
Appointments might involve multiple treatments:
concept Appointment:
has animal: one Animal
has date: one string
has reason: one string
has urgency: one Urgency
has status: one AppointmentStatus
has diagnosis: optional string
has treatments: string
has notes: string
A concept for vaccines
Actually, a vaccine isn’t just a string. It has a name, a date administered, and a batch number. Let’s promote it to a concept:
concept Vaccination:
has vaccine_name: one string
has date_administered: one string
has batch_number: 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
Now each vaccination is a rich object, not just a name. And because vaccinations uses the default cardinality (“any”), an animal can have zero, one, or many vaccinations.
The story so far
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "0.1.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
concept Species:
only values:
Dog
Cat
Bird
Rabbit
Reptile
Other
concept Urgency:
only values:
Routine
Urgent
Emergency
concept AppointmentStatus:
only values:
Scheduled
InProgress
Completed
Cancelled
concept Owner:
has first_name: one string
has last_name: one string
has phone_numbers: at least 1 string
has email: optional string
has address: optional string
concept Veterinarian:
has name: one string
has license_number: one string
has specialization: optional string
concept Vaccination:
has vaccine_name: one string
has date_administered: one string
has batch_number: 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 Appointment:
has animal: one Animal
has date: one string
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
Try it
A Veterinarian can have multiple certifications (at least one) and speaks one or more languages. Add these attributes:
concept Veterinarian:
has name: one string
has license_number: one string
has specialization: optional string
The vaccination records looked clean. Biscuit had three entries; Pixel had none yet. The system handled both gracefully.
But then a colleague, Dr. Reyes, joined the practice. He was a surgeon, not a general vet. And Dr. Portbridge realized her model treated every Veterinarian identically. She needed a way to say “a Surgeon is a Veterinarian, but with extra capabilities.” She needed inheritance.
Chapter 7: The Specialist Problem
Dr. Reyes could do everything Dr. Portbridge could, checkups, vaccinations, prescriptions. Plus surgery. Modeling him as a plain
Veterinarianwould lose the surgery part. Creating a completely separateSurgeonconcept would duplicate all the shared fields. Neither option felt right.
The problem
You have two kinds of things that share most of their structure but differ in some aspects. Duplicating attributes across concepts is fragile: change one, forget the other. You need inheritance.
#sub: concept inheritance
concept Veterinarian:
has name: one string
has license_number: one string
has specialization: optional string
concept Surgeon:
sub Veterinarian
has surgery_count: one int
has certified_procedures: at least 1 string
The keyword sub says: “A Surgeon is a specialization of Veterinarian.” A Surgeon automatically has name, license_number, and specialization (inherited from Veterinarian), plus its own surgery_count and certified_procedures.
In OWL terms, this is rdfs:subClassOf. In object-oriented terms, it’s inheritance. In plain English: every Surgeon is a Veterinarian, but not every Veterinarian is a Surgeon.
Building a hierarchy
Let’s extend this further. The clinic is growing and has different roles:
concept Veterinarian:
has name: one string
has license_number: one string
has specialization: optional string
concept Surgeon:
sub Veterinarian
has surgery_count: one int
has certified_procedures: at least 1 string
concept Dentist:
sub Veterinarian
has dental_certification: one string
concept Intern:
sub Veterinarian
has university: one string
has year: one int
All four concepts share name, license_number, and specialization. Each adds its own details.
Inheritance for animals too
The Species enum tells us what kind of animal something is, but it doesn’t let us attach species-specific attributes. With inheritance, we can:
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 can_fly: one boolean
A Dog is an Animal with breed and neuter status. A Bird is an Animal with a wingspan and flight ability. The shared core (name, species, age, etc.) is defined once and inherited everywhere.
Ordering convention
Inside a concept body, put sub first, then has declarations. This isn’t enforced by the parser, but it’s the standard Dolfin style:
concept Surgeon:
sub Veterinarian # ← parent first
has surgery_count: one int # ← then own attributes
The story so far
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "0.1.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
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 first_name: one string
has last_name: one string
has phone_numbers: at least 1 string
has email: optional string
has address: optional string
concept Veterinarian:
has name: one string
has license_number: one string
has specialization: optional string
concept Surgeon:
sub Veterinarian
has surgery_count: one int
has certified_procedures: at least 1 string
concept Dentist:
sub Veterinarian
has dental_certification: one string
concept Intern:
sub Veterinarian
has university: one string
has year: one int
concept Vaccination:
has vaccine_name: one string
has date_administered: one string
has batch_number: 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 can_fly: one boolean
concept Appointment:
has animal: one Animal
has date: one string
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
Try it
The clinic just hired a Radiologist (a specialized Veterinarian). They have a machine_certified_on attribute (a string, the machine name) and a readings_performed count. Add the concept:
concept Veterinarian:
has name: one string
has license_number: one string
has specialization: optional string
concept Surgeon:
sub Veterinarian
has surgery_count: one int
has certified_procedures: at least 1 string
# Add Radiologist here
The model now captured the difference between Dr. Portbridge (general practice), Dr. Reyes (surgeon), and the new dental specialist who started on Tuesdays. But Dr. Portbridge noticed something: she was manually checking whether each animal’s vaccinations were up to date, whether appointments with surgeons involved surgical cases, whether overdue checkups needed follow-up reminders. She was the reasoning engine, and she was exhausted.
“What if the system could figure these things out by itself?”
Chapter 8: 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
?animal vaccinations 0
then:
?animal a UnvaccinatedAnimal
Note: This is a simplified example. Real vaccine-overdue logic would involve date arithmetic. The important thing 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 |
?animal vaccinations 0 | ?animal has an attribute vaccinations whose value 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 [ preferred_vet ?vet ]
then:
?animal treatedBy ?vet
“If an animal’s owner has a preferred vet, assign that vet to the animal.”
(This requires adding preferred_vet to Owner, we’ll do that below.)
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
The story so far
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "0.1.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
concept Species:
only values:
Dog
Cat
Bird
Rabbit
Reptile
Other
concept Urgency:
only values:
Routine
Urgent
Emergency
concept AppointmentStatus:
only values:
Scheduled
InProgress
Completed
Cancelled
concept Owner:
has first_name: one string
has last_name: one string
has phone_numbers: at least 1 string
has email: optional string
has address: optional string
has preferred_vet: optional Veterinarian
concept Veterinarian:
has name: one string
has license_number: one string
has specialization: optional string
concept Surgeon:
sub Veterinarian
has surgery_count: one int
has certified_procedures: at least 1 string
concept Dentist:
sub Veterinarian
has dental_certification: one string
concept Intern:
sub Veterinarian
has university: one string
has year: one int
concept Vaccination:
has vaccine_name: one string
has date_administered: one string
has batch_number: 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 can_fly: one boolean
concept Appointment:
has animal: one Animal
has date: one string
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
?animal vaccinations 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 [ preferred_vet ?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 Dr. Portbridge wanted to go further, she wanted to express constraints like “a surgery appointment MUST be handled by a Surgeon” and “every animal must have at least one vaccination before age 1.” She needed guard rails.
Chapter 9: Guard Rails
An owner brought in a hamster for dental surgery. The system accepted the appointment without complaint, it didn’t know that dental procedures require a Dentist, or that hamsters aren’t in the Species enum (they’d have to be
Other). The rules from the previous chapter could flag problems after the fact, but Dr. Portbridge wanted to prevent mistakes from being recorded in the first place.
The problem
Rules infer new facts, they add information. But they don’t prevent bad data. We need a way to express expectations: “Every X must satisfy Y”, “No X should ever have Z.”
Quantifiers in rules
Dolfin provides quantifiers that express conditions over collections:
| Quantifier | Meaning |
|---|---|
all | Every element must satisfy the condition |
none | No element may satisfy the condition |
at_least n | At least n elements must satisfy it |
at_most n | At most n elements may satisfy it |
exactly n | Exactly n elements must satisfy it |
Ensuring surgical appointments have surgeons
concept InvalidSurgery
rule validate_surgery_staff:
match:
?appt a Appointment
?appt reason "surgery"
among:
?appt animal [ treatedBy ?vet ]
none:
?vet a Surgeon
then:
?appt a InvalidSurgery
This reads: “If an appointment is for surgery but the treating vet is NOT a Surgeon, flag it as invalid.”
The none ?vet a Surgeon pattern succeeds when the bound ?vet does not belong to the Surgeon concept.
Minimum vaccination rules
Every dog should have at least one vaccination (in the real world, rabies is required by law in most places):
concept UnderVaccinatedDog
rule check_dog_vaccines:
match:
?dog a Dog
?dog vaccinations 0
then:
?dog a UnderVaccinatedDog
Comparison operators
You’ve already seen > and = in rules. Here’s the complete set:
| Operator | Meaning | Example |
|---|---|---|
= | Equal | ?x = 5 |
!= | Not equal | ?x != "none" |
< | Less than | ?age < 1 |
<= | Less than or equal | ?age <= 12 |
> | Greater than | ?weight > 40.0 |
>= | Greater than or equal | ?age >= 10 |
Combining conditions
Rule patterns are combined with implicit AND. All conditions must hold for the rule to fire:
concept AtRiskAnimal
rule flag_at_risk:
match:
?animal a Animal
?animal age [ > 15 ]
?animal weight [ < 2.0 ]
?animal vaccinations 0
then:
?animal a AtRiskAnimal
“An animal older than 15, weighing less than 2kg, with no vaccinations, is at risk.”
Validation concepts as a pattern
Notice the design pattern emerging: we create empty concepts (InvalidSurgery, UnderVaccinatedDog, AtRiskAnimal) that exist only to be assigned by rules. They act as tags or flags. Downstream systems can query for all instances of InvalidSurgery and take action.
This is a powerful idiom:
- Define an empty concept that represents a condition
- Write a rule that classifies instances into that concept
- Query or display instances of the condition
The story so far
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "0.1.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
concept Species:
only values:
Dog
Cat
Bird
Rabbit
Reptile
Other
concept Urgency:
only values:
Routine
Urgent
Emergency
concept AppointmentStatus:
only values:
Scheduled
InProgress
Completed
Cancelled
concept Owner:
has first_name: one string
has last_name: one string
has phone_numbers: at least 1 string
has email: optional string
has address: optional string
has preferred_vet: optional Veterinarian
concept Veterinarian:
has name: one string
has license_number: one string
has specialization: optional string
concept Surgeon:
sub Veterinarian
has surgery_count: one int
has certified_procedures: at least 1 string
concept Dentist:
sub Veterinarian
has dental_certification: one string
concept Intern:
sub Veterinarian
has university: one string
has year: one int
concept Vaccination:
has vaccine_name: one string
has date_administered: one string
has batch_number: 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 can_fly: one boolean
concept Appointment:
has animal: one Animal
has date: one string
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 / flag concepts
concept UnvaccinatedAnimal
concept UnsafeAssignment
concept OverweightAnimal
concept SeniorCat
concept InvalidSurgery
concept UnderVaccinatedDog
concept AtRiskAnimal
# Inference rules
rule flag_unvaccinated:
match:
?animal a Animal
?animal vaccinations 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 flag_senior_cat:
match:
?cat a Cat
?cat age [ >= 10 ]
then:
?cat a SeniorCat
rule assign_primary_vet:
match:
?animal a Animal
?animal owner [ preferred_vet ?vet ]
then:
?animal treatedBy ?vet
rule validate_surgery_staff:
match:
?appt a Appointment
?appt reason "surgery"
among:
?appt animal [ treatedBy ?vet ]
none:
?vet a Surgeon
then:
?appt a InvalidSurgery
rule check_dog_vaccines:
match:
?dog a Dog
?dog vaccinations 0
then:
?dog a UnderVaccinatedDog
rule flag_at_risk:
match:
?animal a Animal
?animal age [ > 15 ]
?animal weight [ < 2.0 ]
?animal vaccinations 0
then:
?animal a AtRiskAnimal
Try it
Write a validation rule that flags an Appointment as MissingDiagnosis when its status is Completed but diagnosis is absent (equals empty string ""):
concept MissingDiagnosis:
rule check_completed_diagnosis:
match:
# your patterns here
then:
# your assertion here
The guard rails caught two problems on day one: a surgery booked with an intern and a completed appointment with no recorded diagnosis. Dr. Portbridge felt like the system was finally earning its keep.
Then the regional veterinary board called. They needed the clinic to export its data in a format compatible with the national animal health registry, which used standard OWL/RDF vocabularies. Dr. Portbridge’s concept names were fine for her clinic, but the outside world expected specific IRIs. She needed a bridge.
Chapter 10: Talking to the Outside World
*The email from the Regional Veterinary Board was blunt:
Please submit your animal health records using the National Animal Health Ontology (NAHO) vocabulary. All concepts must use IRIs from
http://naho.gov/ontology/. All species must reference the FAO species classification athttp://fao.org/species/.Dr. Portbridge looked at her Dolfin file. Her concepts were called
AnimalandDog. The board expectedhttp://naho.gov/ontology/Animalandhttp://fao.org/species/CanineDomestic. She needed a way to map her clean, readable names to the bureaucratic world of IRIs.
The problem
Dolfin ontologies live in a clean, human-readable world. The semantic web lives in a world of IRIs (Internationalized Resource Identifiers), long URLs that uniquely identify every concept, property, and individual. To interoperate, we need to connect the two.
Prefixes
A prefix declares a short alias for an IRI namespace:
prefix naho as <http://naho.gov/ontology/>
prefix fao as <http://fao.org/species/>
Now you can reference external concepts using the prefix:
prefix naho as <http://naho.gov/ontology/>
concept Animal:
has name: one string
When compiled to OWL, Animal will be mapped to the naho namespace, producing http://naho.gov/ontology/Animal.
Using prefixed references
Prefixes let you reference concepts from other ontologies:
prefix schema as <http://schema.org/>
concept Owner:
has first_name: one string
has last_name: one string
has phone_numbers: at least 1 string
has email: optional string
has address: optional schema.PostalAddress
schema.PostalAddress refers to http://schema.org/PostalAddress, Schema.org’s definition of a postal address. You’re linking your clinic ontology to a globally recognized vocabulary.
The @iri_name annotation
Sometimes the external name for a concept differs from the name you want in your code. The @iri_name annotation lets you control the exact IRI segment:
@iri_name
concept DomesticDog:
sub Animal
has breed: optional string
has neutered: one boolean
This is useful when external systems expect a specific IRI fragment that doesn’t match your preferred concept name.
Multiple prefixes
A real-world ontology often bridges multiple external vocabularies:
prefix naho as <http://naho.gov/ontology/>
prefix fao as <http://fao.org/species/>
prefix schema as <http://schema.org/>
prefix dc as <http://purl.org/dc/elements/1.1/>
Each prefix is independent. You can use as many as needed.
The complete clinic with prefixes
prefix naho as <http://naho.gov/ontology/>
prefix fao as <http://fao.org/species/>
prefix schema as <http://schema.org/>
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "1.0.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
concept Species:
only values:
Dog
Cat
Bird
Rabbit
Reptile
Other
concept Urgency:
only values:
Routine
Urgent
Emergency
concept AppointmentStatus:
only values:
Scheduled
InProgress
Completed
Cancelled
concept Owner:
has first_name: one string
has last_name: one string
has phone_numbers: at least 1 string
has email: optional string
has address: optional string
has preferred_vet: optional Veterinarian
concept Veterinarian:
has name: one string
has license_number: one string
has specialization: optional string
concept Surgeon:
sub Veterinarian
has surgery_count: one int
has certified_procedures: at least 1 string
concept Dentist:
sub Veterinarian
has dental_certification: one string
concept Intern:
sub Veterinarian
has university: one string
has year: one int
concept Vaccination:
has vaccine_name: one string
has date_administered: one string
has batch_number: 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 can_fly: one boolean
concept Appointment:
has animal: one Animal
has date: one string
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
# Flag concepts
concept UnvaccinatedAnimal
concept UnsafeAssignment
concept OverweightAnimal
concept SeniorCat
concept InvalidSurgery
concept UnderVaccinatedDog
concept AtRiskAnimal
# Inference rules
rule flag_unvaccinated:
match:
?animal a Animal
?animal vaccinations 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 flag_senior_cat:
match:
?cat a Cat
?cat age [ >= 10 ]
then:
?cat a SeniorCat
rule assign_primary_vet:
match:
?animal a Animal
?animal owner [ preferred_vet ?vet ]
then:
?animal treatedBy ?vet
rule validate_surgery_staff:
match:
?appt a Appointment
?appt reason "surgery"
among:
?appt animal [ treatedBy ?vet ]
none:
?vet a Surgeon
then:
?appt a InvalidSurgery
rule check_dog_vaccines:
match:
?dog a Dog
?dog vaccinations 0
then:
?dog a UnderVaccinatedDog
rule flag_at_risk:
match:
?animal a Animal
?animal age [ > 15 ]
?animal weight [ < 2.0 ]
?animal vaccinations 0
then:
?animal a AtRiskAnimal
Try it
Add a prefix for Dublin Core (http://purl.org/dc/elements/1.1/) and FOAF (http://xmlns.com/foaf/0.1/):
prefix naho as <http://naho.gov/ontology/>
# Add Dublin Core and FOAF prefixes here
Dr. Portbridge submitted the data export. The board’s system accepted it without complaint, her concepts mapped cleanly to NAHO’s IRIs, and the species references aligned with FAO’s vocabulary. Her little clinic was speaking the same language as the national registry.
She leaned back in her chair and looked at the screen. What had started as a napkin sketch was now a complete data model: concepts with inheritance, cardinality constraints, enums for controlled vocabularies, rules for automated reasoning, constraints for validation, and prefixes for interoperability. Biscuit dozed at her feet. Pixel purred on the printer.
Epilogue: The Full Picture
Three months later, Happy Paws had treated 847 animals. The system had caught 23 unsafe assignments, flagged 156 overdue vaccinations, and identified 4 at-risk animals that might have been missed. Dr. Reyes had performed 31 surgeries without a single scheduling error. And Pixel, now a healthy six-month-old, had been adopted by the receptionist.
What you’ve learned
Over ten chapters, you’ve built a complete ontology from scratch. Here’s what each chapter introduced:
| Chapter | Feature | Why you needed it |
|---|---|---|
| 1 | Packages | Identity and metadata for the project |
| 2 | Concepts & primitive types | Describing real-world entities |
| 3 | Properties & references | Connecting concepts to each other |
| 4 | Enums | Controlled vocabularies instead of free text |
| 5 | Cardinality | Constraining how many values an attribute holds |
| 6 | Multi-valued attributes | Lists, required collections, and promoting strings to concepts |
| 7 | Inheritance (sub) | Shared structure without duplication |
| 8 | Rules | Automated reasoning and inference |
| 9 | Constraints & quantifiers | Validation and guard rails |
| 10 | Prefixes & IRIs | Interoperability with external systems |
The complete ontology
Here is the full Happy Paws ontology, everything from every chapter, in one file:
# ============================================================
# Happy Paws Veterinary Clinic: Complete Ontology
# ============================================================
prefix naho as <http://naho.gov/ontology/>
prefix fao as <http://fao.org/species/>
prefix schema as <http://schema.org/>
package <http://happypaws.com/clinic>:
dolfin_version "1"
version "1.0.0"
author "Dr. Helen Portbridge"
description "The Happy Paws veterinary clinic data model"
# ------------------------------------------------------------
# Enumerations
# ------------------------------------------------------------
concept Species:
only values:
Dog
Cat
Bird
Rabbit
Reptile
Other
concept Urgency:
only values:
Routine
Urgent
Emergency
concept AppointmentStatus:
only values:
Scheduled
InProgress
Completed
Cancelled
# ------------------------------------------------------------
# People
# ------------------------------------------------------------
concept Owner:
has first_name: one string
has last_name: one string
has phone_numbers: at least 1 string
has email: optional string
has address: optional string
has preferred_vet: optional Veterinarian
concept Veterinarian:
has name: one string
has license_number: one string
has specialization: optional string
concept Surgeon:
sub Veterinarian
has surgery_count: one int
has certified_procedures: at least 1 string
concept Dentist:
sub Veterinarian
has dental_certification: one string
concept Intern:
sub Veterinarian
has university: one string
has year: one int
# ------------------------------------------------------------
# Medical records
# ------------------------------------------------------------
concept Vaccination:
has vaccine_name: one string
has date_administered: one string
has batch_number: optional string
# ------------------------------------------------------------
# Animals
# ------------------------------------------------------------
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 can_fly: one boolean
# ------------------------------------------------------------
# Appointments
# ------------------------------------------------------------
concept Appointment:
has animal: one Animal
has date: one string
has reason: one string
has urgency: one Urgency
has status: one AppointmentStatus
has diagnosis: optional string
has treatments: string
has notes: optional string
# ------------------------------------------------------------
# Standalone properties
# ------------------------------------------------------------
property treatedBy:
Animal -> Veterinarian
# ------------------------------------------------------------
# Flag concepts (created by rules)
# ------------------------------------------------------------
concept UnvaccinatedAnimal
concept UnsafeAssignment
concept OverweightAnimal
concept SeniorCat
concept InvalidSurgery
concept UnderVaccinatedDog
concept AtRiskAnimal
# ------------------------------------------------------------
# Inference rules
# ------------------------------------------------------------
rule flag_unvaccinated:
match:
?animal a Animal
?animal vaccinations 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 flag_senior_cat:
match:
?cat a Cat
?cat age [ >= 10 ]
then:
?cat a SeniorCat
rule assign_primary_vet:
match:
?animal a Animal
?animal owner [ preferred_vet ?vet ]
then:
?animal treatedBy ?vet
# ------------------------------------------------------------
# Validation rules
# ------------------------------------------------------------
rule validate_surgery_staff:
match:
?appt a Appointment
?appt reason "surgery"
among:
?appt animal [ treatedBy ?vet ]
none:
?vet a Surgeon
then:
?appt a InvalidSurgery
rule check_dog_vaccines:
match:
?dog a Dog
?dog vaccinations 0
then:
?dog a UnderVaccinatedDog
rule flag_at_risk:
match:
?animal a Animal
?animal age [ > 15 ]
?animal weight [ < 2.0 ]
?animal vaccinations 0
then:
?animal a AtRiskAnimal
Dr. Portbridge closed her laptop and looked around the clinic. The walls were covered in thank-you cards from pet owners. The system hummed quietly in the background, catching errors, inferring relationships, and speaking the language of the wider world. What had started as a napkin sketch on opening day was now a living, breathing data model.
Biscuit dozed at her feet. Pixel purred on the printer. All was well at Happy Paws.
Syntax Overview
Dolfin uses indentation-sensitive syntax, like Python. Blocks are opened with a colon (:) and delimited by consistent indentation, tabs or spaces, but never mixed. All lines inside a block must be at the same indentation level.
Comments begin with # and run to the end of the line.
# This is a comment
concept Foo:
has bar: string # inline comment
File types
A Dolfin project uses two kinds of files.
package.dlf: one per project, declares the package identity:
package <http://example.com/my-ontology>:
dolfin_version "1"
version "0.3.0"
author "Alice"
description "A short description"
| Field | Required | Description |
|---|---|---|
dolfin_version | yes | Language version to use (currently "1") |
version | yes | Ontology version (semver string) |
author | no | Author name |
description | no | Human-readable description of the ontology |
*.dlf ontology files: one or more files containing declarations (concepts, enums, properties, rules).
Prefixes
Prefixes bind short aliases to IRI namespaces, enabling interoperability with external vocabularies.
prefix schema as <http://schema.org/>
prefix dc as <http://purl.org/dc/elements/1.1/>
Once declared, a prefix can be used in qualified names:
has address: optional schema.PostalAddress
Prefixes can also be declared in a hierarchical block to share a common path:
prefix com.example:
Person
Organization as Org
This is equivalent to:
prefix com.example.Person
prefix com.example.Organization as Org
Concepts
A concept defines a category of things and the attributes they can carry.
concept Person:
has first_name: one string
has last_name: one string
has email: optional string
has age: optional int
An empty concept (no body) is also valid, useful as a flag or tag:
concept FlaggedForReview
Inheritance
A concept can inherit from one or more parents using sub:
concept Employee:
sub Person
has employee_id: one string
has department: one string
concept Manager:
sub Employee
has reports: Employee
Multiple parents are comma-separated:
concept PartTimeEmployee:
sub Employee, Contractor
Attributes
Each attribute is declared with has:
has <name>: [cardinality] <type>
The type can be a primitive or another concept name:
has count: one int
has owner: optional Person
has tags: string # zero or more (default cardinality)
Cardinality
Cardinality constrains how many values an attribute can hold.
| Keyword | Meaning |
|---|---|
| (none) | Any number (zero or more) |
any | Any number (zero or more, explicit) |
one | Exactly one (required) |
optional | Zero or one |
some | One or more |
N | Exactly N (integer literal) |
N..M | Between N and M (inclusive) |
N..* | At least N |
has name: one string # required, single value
has nickname: optional string # may be absent
has tags: some string # at least one
has aliases: string # any number (default)
has lucky_numbers: 3 int # exactly three
has scores: 1..5 float # between one and five
has comments: 0..* string # zero or more (explicit range)
Primitive types
| Type | Description | Examples |
|---|---|---|
string | Text | "hello", "" |
int | Integer | 0, 42, -7 |
float | Floating-point | 3.14, -0.5 |
boolean | Boolean | true, false |
Closed concepts
An closed concept defines a closed set of named values. Only the declared variants are valid:
concept Status:
one of:
Pending
Active
Archived
Enumurated values are referenced by name in attributes and rules:
has status: one Status
Properties
A property is a named relationship declared independently of any concept, rather than inside one. It connects a domain type to a range type:
property worksFor: Employee -> Organization
Cardinality can be specified on either side:
property manages: one Manager -> Employee
Properties are used in rule patterns and assertions like any attribute.
Rules
A rule defines an if-then inference. When all patterns in the match: block hold, the assertions in the then: block are applied.
rule classify_senior:
match:
?p a Person
?p age [ >= 65 ]
then:
?p a SeniorPerson
Variables
Variables begin with ?. They bind to values during matching and can be referenced in assertions:
rule link_preferred_contact:
match:
?org a Organization
?org primary_contact ?person
then:
?person worksFor ?org
Match patterns
Each line in match: is a pattern. All patterns are combined with implicit AND.
Type pattern: checks that a variable is an instance of a concept:
?x a SomeConcept
Triple pattern: checks that a subject has a property with a given value:
?x someProperty someValue
?x someProperty ?y
?x someProperty "literal"
?x someProperty 42
Constraint block: inline conditions on a value using [...]:
?x age [ > 18 ]
?x status [ = Active ]
?x address [ city "Paris" ]
?x manager [ a Director ]
Multiple constraints in a block are AND-combined:
?x score [ >= 50, <= 100 ]
Constraint blocks can be nested:
?x owner [ address [ country "France" ] ]
Comparison operators
Used inside constraint blocks:
| Operator | Meaning |
|---|---|
= | Equal |
!= | Not equal |
< | Less than |
<= | Less than or equal |
> | Greater than |
>= | Greater than or equal |
Quantifiers
Quantifiers express conditions over collections of matching bindings:
| Quantifier | Meaning |
|---|---|
all | Every binding must satisfy the sub-patterns |
none | No binding may satisfy the sub-patterns |
at_least N | At least N bindings must satisfy the sub-patterns |
at_most N | At most N bindings may satisfy the sub-patterns |
exactly N | Exactly N bindings must satisfy the sub-patterns |
between N, M | Between N and M bindings (inclusive) |
rule require_manager_approval:
match:
?req a Request
none ?approver:
?approver a Manager
?req approvedBy ?approver
then:
?req a UnapprovedRequest
An optional constraint block on the quantifier variable filters the set being quantified:
at_least 2 ?member [ a SeniorEmployee ]:
?member worksIn ?dept
Then assertions
Each line in then: asserts a new fact:
Type assertion: classifies a variable as an instance of a concept:
?x a SomeConcept
Triple assertion: asserts a property relationship:
?x someProperty ?y
?x someProperty "value"
Nested rules
A then: block can contain a nested match:/then: block for conditional sub-inferences:
rule complex_inference:
match:
?x a Foo
then:
match:
?x bar ?y
then:
?y a Baz
Names and identifiers
Simple names are alphanumeric identifiers (with underscores): Person, first_name, status.
Qualified names use dot-notation to reference names in a namespace: schema.Person, com.example.Thing.
IRIs are enclosed in angle brackets: <http://example.com/Thing>. They can appear as package names and prefix targets.
Variables begin with ?: ?person, ?count.
The @iri_name annotation
The @iri_name annotation overrides the IRI segment derived from a file’s name. It appears at the top of an ontology file, before any declarations:
@iri_name "custom-segment"
concept Foo:
has bar: string
This is useful when the file name doesn’t match the IRI fragment expected by external systems.
Type System
Full Grammar
There are two kind of files in a dolfin package. The package.dlf
placed at the root of a package folder, it serves as a manifest of the whole package. We give an exemple of such a file here and let the grammar being deduced.
package <http://my.special.iri/of-ontology>:
dolfin_version: 1
version: 1.2.3
author: not only me
author: but also you
description: This package is an example
There are regular dolfin files. The folder in which they are, are used to compose the iri of each ontology and then the iri of each element.
ontology ::=
iri_name_annotation?
prefix_statement*
declaration*
EOF
iri_name_annotation ::=
"@iri_name" String NEWLINE
prefix_statement ::=
"prefix" prefix_target
prefix_target ::=
qualified_name_or_iri ":" NEWLINE
INDENT
prefix_target+
DEDENT
| qualified_name_or_iri "as" Name NEWLINE
| qualified_name_or_iri NEWLINE
declaration ::=
concept
| property
| rule
concept ::=
"concept" Name ":" NEWLINE
INDENT
concept_member+
DEDENT
concept_member ::=
sub_concept
| has_property
| "one" "of" ":" NEWLINE
INDENT
enum_value+
DEDENT
sub_concept ::= "sub" type_ref ("," typeref)*
has_property ::= "has" Name ":" cardinality? type_ref NEWLINE
property ::=
"property" Name ":" cardinality? type_ref "->" cardinality? type_ref NEWLINE
rule ::=
"rule" Name ":" NEWLINE
INDENT
match_block
then_block
DEDENT
match_block ::=
"match" ":" NEWLINE
INDENT
match_pattern+
DEDENT
then_block ::=
"then" ":" NEWLINE
INDENT
then_item+
DEDENT
match_pattern ::=
subject qualified_name object NEWLINE
| subject qualified_name contraint_block NEWLINE
| subject "a" tyep_ref NEWLINE
| "among" ":" NEWLINE
INDENT
match_pattern+
DEDENT quantifier ":" NEWLINE
INDENT
match_pattern+
DEDENT
| quantifier ":" NEWLINE
INDENT
match_pattern+
DEDENT
quantifier ::=
"all"
| "none"
| "at least" Integer
| "at most" Integer
| "exactly" Integer
| "between" Integer "," Integer
then_item ::=
assertion NEWLINE
| nested_rule
assertion ::=
subject qualified_name object
| subject qualified_name no_comp_contraint_block
| subject "a" type_ref
nested_rule ::= match_block then_block
subject ::=
variable
| qualified_name
| contraint_block
object ::=
variable
| literal
| qualified_name
constraint_block ::= "[" constraint ("," contraint)+ ]
contraint ::=
"a" type_ref
| comparison_op literal
| qualified_name object
| qualified_name contraint_block
comparion_op ::=
"="
| "!="
| "<"
| "<="
| ">"
| ">="
type_ref ::=
"string"
| "int"
| "float"
| "boolean"
qualified_name ::=
cardinality ::=
"one"
| "any"
| "some"
| "optional"
| Integer
| Integer ".." Integer
| Integer ".." "*"