Tutorial: a controlled vocabulary with SKOS
A complete, working Cortex setup built on a standard ontology. You write no OWL at all β SKOS is the vocabulary β so this is the shortest path from an empty project to a real graph an agent can read and write.
Why SKOS makes a good first graph
SKOS (Simple Knowledge Organization System) is the W3C vocabulary for thesauri, taxonomies, and controlled vocabularies. It models concepts and how they relate β broader, narrower, related β rather than any particular domain, which is exactly what you want when the thing you are building is a memory of topics, tags, product categories, or subject headings.
It suits Cortex specifically because it is small (a handful of classes and
properties, so linting has a tight vocabulary to check against) and because its own
rdfs:subPropertyOf declarations do a lot of the inference work for you β as the
rules below show.
cortex-spring-boot-starter-example/src/main/resources/skos.ttl is the SKOS Core
vocabulary, merged alongside the example's own ontology. Copy that file into your own
src/main/resources/, or download it from
w3.org and convert it to
Turtle.
-
Use SKOS as the ontology
Point
cortex.ontologiesat the SKOS file. There is nothing to author β the classes (skos:Concept,skos:ConceptScheme) and properties (skos:prefLabel,skos:broader, β¦) come from the standard.Remember what the ontology does in Cortex: linting is a closed-world check, so after this step an agent can use SKOS terms and nothing else. A made-up
ex:inventedpredicate is rejected before it ever reaches SHACL. -
Write shapes for your vocabulary
SKOS says what the terms mean; it does not say which of them your data must carry. That is the shapes file β and it is where you pin your own IRI scheme, which SKOS has no opinion about.
ontology.shapes@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . @prefix sh: <http://www.w3.org/ns/shacl#> . @prefix skos: <http://www.w3.org/2004/02/skos/core#> . @prefix s: <https://example.org/shapes#> . s:VocabularyShape a sh:NodeShape ; sh:targetSubjectsOf rdf:type ; sh:nodeKind sh:IRI ; sh:pattern "^https://example.org/vocab/[^/?#]+$" ; sh:message "vocabulary terms must be https://example.org/vocab/{name} IRIs" ; sh:property [ sh:path rdf:type ; sh:minCount 1 ; sh:in ( skos:Concept skos:ConceptScheme ) ; sh:message "only skos:Concept and skos:ConceptScheme may be asserted" ; ] ; sh:property [ sh:path skos:prefLabel ; sh:minCount 1 ; sh:maxCount 1 ; sh:nodeKind sh:Literal ; sh:message "every term needs exactly one skos:prefLabel" ; ] . s:ConceptShape a sh:NodeShape ; sh:targetClass skos:Concept ; sh:property [ sh:path skos:definition ; sh:minCount 1 ; sh:nodeKind sh:Literal ; sh:message "every concept needs a skos:definition" ; ] ; sh:property [ sh:path skos:broader ; sh:nodeKind sh:IRI ; sh:message "skos:broader must point at another concept" ; ] .Three decisions worth calling out:
- One
skos:prefLabel, no more. This is SKOS's own integrity condition, and it is the constraint that stops an agent quietly introducing a second name for a concept that already has one. sh:inonrdf:type. SKOS declares classes you probably do not want asserted by hand β restrict the assertable set explicitly rather than inheriting whatever the standard happens to define.- An IRI pattern. Cortex reserves
cortex://; everything else is yours. Pinning a pattern is what keeps an agent from inventing a parallel namespace for the same concepts.
- One
-
Add rules β SKOS does most of the work
ontology.rules@prefix rdf: <http://www.w3.org/1999/02/22-rdf-syntax-ns#> . @prefix rdfs: <http://www.w3.org/2000/01/rdf-schema#> . @prefix skos: <http://www.w3.org/2004/02/skos/core#> . [subClass: (?x rdf:type ?c1) (?c1 rdfs:subClassOf ?c2) -> (?x rdf:type ?c2)] [subProperty: (?x ?p1 ?y) (?p1 rdfs:subPropertyOf ?p2) -> (?x ?p2 ?y)] [narrower: (?a skos:broader ?b) -> (?b skos:narrower ?a)] [broaderTransitive: (?a skos:broader ?b) (?b skos:broaderTransitive ?c) -> (?a skos:broaderTransitive ?c)] [hasTopConcept: (?c skos:topConceptOf ?s) -> (?s skos:hasTopConcept ?c)] [definitionComment: (?c skos:definition ?d) -> (?c rdfs:comment ?d)]The generic
subPropertyrule is the one that pays off, because SKOS is full ofrdfs:subPropertyOfdeclarations. Three of them matter here:Declared in SKOS What you get for free skos:prefLabel,skos:altLabelβrdfs:labelEvery concept becomes searchable and renders with a name in the web UI β Cortex indexes rdfs:label, and the inferred one counts.skos:broaderβskos:broaderTransitiveThe broaderTransitiverule above only has to chain; the base case is already entailed.skos:topConceptOfβskos:inSchemeDeclaring a top concept puts it in the scheme without a second statement. WhydefinitionCommentis worth having.skos:definitionis a sub-property ofskos:note, not ofrdfs:commentβ so without that one rule your definitions never reach the comment field the Describe view renders and the search index covers. It is a one-line rule that makes a SKOS graph feel native in the Cortex UI. -
Configure it
application.yamlcortex: persistent: true ontologies: - classpath:skos.ttl shapes: - classpath:ontology.shapes rules: - classpath:ontology.rulesEach property takes a list, merged in order β so if you later add domain classes of your own, append
classpath:ontology.ttlafterskos.ttlrather than editing the standard vocabulary. That is exactly what the shipped example does. -
Ingest a vocabulary
Ask an agent over MCP to add some concepts, or POST this to
/import. Either way it is linted, SHACL-validated, and staged on a branch for review β nothing below is queryable until you approve it at/branches.Turtle@prefix skos: <http://www.w3.org/2004/02/skos/core#> . @prefix : <https://example.org/vocab/> . :topics a skos:ConceptScheme ; skos:prefLabel "Documentation topics" . :knowledge-graph a skos:Concept ; skos:prefLabel "Knowledge graph" ; skos:definition "A graph-structured store of facts about entities and their relationships." ; skos:topConceptOf :topics . :ontology a skos:Concept ; skos:prefLabel "Ontology" ; skos:definition "A formal vocabulary of classes and properties for a domain." ; skos:broader :knowledge-graph . :shacl a skos:Concept ; skos:prefLabel "SHACL" ; skos:altLabel "Shapes Constraint Language" ; skos:definition "A language for validating the structure of RDF data." ; skos:broader :ontology ; skos:related :ontology .
What you get after approving
Four subjects were asserted. Once the branch is merged, the inference closure holds a good deal more β all of it queryable through Query, Describe, and the web UI:
| Inferred statement | From |
|---|---|
:shacl skos:broaderTransitive :knowledge-graph | The broaderTransitive rule chaining :shacl β :ontology β :knowledge-graph. |
:knowledge-graph skos:narrower :ontology | The narrower rule inverting skos:broader. |
:topics skos:hasTopConcept :knowledge-graph | The hasTopConcept rule inverting skos:topConceptOf. |
:knowledge-graph skos:inScheme :topics | skos:topConceptOf β skos:inScheme, via the generic sub-property rule. |
:shacl rdfs:label "SHACL", "Shapes Constraint Language" | skos:prefLabel and skos:altLabel β rdfs:label. |
:shacl rdfs:comment "A language for validatingβ¦" | The definitionComment rule. |
Walk the hierarchy with a SPARQL SELECT β through the Query MCP tool, or
Cortex.query(String):
PREFIX skos: <http://www.w3.org/2004/02/skos/core#>
SELECT ?ancestor WHERE {
<https://example.org/vocab/shacl> skos:broaderTransitive ?ancestor .
}
That returns both :ontology and :knowledge-graph, though only the
first was asserted.
Search works on the inferred labels too: Shapes Constraint finds
:shacl through its skos:altLabel, and validating
finds it through the definition that became an rdfs:comment. Search is fuzzy, so
near-misses still land β see how search ranks.
What gets rejected
The point of the shapes is that these fail before a human is asked to review anything. Each of these is refused at ingest:
| Attempted assertion | Rejected by |
|---|---|
A concept at https://elsewhere.example/thing | sh:pattern β outside your vocabulary namespace. |
A skos:Concept with no skos:definition | s:ConceptShape's sh:minCount 1. |
A concept with two skos:prefLabels | sh:maxCount 1 β SKOS's own integrity condition. |
A made-up predicate such as ex:invented | Linting, not SHACL β the term is not declared in SKOS. |
A resource typed skos:Collection | sh:in β SKOS declares it, your shapes do not admit it. |
Where to go next
- Extend the vocabulary. Add
classpath:ontology.ttlafterskos.ttlwith domain classes of your own, and let concepts carry properties SKOS does not model. - Point an agent at it. The MCP server guide covers
client setup; the agent reads
cortex://ontologyand grounds itself in SKOS automatically. - Make it durable. Scheduled backups and restore on startup let a replaceable instance come up fully seeded.