Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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 columnAn animal must have exactly one name
An appointment has a vet_id foreign keyAn 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"
FieldRequiredWhat it does
dolfin_versionWhich version of the Dolfin language to use
versionYour ontology’s own version (semver)
authorA human name
descriptionA 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:

  1. There is a concept called Animal.
  2. An Animal can have a name, which is text.
  3. An Animal can have a species (also text) and an age (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:

TypeWhat it holdsExamples
stringText"Biscuit", "cat"
intWhole numbers42, 0, -3
floatDecimal numbers3.14, 36.6
booleanTrue or falsetrue, 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 date a 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 like xsd: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 caseUse hasUse 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: string for 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 string field 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:

EnumConcept
InstancesFixed at design timeCreated at runtime
Has attributes
Can be extended❌ (closed list)✅ (via sub)
Use caseDropdowns, categoriesReal-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

KeywordMeaningExample
any (default)Any (0 to ∞), the defaulthas task: any Task
oneExactly one (required)has name: one string
optionalZero or onehas 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:

  • name and species are required: exactly one value, always present
  • age, weight, and owner are 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?”

AttributeCardinalityReasoning
nameoneEvery animal must have a name, even “Unknown”
speciesoneYou always know if it’s a dog or a cat
ageoptionalStrays often have unknown ages
weightoptionalNot always measured on first visit
owneroptionalStrays 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 string but 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:

KeywordMeaningExample
at least NN or more valueshas phone: at least 1 string
between N MBetween N and M (inclusive)has ref: between 2 5 Owner
at most NBetween 0 and N (inclusive)has parents: at most 2 Person
exactly NExactly N valueshas 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

SyntaxMeaningExample use case
one TypeExactly one (required)has name: one string
optional TypeZero or onehas nickname: optional string
TypeAny (0 to ∞, the default)has tag: string
at least N TypeN or morehas phone: at least 1 string
at most N Type0 to N (inclusive)has parents: at most 2 Person
between N M TypeBetween N and M (inclusive)has references: between 2 5 Owner
exactly N TypeExactly Nhas 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 Veterinarian would lose the surgery part. Creating a completely separate Surgeon concept 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:

PatternMeaning
?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:

AssertionMeaning
?animal a UnvaccinatedAnimalClassify ?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:

QuantifierMeaning
allEvery element must satisfy the condition
noneNo element may satisfy the condition
at_least nAt least n elements must satisfy it
at_most nAt most n elements may satisfy it
exactly nExactly 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:

OperatorMeaningExample
=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:

  1. Define an empty concept that represents a condition
  2. Write a rule that classifies instances into that concept
  3. 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 at http://fao.org/species/.

Dr. Portbridge looked at her Dolfin file. Her concepts were called Animal and Dog. The board expected http://naho.gov/ontology/Animal and http://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:

ChapterFeatureWhy you needed it
1PackagesIdentity and metadata for the project
2Concepts & primitive typesDescribing real-world entities
3Properties & referencesConnecting concepts to each other
4EnumsControlled vocabularies instead of free text
5CardinalityConstraining how many values an attribute holds
6Multi-valued attributesLists, required collections, and promoting strings to concepts
7Inheritance (sub)Shared structure without duplication
8RulesAutomated reasoning and inference
9Constraints & quantifiersValidation and guard rails
10Prefixes & IRIsInteroperability 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"
FieldRequiredDescription
dolfin_versionyesLanguage version to use (currently "1")
versionyesOntology version (semver string)
authornoAuthor name
descriptionnoHuman-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.

KeywordMeaning
(none)Any number (zero or more)
anyAny number (zero or more, explicit)
oneExactly one (required)
optionalZero or one
someOne or more
NExactly N (integer literal)
N..MBetween 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

TypeDescriptionExamples
stringText"hello", ""
intInteger0, 42, -7
floatFloating-point3.14, -0.5
booleanBooleantrue, 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:

OperatorMeaning
=Equal
!=Not equal
<Less than
<=Less than or equal
>Greater than
>=Greater than or equal

Quantifiers

Quantifiers express conditions over collections of matching bindings:

QuantifierMeaning
allEvery binding must satisfy the sub-patterns
noneNo binding may satisfy the sub-patterns
at_least NAt least N bindings must satisfy the sub-patterns
at_most NAt most N bindings may satisfy the sub-patterns
exactly NExactly N bindings must satisfy the sub-patterns
between N, MBetween 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 ".." "*"