| name | ontology-modeling |
| description | Use when modeling any of the 6 atoms of an ontology/digital twin — ObjectType, AttributeType, LinkType, ActionType, PolicyType, EventType — the semantic-first way. Covers the business questions to ask before listing attributes, links as native slots, kinetic atoms as contracts, and data binding for twinning. Make sure to use this whenever the user wants to add, define, model, or create a domain object, attribute, relation/link, action, policy/rule, or event — even if they don't say 'ontology'. |
Ontology Modeling
The point of ontology modeling is not filling a template — it is answering business questions
before touching YAML. An un-owned definition drifts. An un-enforced policy is theatre. A link
buried in a separate file gets orphaned. This skill makes those traps visible and shows you how to
avoid them.
Principle: semantic-first
Answer these four questions before you list a single attribute:
- What concept is this? Write one sentence. If you cannot, the boundary is wrong.
- Who owns the definition? Record it as
x_governance_owner. Without an owner, every team will
add columns independently and you get five definitions of "customer".
- Does it carry PII? If yes, name the fields in
x_pii right now, while you still remember.
Retrofitting PII tagging is expensive and error-prone.
- What is the identifier? Mark one attribute
identifier: true. Without a stable key,
cross-system joins break silently.
Only after you have those four answers does writing attributes pay off.
The 6 atoms
The kit's six atoms map cleanly to LinkML constructs. The first three are semantic (what the
world looks like); the last three are kinetic (what happens in it).
Atom 1 — ObjectType (class)
An ObjectType is a named concept in your domain. It becomes a class in LinkML. It carries
governance annotations at the class level.
classes:
Customer:
description: A borrower in the lending domain.
annotations:
x_governance_owner: "growth-team"
x_pii: "customer_id,full_name"
x_data_binding: "curated/customers"
x_sync_mode: "batch"
x_actions: "upgradeTier"
attributes:
customer_id: { identifier: true, range: string, required: true }
full_name: { range: string }
The annotations block is the kit's extension layer — all x_* fields are preserved by LinkML
into linkml_meta in generated code and do not interfere with validation.
Atom 2 — AttributeType (a slot with a scalar or enum range)
An attribute is a slot inside a class whose range is a built-in type (string, integer,
float, boolean) or an enum. Use required: true for fields that must always be present.
Use identifier: true for exactly one slot per class — the stable key.
attributes:
customer_id: { identifier: true, range: string, required: true }
tier: { range: TierEnum }
credit_score:{ range: integer }
TierEnum is defined in a shared enums file and imported at the root. The validator rejects any
value outside the enum's permissible_values.
Atom 3 — LinkType (a slot whose range is a class)
A link is not a separate file or construct — it is a slot whose range points to another
class. Add it directly inside the object that owns the relationship.
attributes:
loans:
description: Loans belonging to this customer.
range: Loan
multivalued: true
inlined_as_list: false
range: Loan resolves at the root schema because the root imports both objects/customer and
objects/loan. The customer.yaml file does not need to see loan.yaml directly. Do not create
a links/ folder — the link lives inside the object that holds it.
multivalued: true means the field is a list. inlined_as_list: false means the validator
expects references (identifiers), not embedded inline objects.
Atom 4 — ActionType (a kinetic class as contract)
An ActionType models something the system can do to an object. Declare it as a class with
three fields as a minimum contract: function_ref (what code runs), validation_rule (what must
be true before it runs), and emits_event (what event fires when it succeeds).
ActionType:
description: Base class for domain actions.
attributes:
function_ref: { range: string, required: true }
validation_rule: { range: string }
emits_event: { range: string }
UpgradeTierAction:
is_a: ActionType
description: Promotes a customer to the next tier.
attributes:
function_ref: { range: string, required: true }
validation_rule: { range: string }
emits_event: { range: string }
Instances of this action live in data/ — the class is the contract, not the data.
Atom 5 — PolicyType (a kinetic class as rule contract)
A PolicyType models a constraint that the system enforces. Always call it "policy" — not
"rule" and not "luật". The two mandatory fields are rule (the condition) and enforcement
(the consequence).
enforcement has three valid values:
block — reject the operation outright
warn — allow but surface a warning
audit — allow and log for later review
PolicyType:
description: Base class for domain policies.
attributes:
rule: { range: string, required: true }
enforcement: { range: EnforcementEnum, required: true }
MaxLoansPolicy:
is_a: PolicyType
description: A customer may not hold more than 5 active loans.
attributes:
rule: { range: string, required: true }
enforcement: { range: EnforcementEnum, required: true }
enums:
EnforcementEnum:
permissible_values: { block: , warn: , audit: }
A policy with enforcement: block and an invalid instance will cause linkml-validate to fail.
That is the point — the model enforces the constraint, not a downstream data check.
Atom 6 — EventType (a kinetic class as state transition)
An EventType records that something happened — a state change with a cause. Three fields form
the contract: from_state, to_state, and triggered_by.
EventType:
description: Base class for domain events.
attributes:
from_state: { range: string }
to_state: { range: string, required: true }
triggered_by: { range: string, required: true }
TierUpgradedEvent:
is_a: EventType
description: Fired when a customer's tier is promoted.
attributes:
from_state: { range: string }
to_state: { range: string, required: true }
triggered_by: { range: string, required: true }
Connect the event back to the action that fires it via the emits_event field on the ActionType.
Binding — turning a static model into a living twin
Two annotations on an ObjectType activate the twinning layer:
annotations:
x_data_binding: "curated/customers"
x_sync_mode: "batch"
x_data_binding tells the kit where to find or write real instances. x_sync_mode tells the
sync engine how often to refresh. Without these two fields, the model is a schema. With them, it
is a twin — the validator can run against live data on every sync cycle.
Workflow
Follow this order to avoid cross-reference errors:
- Semantic atoms first — ObjectType + AttributeType + LinkType. Validate green before
moving on.
- Kinetic atoms second — ActionType, PolicyType, EventType. Each in its own file.
- One atom = one file inside the appropriate subfolder (
objects/, actions/,
policies/, events/).
- Register in the root — add each new file to the root schema's
imports list.
- Validate:
linkml-validate -s ontology/root.yaml -C Customer data/customers.yaml
- Try a wrong value — pass
tier: platinum when only bronze/silver/gold are permitted.
Watch the validator catch it. That confirmation is the test that your model is enforcing
something real.
To scaffold a new atom file, use the helper script:
bash scripts/ontology-new.sh ObjectType Customer
To model interactively with the assistant:
/ontology-as-code:model
To validate a full schema:
/ontology-as-code:validate
Worked example
examples/nova-lending/ is the kit's reference implementation. Start with:
ontology/objects/customer.yaml — ObjectType with AttributeType, LinkType, all four
governance annotations, and data binding.
ontology/_shared/enums.yaml — TierEnum shared across objects.
ontology/root.yaml — the import list that ties everything together.
data/customers.yaml — sample instances to run through linkml-validate.
Run the full validation from the repo root:
cd examples/nova-lending
linkml-validate -s ontology/root.yaml -C Customer data/customers.yaml
A passing run means your schema and your data agree. A failing run with an intentionally bad value
means your policy is being enforced. Both outcomes are useful.