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:
| Feature | Status | Notes |
|---|---|---|
| Turtle format (.ttl) | Supported | Content-Type: text/turtle |
| OWL/XML format (.owl) | Supported | Content-Type: application/owl+xml (auto-detected) |
| RDF/XML | Not yet supported | Convert to Turtle or OWL/XML first |
| owl:imports | Not resolved automatically | Merge imported ontologies manually |
| OWL2 RL profile | Fully supported | See OWL2 RL Profile |
| Full OWL DL | Not supported | Cardinality restrictions, nominals, etc. are parsed but not enforced at runtime |
| Subsumption queries | Supported | type(Parent) returns all subclass instances |
| Property hierarchies | Supported via subPropertyOf | Write-time materialization |
| Inverse/symmetric/transitive | Supported | Materialized at write time |
| Disjointness | Supported | Enforced at write time (rejects violations) |
| owl:hasKey | Parsed; predicates auto-indexed | @index(exact) is added so eq() lookups work without manual /alter. Uniqueness is not enforced. |
| Cardinality restrictions | Parsed but not enforced | OWL2 RL limitation |
Step 1: Prepare Your Ontology
Section titled “Step 1: Prepare Your Ontology”OWL/XML Support (Direct Upload)
Section titled “OWL/XML Support (Direct Upload)”OWLGraph natively supports OWL/XML format — no conversion needed. You can upload your .owl file directly:
curl -X POST https://YOUR-INSTANCE/ontology \ -H "Content-Type: application/owl+xml" \ --data-binary @theology.owlThe 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:
# 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')"Merge Imported Ontologies
Section titled “Merge Imported Ontologies”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)
# Using robotrobot merge \ --input bfo-core.owl \ --input cco-agent.owl \ --input cco-event.owl \ --input theology.owl \ --output theology-merged.ttl \ --format ttlOption B: Load in dependency order
Load base ontologies first, then your domain ontology:
# 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.ttlImportant: 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.
Strip Unsupported Constructs (Optional)
Section titled “Strip Unsupported Constructs (Optional)”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— ignoredowl:hasKey— parsed but not enforcedowl:oneOf(nominals) — not supported
The ontology will load successfully with these present — they just won’t have runtime effects.
Step 2: Apply the Schema
Section titled “Step 2: Apply the Schema”HTTP API
Section titled “HTTP API”curl -X POST https://YOUR-INSTANCE/ontology \ -H "Content-Type: text/turtle" \ -H "X-OWLGraph-Key: YOUR-API-KEY" \ --data-binary @theology-merged.ttlResponse:
{ "classes": 45, "objectProperties": 63, "dataProperties": 32, "compiledSchema": "...", "status": "success", "message": "Ontology loaded and materializer activated"}Via the Dashboard
Section titled “Via the Dashboard”- Go to your database detail page
- Click the Ontology tab
- Paste Turtle content or upload a
.ttlfile - Click Apply Ontology
Validate Without Applying
Section titled “Validate Without Applying”Test your ontology before committing:
curl -X POST "https://YOUR-INSTANCE/ontology?validate=true" \ -H "Content-Type: text/turtle" \ --data-binary @theology-merged.ttlReturns the compiled schema and any validation errors without applying changes.
Via the OWLGraph API (Programmatic)
Section titled “Via the OWLGraph API (Programmatic)”Using your API key through the OWLGraph proxy:
const fs = require('fs');
const API_BASE = 'https://api.owlgraph.ai/api/v1';const DB_ID = '7f34a7a8-...'; // your database IDconst 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();What Happens During Schema Application
Section titled “What Happens During Schema Application”- Parse — Turtle file parsed into OWL internal representation
- Validate — OWL2 RL profile checked (circular subclass, conflicting characteristics)
- Compile — Classes become Dgraph types, properties become predicates
- Apply — Schema applied to the database
- Persist — Ontology saved to disk for restart recovery
- Activate — Reasoning engine initialized for write-time materialization
- Retroactive — Existing data re-materialized with new ontology
Verify the Schema
Section titled “Verify the Schema”# List all classescurl "https://YOUR-INSTANCE/ontology/introspect" \ -H "X-OWLGraph-Key: YOUR-API-KEY"
# Inspect a specific classcurl "https://YOUR-INSTANCE/ontology/introspect?class=BiblicalVerse" \ -H "X-OWLGraph-Key: YOUR-API-KEY"Or via the API proxy:
curl "https://api.owlgraph.ai/api/v1/databases/DB_ID/introspect?class=BiblicalVerse" \ -H "X-OWLGraph-Key: YOUR-API-KEY"Is Schema Application Idempotent?
Section titled “Is Schema Application Idempotent?”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
Step 3: Load Data
Section titled “Step 3: Load Data”Mutation Format
Section titled “Mutation Format”OWLGraph uses Dgraph-style JSON mutations:
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 aquotesedge also materializesintertextuallyRelatedTo. - 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@reverseindex). - Symmetric and chain properties:
owl:SymmetricPropertyandowl:propertyChainAxiomare materialized at write time. - Domain / range inference: using
inBookon a node infers its domain type. - Disjointness validation:
owl:disjointWithis enforced across mutations and across the type hierarchy — writingCatto a node already typedDogis rejected.
Batch Loading (Recommended for Large Datasets)
Section titled “Batch Loading (Recommended for Large Datasets)”For ~850K nodes, load in batches of 500-1000 mutations:
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}`); }}Handling Identity and Upserts
Section titled “Handling Identity and Upserts”OWLGraph uses Dgraph UIDs internally. For idempotent loading, use external IDs with @upsert:
# First, add an index on your identifier predicatecurl -X POST "https://YOUR-INSTANCE/alter" \ -d 'osisRef: string @index(exact) @upsert .'
# Then use the identifier for upsertscurl -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.
Loading Order
Section titled “Loading Order”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.
Step 4: Query the Graph
Section titled “Step 4: Query the Graph”Query Languages
Section titled “Query Languages”| Language | Endpoint | Best For |
|---|---|---|
| DQL (Dgraph Query Language) | /query | Complex graph traversals, aggregations |
| GraphQL | /graphql | Typed queries, auto-generated from ontology |
DQL Examples
Section titled “DQL Examples”# 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 } }}GraphQL (Auto-Generated)
Section titled “GraphQL (Auto-Generated)”The ontology compiles to a GraphQL schema automatically:
query { queryBiblicalVerse(filter: { osisRef: { eq: "Gen.1.1" } }) { osisRef text inBook { bookName } inChapter { chapterNumber } }}Property Hierarchy in Queries
Section titled “Property Hierarchy in Queries”If theo:quotes is a rdfs:subPropertyOf theo:intertextuallyRelatedTo, and both properties are in the ontology, then:
- Querying
intertextuallyRelatedTowill includequotesresults (via subsumption on property hierarchies) - This is handled by the materializer — when a
quotesedge is written, anintertextuallyRelatedToedge is also materialized
TypeScript/JavaScript Client
Section titled “TypeScript/JavaScript Client”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();}
// Usageconst 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.
Step 5: Schema Evolution
Section titled “Step 5: Schema Evolution”Adding New Classes/Properties
Section titled “Adding New Classes/Properties”Safe and non-destructive. Re-apply the ontology with additions:
# Updated ontology with new classcurl -X POST "https://YOUR-INSTANCE/ontology" \ -H "Content-Type: text/turtle" \ --data-binary @theology-v2.ttlExisting data is preserved. The new classes/properties are added to the schema, and retroactive materialization runs for any data affected by the changes.
Modifying Existing Definitions
Section titled “Modifying Existing Definitions”- 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
Migration Strategy
Section titled “Migration Strategy”There’s no built-in migration system. Recommended approach:
- Keep your ontology in version control
- Use
?validate=trueto test changes before applying - Apply the updated ontology (replaces the previous one)
- The reasoning engine re-initializes with the new ontology
- Retroactive materialization handles existing data
Automation Script Template
Section titled “Automation Script Template”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:
OWLGRAPH_DB_ID=your-db-id OWLGRAPH_API_KEY=owlg_sk_... npx tsx scripts/pipeline.ts