Skip to content

First Schema Application Guide

This guide walks through applying a real OWL ontology to OWLGraph, loading data, and querying with OWL2 RL reasoning. It’s written for teams with existing OWL ontologies (like THEO’s BFO-aligned theology ontology) who want to use OWLGraph as their graph database.

Before You Start: What OWLGraph Supports Today

Section titled “Before You Start: What OWLGraph Supports Today”

Be aware of these constraints:

FeatureStatusNotes
Turtle format (.ttl)SupportedContent-Type: text/turtle
OWL/XML format (.owl)SupportedContent-Type: application/owl+xml (auto-detected)
RDF/XMLNot yet supportedConvert to Turtle or OWL/XML first
owl:importsNot resolved automaticallyMerge imported ontologies manually
OWL2 RL profileFully supportedSee OWL2 RL Profile
Full OWL DLNot supportedCardinality restrictions, nominals, etc. are parsed but not enforced at runtime
Subsumption queriesSupportedtype(Parent) returns all subclass instances
Property hierarchiesSupported via subPropertyOfWrite-time materialization
Inverse/symmetric/transitiveSupportedMaterialized at write time
DisjointnessSupportedEnforced at write time (rejects violations)
owl:hasKeyParsed; predicates auto-indexed@index(exact) is added so eq() lookups work without manual /alter. Uniqueness is not enforced.
Cardinality restrictionsParsed but not enforcedOWL2 RL limitation

OWLGraph natively supports OWL/XML format — no conversion needed. You can upload your .owl file directly:

Terminal window
curl -X POST https://YOUR-INSTANCE/ontology \
-H "Content-Type: application/owl+xml" \
--data-binary @theology.owl

The format is auto-detected — if the file starts with <?xml or <rdf:RDF, OWLGraph uses the OWL/XML parser. Otherwise it uses the Turtle parser. You can also explicitly set the Content-Type.

If you prefer Turtle format, convert with:

Terminal window
# Using robot (CLI)
robot convert --input theology.owl --output theology.ttl --format ttl
# Using Python (rdflib)
python3 -c "from rdflib import Graph; g=Graph(); g.parse('theology.owl'); g.serialize('theology.ttl', format='turtle')"

OWLGraph does not resolve owl:imports declarations. You need to merge all imported ontologies into a single Turtle file, or load them in order.

Option A: Merge everything into one file (recommended)

Terminal window
# Using robot
robot merge \
--input bfo-core.owl \
--input cco-agent.owl \
--input cco-event.owl \
--input theology.owl \
--output theology-merged.ttl \
--format ttl

Option B: Load in dependency order

Load base ontologies first, then your domain ontology:

Terminal window
# 1. Load BFO (foundation)
curl -X POST https://YOUR-INSTANCE/ontology \
-H "Content-Type: text/turtle" \
--data-binary @bfo.ttl
# 2. Load your domain ontology (references BFO classes)
curl -X POST https://YOUR-INSTANCE/ontology \
-H "Content-Type: text/turtle" \
--data-binary @theology.ttl

Important: Each ontology load replaces the previous one. If you use Option B, the second load must include all classes/properties from the first. Option A (merge) is simpler and recommended.

OWLGraph parses but doesn’t enforce some OWL DL constructs. You can leave them in (they’re ignored gracefully) or strip them to keep the ontology clean:

  • owl:cardinality, owl:minCardinality, owl:maxCardinality — ignored
  • owl:hasKey — parsed but not enforced
  • owl:oneOf (nominals) — not supported

The ontology will load successfully with these present — they just won’t have runtime effects.

Terminal window
curl -X POST https://YOUR-INSTANCE/ontology \
-H "Content-Type: text/turtle" \
-H "X-OWLGraph-Key: YOUR-API-KEY" \
--data-binary @theology-merged.ttl

Response:

{
"classes": 45,
"objectProperties": 63,
"dataProperties": 32,
"compiledSchema": "...",
"status": "success",
"message": "Ontology loaded and materializer activated"
}
  1. Go to your database detail page
  2. Click the Ontology tab
  3. Paste Turtle content or upload a .ttl file
  4. Click Apply Ontology

Test your ontology before committing:

Terminal window
curl -X POST "https://YOUR-INSTANCE/ontology?validate=true" \
-H "Content-Type: text/turtle" \
--data-binary @theology-merged.ttl

Returns the compiled schema and any validation errors without applying changes.

Using your API key through the OWLGraph proxy:

scripts/4-apply-ontology.ts
const fs = require('fs');
const API_BASE = 'https://api.owlgraph.ai/api/v1';
const DB_ID = '7f34a7a8-...'; // your database ID
const API_KEY = 'owlg_sk_...'; // your API key
async function applyOntology() {
const ttl = fs.readFileSync('theology-merged.ttl', 'utf8');
const resp = await fetch(`${API_BASE}/databases/${DB_ID}/ontology`, {
method: 'POST',
headers: {
'Content-Type': 'text/turtle',
'X-OWLGraph-Key': API_KEY,
},
body: ttl,
});
const data = await resp.json();
console.log(`Loaded: ${data.classes} classes, ${data.objectProperties} object properties`);
if (data.status !== 'success') {
throw new Error(data.message || 'Failed to apply ontology');
}
}
applyOntology();
  1. Parse — Turtle file parsed into OWL internal representation
  2. Validate — OWL2 RL profile checked (circular subclass, conflicting characteristics)
  3. Compile — Classes become Dgraph types, properties become predicates
  4. Apply — Schema applied to the database
  5. Persist — Ontology saved to disk for restart recovery
  6. Activate — Reasoning engine initialized for write-time materialization
  7. Retroactive — Existing data re-materialized with new ontology
