Properties & Relationships

FOLIO classes aren’t just an isolated taxonomy — they’re connected to each other by object properties that express semantic relationships like “Actor / Player → folio:drafted → Document / Artifact” or “Actor / Player → folio:observed → Event”. The library exposes 175 such properties as first-class OWLObjectProperty instances, plus the full set of 130,427 raw RDF triples for direct graph traversal when you need to go below the OWLClass layer.

This page covers the three APIs you’ll actually use: get_all_properties / get_property for property metadata, find_connections for semantic relationship traversal, and the get_triples_by_* family for lower-level RDF analysis.

Object properties overview

Object properties in FOLIO are first-class citizens, not just edge labels. Each one is parsed into its own OWLObjectProperty Pydantic model with its own IRI, label, domain, range, and (sometimes) a definition and examples. When you call find_connections, you get back tuples of (OWLClass, OWLObjectProperty, OWLClass) — real model instances on both ends and in the middle.

FOLIO 2.0.0 ships with 175 object properties, accessible as f.object_properties (a mutable List[OWLObjectProperty]) or via the get_all_properties() / get_property() / get_properties_by_label() helpers described below.

The OWLObjectProperty model has the following fields (defined in folio/models.py):

FieldTypeDescription
iristrFull FOLIO IRI of the property (required).
labelOptional[str]rdfs:label — e.g. hasFigure, folio:drafted.
sub_property_ofList[str]Parent-property IRIs (rdfs:subPropertyOf).
domainList[str]rdfs:domain — class IRIs this property can be applied to.
rangeList[str]rdfs:range — class IRIs this property can point to.
inverse_ofOptional[str]owl:inverseOf — IRI of the inverse property.
preferred_labelOptional[str]skos:prefLabel.
alternative_labelsList[str]skos:altLabel.
definitionOptional[str]skos:definition.
examplesList[str]skos:example.

A few things to know before you start wiring this into your code:

  • Many properties have empty domain and range lists. At the time of writing, 128 of the 175 properties in FOLIO 2.0.0 are declared without any rdfs:domain or rdfs:range constraint — they’re valid, they just aren’t restricted at the ontology level. Only 47 properties carry some domain or range metadata. Don’t assume every property tells you which classes it connects; use find_connections for that instead.
  • OWLObjectProperty has no to_* serializer methods. Unlike OWLClass — which has to_json(), to_markdown(), to_owl_xml(), and to_jsonld()OWLObjectProperty only exposes is_valid() and __str__(). To serialize a property to JSON, call Pydantic’s model_dump_json() directly.
from folio import FOLIO

f = FOLIO()
prop = f.get_property("https://folio.openlegalstandard.org/R0q5hTo2yTMlnIAbmFnwCH")
print(prop.model_dump_json(indent=2))

# Output:
# {
#   "iri": "https://folio.openlegalstandard.org/R0q5hTo2yTMlnIAbmFnwCH",
#   "label": "hasFigure",
#   "sub_property_of": [
#     "https://folio.openlegalstandard.org/RCBqIJm4IPngJgyvh49kP62"
#   ],
#   "domain": [],
#   "range": [],
#   "inverse_of": null,
#   "preferred_label": null,
#   "alternative_labels": [],
#   "definition": null,
#   "examples": []
# }

If you try prop.to_json() or prop.to_markdown() you’ll get an AttributeError. Use model_dump_json() (or model_dump() for a plain dict) and you’re set.

Listing all properties

get_all_properties() returns a copy of f.object_properties as a plain List[OWLObjectProperty]. No filtering, no sorting — just the raw list in insertion order.

from folio import FOLIO

f = FOLIO()
props = f.get_all_properties()

print(f"Total object properties: {len(props)}")
for p in props[:3]:
    print(f"  {p.label!r:30s} {p.iri}")

# Output:
# Total object properties: 175
#   'hasFigure'                    https://folio.openlegalstandard.org/R0q5hTo2yTMlnIAbmFnwCH
#   'folio:opposed'                https://folio.openlegalstandard.org/R1L3IueMeHCrxnDlOU2dRg
#   'folio:observed'               https://folio.openlegalstandard.org/R1us3pQhG9zkEb39dZHByB

Note the labels: some properties (hasFigure) use a camelCase bare name while most (folio:observed, folio:drafted, folio:opposed) carry the folio: prefix in their rdfs:label itself. When you call get_properties_by_label() you have to match the label exactly as it appears in the ontology, prefix and all.

Looking up a property

get_property(iri)