Terminal window
# List all classes
curl "https://YOUR-INSTANCE/ontology/introspect" \
-H "X-OWLGraph-Key: YOUR-API-KEY"
# Inspect a specific class
curl "https://YOUR-INSTANCE/ontology/introspect?class=BiblicalVerse" \
-H "X-OWLGraph-Key: YOUR-API-KEY"

Or via the API proxy:

Terminal window
curl "https://api.owlgraph.ai/api/v1/databases/DB_ID/introspect?class=BiblicalVerse" \
-H "X-OWLGraph-Key: YOUR-API-KEY"

Yes, with caveats. Re-applying the same ontology is safe — it replaces the schema and re-activates the reasoning engine. Existing data is retroactively materialized with the new ontology.

However:

  • If you remove a class that has existing data, the data remains but won’t be reasoned about
  • If you change a class hierarchy, retroactive materialization will update type edges
  • Adding new classes/properties is always safe

OWLGraph uses Dgraph-style JSON mutations:

Terminal window
curl -X POST "https://YOUR-INSTANCE/mutate?commitNow=true" \
-H "Content-Type: application/json" \
-H "X-OWLGraph-Key: YOUR-API-KEY" \
-d '{
"set": [
{
"uid": "_:gen1_1",
"dgraph.type": "BiblicalVerse",
"osisRef": "Gen.1.1",
"text": "In the beginning God created the heavens and the earth.",
"inChapter": { "uid": "_:gen1" },
"inBook": { "uid": "_:genesis" }
},
{
"uid": "_:gen1",
"dgraph.type": "BiblicalChapter",
"chapterNumber": 1,
"inBook": { "uid": "_:genesis" }
},
{
"uid": "_:genesis",
"dgraph.type": "BiblicalBook",
"bookName": "Genesis",
"testament": "Old"
}
]
}'

What OWLGraph infers automatically:

  • Type hierarchy: if BiblicalVerse ⊑ BiblicalText ⊑ InformationContentEntity, all ancestor types are materialized.
  • Inverse properties: if inBook owl:inverseOf hasChapter, the reverse edge is created.
  • Property hierarchy: if quotes rdfs:subPropertyOf intertextuallyRelatedTo, writing a quotes edge also materializes intertextuallyRelatedTo.
  • Transitive closure: for any property declared owl:TransitiveProperty, the engine closes over the chain — both within the batch and against already-persisted edges (via the predicate’s @reverse index).
  • Symmetric and chain properties: owl:SymmetricProperty and owl:propertyChainAxiom are materialized at write time.
  • Domain / range inference: using inBook on a node infers its domain type.
  • Disjointness validation: owl:disjointWith is enforced across mutations and across the type hierarchy — writing Cat to a node already typed Dog is rejected.
Section titled “Batch Loading (Recommended for Large Datasets)”

For ~850K nodes, load in batches of 500-1000 mutations:

scripts/5-load-data.ts
const API_BASE = 'https://api.owlgraph.ai/api/v1';
const DB_ID = '...';
const API_KEY = 'owlg_sk_...';
const BATCH_SIZE = 500;
async function loadData(nodes: any[]) {
for (let i = 0; i < nodes.length; i += BATCH_SIZE) {
const batch = nodes.slice(i, i + BATCH_SIZE);
const resp = await fetch(`${API_BASE}/databases/${DB_ID}/mutate`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-OWLGraph-Key': API_KEY,
},
body: JSON.stringify({ set: batch }),
});
if (!resp.ok) {
const err = await resp.json();
throw new Error(`Batch ${i}: ${err.error || resp.statusText}`);
}
console.log(`Loaded ${Math.min(i + BATCH_SIZE, nodes.length)} / ${nodes.length}`);
}
}

OWLGraph uses Dgraph UIDs internally. For idempotent loading, use external IDs with @upsert:

Terminal window
# First, add an index on your identifier predicate
curl -X POST "https://YOUR-INSTANCE/alter" \
-d 'osisRef: string @index(exact) @upsert .'
# Then use the identifier for upserts
curl -X POST "https://YOUR-INSTANCE/mutate?commitNow=true" \
-H "Content-Type: application/json" \
-d '{
"query": "{ v as var(func: eq(osisRef, \"Gen.1.1\")) }",
"set": [
{
"uid": "uid(v)",
"dgraph.type": "BiblicalVerse",
"osisRef": "Gen.1.1",
"text": "In the beginning..."
}
]
}'

This creates the node if it doesn’t exist, or updates it if it does.