get_property() takes either a full FOLIO IRI or an integer index into f.object_properties and returns the matching OWLObjectProperty, or None if nothing matches. IRIs are normalized the same way as class IRIs — legacy lmss.sali.org URLs and folio:/soli: prefixed names all resolve correctly.

# By IRI
prop = f.get_property("https://folio.openlegalstandard.org/R0q5hTo2yTMlnIAbmFnwCH")
print(prop)

# Output:
# OWLObjectProperty(label=hasFigure, iri=https://folio.openlegalstandard.org/R0q5hTo2yTMlnIAbmFnwCH)

# By integer position
first = f.get_property(0)
print(first)

# Output:
# OWLObjectProperty(label=hasFigure, iri=https://folio.openlegalstandard.org/R0q5hTo2yTMlnIAbmFnwCH)

get_properties_by_label(label)

get_properties_by_label() is an exact-match lookup keyed on the rdfs:label of each property. It returns a List[OWLObjectProperty] because the ontology allows (in principle) multiple properties with the same label — in practice you’ll usually get a list of length 1 or 0. Because many FOLIO property labels include the folio: prefix verbatim, you need to pass the prefix as part of the query string.

# Correct: label includes the 'folio:' prefix
drafted = f.get_properties_by_label("folio:drafted")
print(len(drafted), drafted[0] if drafted else None)

# Output:
# 1 OWLObjectProperty(label=folio:drafted, iri=https://folio.openlegalstandard.org/R6qohvM786wjw0MNQJg9Dq)

# Wrong: 'drafted' without the prefix does not match
print(f.get_properties_by_label("drafted"))

# Output:
# []

If you’re not sure whether a property label includes the prefix, iterate get_all_properties() once and search by .label.lower() — or just use find_connections with a label string, which tries get_property() first and then falls back to a label match.

Finding semantic connections

find_connections() is the high-level API for semantic relationship traversal. Given a subject class (and optionally a property and/or target class), it walks the cached triples, joins them back to real OWLClass and OWLObjectProperty instances, and returns a list of (subject, property, object) tuples.

def find_connections(
    subject_class: str | OWLClass,
    property_name: str | OWLObjectProperty | None = None,
    object_class: str | OWLClass | None = None,
) -> List[Tuple[OWLClass, OWLObjectProperty, OWLClass]]:
    ...

All three arguments are polymorphic:

  • subject_class — an OWLClass instance or an IRI string (full URI, folio: prefix, or short ID).
  • property_name — an OWLObjectProperty, an IRI string, or a label string like "folio:drafted". None (the default) returns every connection from subject_class regardless of property.
  • object_class — an OWLClass or an IRI string that narrows results to a specific target. None returns every matching target.

The big caveat: only root-type classes have outbound connections

Object-property triples in FOLIO are declared at the type root level — Actor / Player, Document / Artifact, Event, Legal Entity, and so on — not on individual subclasses. If you call find_connections on a leaf class like Michigan or even on a rich non-leaf like Contract Law, you’ll get an empty list back, and it’s easy to misread that as “the method is broken.”

# Leaf class: no outbound object-property triples
michigan = "https://folio.openlegalstandard.org/R8BD30978Ccbc4C2f0f8459f"
print(len(f.find_connections(michigan)))
# Output: 0

# Contract Law is a rich non-leaf class, but still no outbound triples
contract_law = "https://folio.openlegalstandard.org/RCIPwpgRpMs1eVz4vPid0pV"
print(len(f.find_connections(contract_law)))
# Output: 0

To get useful results, pass a FOLIO type root IRI — one of the 24 top-level branch roots. Actor / Player (R8CdMpOM0RmyrgCCvbpiLS0) is the most connected root in the 2.0.0 ontology and the easiest to demo against.

Example 1 — all connections from a class

With no property_name or object_class, find_connections returns every outbound semantic edge from the subject.

actor_player = "https://folio.openlegalstandard.org/R8CdMpOM0RmyrgCCvbpiLS0"

for subj, prop, obj in f.find_connections(actor_player):
    print(f"'{subj.label}' --{prop.label}--> '{obj.label}'")