Forward references are handled automatically. You don’t need to load Books before Chapters before Verses. Blank nodes (_:label) create new nodes, and edges between them are resolved within the same mutation batch.

However, for very large datasets, loading in logical order (Books → Chapters → Verses → CrossReferences) helps with debugging and progress tracking.

LanguageEndpointBest For
DQL (Dgraph Query Language)/queryComplex graph traversals, aggregations
GraphQL/graphqlTyped queries, auto-generated from ontology
# All verses in Genesis
{
verses(func: type(BiblicalVerse)) @filter(has(inBook)) {
osisRef
text
inBook @filter(eq(bookName, "Genesis")) {
bookName
}
}
}
# Subsumption: all InformationContentEntity (returns Verses, Chapters, etc.)
{
q(func: type(InformationContentEntity)) {
dgraph.type
osisRef
bookName
}
}
# Cross-references from a verse (multi-hop)
{
q(func: eq(osisRef, "Isa.53.5")) {
osisRef
text
fulfilledBy {
osisRef
text
inBook { bookName }
}
}
}
# Transitive path: follow intertextuallyRelatedTo
{
q(func: eq(osisRef, "Gen.3.15")) {
osisRef
intertextuallyRelatedTo* {
osisRef
dgraph.type
}
}
}

The ontology compiles to a GraphQL schema automatically:

query {
queryBiblicalVerse(filter: { osisRef: { eq: "Gen.1.1" } }) {
osisRef
text
inBook { bookName }
inChapter { chapterNumber }
}
}

If theo:quotes is a rdfs:subPropertyOf theo:intertextuallyRelatedTo, and both properties are in the ontology, then:

  • Querying intertextuallyRelatedTo will include quotes results (via subsumption on property hierarchies)
  • This is handled by the materializer — when a quotes edge is written, an intertextuallyRelatedTo edge is also materialized

No official SDK yet. Use fetch:

async function query(dql: string) {
const resp = await fetch(`${API_BASE}/databases/${DB_ID}/query`, {
method: 'POST',
headers: {
'Content-Type': 'application/dql',
'X-OWLGraph-Key': API_KEY,
},
body: dql,
});
return resp.json();
}
// Usage
const result = await query(`{
q(func: eq(osisRef, "Gen.1.1")) {
text
inBook { bookName }
}
}`);
console.log(result.data.q);

For gRPC, use the dgo client library — all Dgraph clients are compatible with OWLGraph.

Safe and non-destructive. Re-apply the ontology with additions:

Terminal window
# Updated ontology with new class
curl -X POST "https://YOUR-INSTANCE/ontology" \
-H "Content-Type: text/turtle" \
--data-binary @theology-v2.ttl

Existing data is preserved. The new classes/properties are added to the schema, and retroactive materialization runs for any data affected by the changes.

  • Adding a superclass: Safe — retroactive materialization adds the new ancestor type to existing nodes
  • Removing a superclass: The old type edges remain but won’t be maintained on new writes
  • Adding restrictions: Parsed but cardinality restrictions aren’t enforced at runtime
  • Changing domain/range: New writes use the updated inference; existing data isn’t retroactively modified

There’s no built-in migration system. Recommended approach:

  1. Keep your ontology in version control
  2. Use ?validate=true to test changes before applying
  3. Apply the updated ontology (replaces the previous one)
  4. The reasoning engine re-initializes with the new ontology
  5. Retroactive materialization handles existing data
scripts/pipeline.ts
import { readFileSync } from 'fs';
const API = 'https://api.owlgraph.ai/api/v1';
const DB_ID = process.env.OWLGRAPH_DB_ID!;
const KEY = process.env.OWLGRAPH_API_KEY!;
const headers = { 'X-OWLGraph-Key': KEY };
async function applyOntology() {
console.log('Applying ontology...');
const ttl = readFileSync('theology-merged.ttl', 'utf8');
const resp = await fetch(`${API}/databases/${DB_ID}/ontology`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'text/turtle' },
body: ttl,
});
const data = await resp.json();
if (data.status !== 'success') throw new Error(data.message);
console.log(` ${data.classes} classes, ${data.objectProperties} object properties`);
}
async function loadData() {
console.log('Loading data...');
// Your ETL logic here — batch mutations
}
async function validate() {
console.log('Validating...');
const resp = await fetch(`${API}/databases/${DB_ID}/introspect`, { headers });
const data = await resp.json();
console.log(` ${data.classes?.length || 0} classes loaded`);
// Verify a sample query
const qResp = await fetch(`${API}/databases/${DB_ID}/query`, {
method: 'POST',
headers: { ...headers, 'Content-Type': 'application/dql' },
body: '{ q(func: type(BiblicalBook), first: 3) { bookName } }',
});
const qData = await qResp.json();
console.log(` Sample books: ${JSON.stringify(qData.data?.q)}`);
}
async function main() {
await applyOntology();
await loadData();
await validate();
console.log('Pipeline complete.');
}
main().catch(console.error);

Run with:

Terminal window
OWLGRAPH_DB_ID=your-db-id OWLGRAPH_API_KEY=owlg_sk_... npx tsx scripts/pipeline.ts