# Output (14 connections):
# 'Actor / Player' --folio:observed--> 'Event'
# 'Actor / Player' --folio:observed--> 'Deposition'
# 'Actor / Player' --folio:drafted--> 'Document / Artifact'
# 'Actor / Player' --folio:received--> 'Document / Artifact'
# 'Actor / Player' --folio:asserted--> 'Legal Proposition'
# 'Actor / Player' --folio:asserted--> 'Factual Statement'
# 'Actor / Player' --folio:workedFor--> 'Legal Entity'
# 'Actor / Player' --folio:workedFor--> 'Actor / Player'
# 'Actor / Player' --folio:workedFor--> 'Forums and Venues'
# 'Actor / Player' --folio:cited--> 'Citation to Caselaw'
# 'Actor / Player' --folio:cited--> 'Document / Artifact'
# 'Actor / Player' --folio:cited--> 'Citations to Another Document'
# 'Actor / Player' --folio:cited--> 'Citation to Non-Legal-Authority Document'
# 'Actor / Player' --folio:participatedIn--> 'Event'

Each element is a real (OWLClass, OWLObjectProperty, OWLClass) tuple — you can read .definition, .alternative_labels, etc. off any of them.

Example 2 — connections via a specific property

Pass a label string (or an IRI, or an OWLObjectProperty) as property_name to narrow the results.

for subj, prop, obj in f.find_connections(
    actor_player, property_name="folio:drafted"
):
    print(f"'{subj.label}' --{prop.label}--> '{obj.label}'")

# Output:
# 'Actor / Player' --folio:drafted--> 'Document / Artifact'

Example 3 — connections to a specific target

Pass an object_class IRI (or instance) to filter by the target of the edge. This is the fastest way to answer “which properties connect A to B?”

document = "https://folio.openlegalstandard.org/RDt4vQCYDfY0R9fZ5FNnTbj"

for subj, prop, obj in f.find_connections(actor_player, object_class=document):
    print(f"'{subj.label}' --{prop.label}--> '{obj.label}'")

# Output:
# 'Actor / Player' --folio:drafted--> 'Document / Artifact'
# 'Actor / Player' --folio:received--> 'Document / Artifact'
# 'Actor / Player' --folio:cited--> 'Document / Artifact'

You can combine property_name and object_class to ask very targeted questions, or leave both None to enumerate everything the root class is connected to.

The triples API

Underneath OWLClass and OWLObjectProperty, FOLIO is parsed into 130,427 RDF triples that you can walk directly. The triples are exposed two ways:

  • f.triples — a mutable List[Tuple[str, str, str]]. Useful for ad-hoc iteration.
  • f._cached_triples — the same data frozen into a Tuple[Tuple[str, str, str], ...] so it can be used as a cache key. The get_triples_by_* methods below route through this frozen copy and a functools.cache-decorated filter, so repeated lookups are effectively free.

Each triple is a (subject, predicate, object) 3-tuple:

  • subject is always a full FOLIO IRI (or an object-property IRI for property-describing triples).
  • predicate is a prefixed name like rdfs:label, skos:prefLabel, dc:identifier, or folio:draftednot a full URI. There are 35 unique predicates in the 2.0.0 ontology.
  • object is either an IRI (for relationship triples like rdfs:subClassOf or folio:drafted) or a literal string (for things like rdfs:label, skos:definition, dc:identifier).
for t in f.triples[:3]:
    print(t)

# Output:
# ('https://folio.openlegalstandard.org/R0q5hTo2yTMlnIAbmFnwCH', 'rdfs:subPropertyOf', 'https://folio.openlegalstandard.org/RCBqIJm4IPngJgyvh49kP62')
# ('https://folio.openlegalstandard.org/R0q5hTo2yTMlnIAbmFnwCH', 'rdfs:label', 'hasFigure')
# ('https://folio.openlegalstandard.org/R1L3IueMeHCrxnDlOU2dRg', 'rdfs:subPropertyOf', 'https://folio.openlegalstandard.org/R7VtpAC807aOYLVtKmPvFRt')

Because the predicates are prefixed strings, you use prefixed strings when you filter — see the next section.

Triple lookups

All three lookup helpers share a single cached implementation (_filter_triples, decorated with functools.cache), so the first call for any (value, filter_by) combination walks the 130k-tuple list once and every subsequent call for the same argument is an O(1) dict hit.

get_triples_by_subject(iri)

Returns every triple whose subject equals iri. The input must be the full IRI (not a short ID), though normalize_iri isn’t applied here — pass the same canonical IRI you’d use with f[iri].

michigan = "https://folio.openlegalstandard.org/R8BD30978Ccbc4C2f0f8459f"

for t in f.get_triples_by_subject(michigan):
    print(t)

# Output:
# ('https://folio.openlegalstandard.org/R8BD30978Ccbc4C2f0f8459f', 'rdfs:subClassOf', 'https://folio.openlegalstandard.org/R1E70ce4D699e90144cB32b8')
# ('https://folio.openlegalstandard.org/R8BD30978Ccbc4C2f0f8459f', 'dc:identifier', 'NAM-US-US+MI')
# ('https://folio.openlegalstandard.org/R8BD30978Ccbc4C2f0f8459f', 'rdfs:label', 'Michigan')
# ('https://folio.openlegalstandard.org/R8BD30978Ccbc4C2f0f8459f', 'skos:hiddenLabel', 'US+MI')

Four triples: a rdfs:subClassOf edge up to the parent class, a dc:identifier literal, the rdfs:label, and a skos:hiddenLabel alias. The model-level view of this same class (f[michigan].model_dump()) packages all of this into structured fields, but the triple view is what you want when you’re doing low-level RDF analysis or debugging what actually got parsed.

get_triples_by_predicate(predicate)

Returns every triple with the given predicate. The predicate must be in prefixed-name form (rdfs:label, skos:prefLabel, dc:identifier, folio:drafted, …). Passing the full URI returns zero results — this is the single most common footgun when working with triples.

# Wrong: full URI returns nothing
print(len(f.get_triples_by_predicate(
    "http://www.w3.org/2000/01/rdf-schema#label"
)))
# Output: 0

# Right: prefixed form
print(len(f.get_triples_by_predicate("rdfs:label")))
# Output: 18498

print(len(f.get_triples_by_predicate("skos:hiddenLabel")))
# Output: 5117

The 35 predicates you’ll encounter include the usual RDFS/OWL suspects (rdfs:label, rdfs:subClassOf, rdfs:subPropertyOf, rdfs:domain, rdfs:range, rdfs:comment, rdfs:seeAlso, owl:deprecated, owl:inverseOf), the SKOS lexical predicates (skos:prefLabel, skos:altLabel, skos:hiddenLabel, skos:definition, skos:example), Dublin Core metadata (dc:identifier, dc:source), and the FOLIO-specific object-property labels (folio:drafted, folio:observed, folio:cited, folio:workedFor, …).

get_triples_by_object(value)

Returns every triple whose object equals value. The object is either an IRI (for relationship triples) or a literal string (for label-style triples), and this method matches by exact string equality either way.

# Object is an IRI — who points at Document / Artifact?
document = "https://folio.openlegalstandard.org/RDt4vQCYDfY0R9fZ5FNnTbj"
incoming = f.get_triples_by_object(document)
print(f"{len(incoming)} triples point at Document / Artifact")
for t in incoming[:3]:
    print(t)

# Output:
# 31 triples point at Document / Artifact
# ('https://folio.openlegalstandard.org/R6qohvM786wjw0MNQJg9Dq', 'rdfs:range', 'https://folio.openlegalstandard.org/RDt4vQCYDfY0R9fZ5FNnTbj')
# ('https://folio.openlegalstandard.org/R8CdMpOM0RmyrgCCvbpiLS0', 'folio:drafted', 'https://folio.openlegalstandard.org/RDt4vQCYDfY0R9fZ5FNnTbj')
# ('https://folio.openlegalstandard.org/R7IaFnjI5OndDVqgzfMPpco', 'rdfs:range', 'https://folio.openlegalstandard.org/RDt4vQCYDfY0R9fZ5FNnTbj')

# Object is a literal — which class has the label "Michigan"?
print(f.get_triples_by_object("Michigan"))

# Output:
# [('https://folio.openlegalstandard.org/R8BD30978Ccbc4C2f0f8459f', 'rdfs:label', 'Michigan')]

get_triples_by_object is the cheapest way to answer “who references X?” — reverse lookups that would otherwise require a full scan of f.triples.

When to use which

Use find_connections for semantic relationship traversal between classes: it does the work of joining triples to real OWLClass and OWLObjectProperty instances and hides the prefix-name weirdness. Just remember it only returns data for FOLIO type-root classes. Use the get_triples_by_* methods when you need lower-level RDF analysis, when you’re looking at non-object-property data (labels, identifiers, definitions, hidden labels), or when you want to do a reverse lookup by object value. Use get_all_properties / get_property / get_properties_by_label when you only need property metadata — domain, range, sub-property chain, definition — without doing any class-to-class traversal.

See also

See also: Taxonomy for class-to-class traversal through sub_class_of and parent_class_of (the sibling operation to object-property traversal), Querying for the query_properties() helper that filters object properties with structured filters, and API Reference for the full method catalog across FOLIO, OWLClass, and OWLObjectProperty.