Open Ontology is a digital twin platform that unifies disparate business data sources into a queryable knowledge graph. This document provides a formal specification of the data model, query language, and core concepts.
Open Ontology stores all data as triples—atomic facts in the form:
This is also known as the Entity-Attribute-Value (EAV) model. Every piece of information in the system is represented as a statement about some entity.
Example facts about an employee:
The triple model provides several advantages over traditional relational tables:
| Aspect | Traditional Tables | Triple Store |
|---|---|---|
| Schema changes | Require migrations | Add attributes freely |
| Sparse data | NULL columns everywhere | Store only what exists |
| Relationships | Foreign keys, join tables | First-class references |
| History | Manual audit tables | Built-in timestamps |
| Flexibility | Fixed columns | Any attribute on any entity |
Each triple in the database contains the following fields:
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier for this triple |
entityId | string | The entity this fact describes |
attribute | string | The property name (conventionally prefixed with :) |
value | TripleValue | The fact's value (typed) |
createdAt | number | Unix timestamp (milliseconds) when created |
createdBy | string? | Optional: who created this fact |
retractedAt | number? | Unix timestamp when soft-deleted (null = active) |
entityType | string? | Optional classification (e.g., "Employee") |
txId | string? | Transaction ID this triple belongs to |
Entity IDs should be namespaced to avoid collisions across data sources:
Attributes conventionally start with : (colon):
Assert — Add a new fact:
Retract — Soft-delete a fact (sets retractedAt):
Query — Retrieve facts by pattern:
Open Ontology supports six primitive value types:
Text values of any length.
Numeric values (integers or floating-point).
True or false values.
Timestamps stored as Unix milliseconds.
A pointer to another entity. This creates a graph edge.
Arbitrary JSON data for complex, nested structures.
Values are stored in typed columns in SQLite:
| Type | SQL Column |
|---|---|
| string | value_string |
| number | value_number |
| boolean | value_boolean |
| datetime | value_datetime |
| ref | value_string |
| json | value_json |
The value_type column discriminates which column contains the actual value.
Datalog is a declarative query language that makes complex relational queries feel like pattern matching. Open Ontology uses a JSON-based syntax.
Variables begin with ? and represent unknown values to be bound:
Attributes begin with : and identify properties:
A pattern clause matches triples in the database:
This matches any triple where:
?person:employee:name?nameShared variables create implicit joins:
This query joins people to their departments because ?person and ?dept appear in multiple patterns.
Filter results with comparison operators:
Supported operators: >, >=, <, <=, =, !=
Find entities that don't match a pattern:
This matches people who do not have :terminated set to true.
Match any of several patterns:
Find high-earning employees in the engineering department who haven't been terminated:
Group and aggregate results:
Supported aggregation operators: count, sum, avg, min, max
Namespaces provide isolation for multi-tenant deployments and multi-source data federation.
Entities in different namespaces can reference each other using the :same-as attribute:
This enables unified queries across data sources.
Object Types define the expected structure of entities, enabling validation and documentation.
Object types enable conformance checking:
Object types are versioned. When the schema changes, create a new version rather than modifying the existing one. This preserves historical validation semantics.
Link Types define typed, validated relationships between entities.
Use the link clause in Datalog queries:
Every fact in Open Ontology is timestamped, enabling queries against historical state.
A fact is active at time T if:
Group related assertions into atomic units with shared metadata.
txId| Method | Path | Description |
|---|---|---|
POST | /namespaces | Create namespace |
GET | /namespaces | List namespaces |
GET | /namespaces/:ns | Get namespace metadata |
DELETE | /namespaces/:ns | Delete namespace |
POST | /namespaces/:ns/triples | Assert triples |
DELETE | /namespaces/:ns/triples/:id | Retract triple |
GET | /namespaces/:ns/entities/:id | Get entity triples |
GET | /namespaces/:ns/entities/:id/history | Get entity history |
POST | /namespaces/:ns/query | Pattern query |
POST | /namespaces/:ns/query/as-of | Time-travel query |
POST | /namespaces/:ns/datalog | Datalog query |
All operations are wrapped in Effect for composability:
| Term | Definition |
|---|---|
| Triple | An atomic fact: [entity, attribute, value] |
| Entity | Something that facts can describe; identified by a string ID |
| Attribute | A property name, conventionally prefixed with : |
| Value | A typed datum (string, number, boolean, datetime, ref, or json) |
| Namespace | An isolated database for grouping related data |
| Object Type | A schema definition for entity validation |
| Link Type | A typed relationship definition between entities |
| Retraction | Soft-deletion by setting retractedAt timestamp |
| Transaction | A group of facts created atomically with shared metadata |
| Datalog | A declarative query language based on pattern matching |
| Variable | A query placeholder, prefixed with ? |
[entity, attribute, value]["emp:alice", ":employee:name", "Alice Chen"]["emp:alice", ":employee:email", "[email protected]"]["emp:alice", ":employee:department", "dept:engineering"]["emp:alice", ":employee:salary", 95000]["emp:alice", ":employee:start-date", 1704067200000]salesforce/account-123 # From Salesforcestripe/cus_abc # From Stripeinternal/emp-alice # Internal system:employee:name # Simple attribute:employee:department # Reference to another entity:_meta/type # System metadata (underscore prefix)yield* store.assert({ entityId: "emp:alice", attribute: ":employee:name", value: string("Alice Chen"), entityType: "Employee"})yield* store.retract(tripleId)const triples = yield* store.getEntity("emp:alice")import { string } from "@open-ontology/core"string("Hello, world")import { number } from "@open-ontology/core"number(42)number(3.14159)import { boolean } from "@open-ontology/core"boolean(true)boolean(false)import { datetime } from "@open-ontology/core"datetime(Date.now())datetime(new Date("2024-01-01").getTime())import { ref } from "@open-ontology/core"ref("dept:engineering")ref("salesforce/account-123")import { json } from "@open-ontology/core"json({ address: { street: "123 Main", city: "Boston" } }){ find: ["?var1", "?var2", ...], // Variables to return where: [...clauses...], // Pattern matching and filters aggregate?: [...], // Optional: aggregation orderBy?: [...], // Optional: sorting limit?: number, // Optional: limit results offset?: number // Optional: pagination}?person ?name ?age ?department:employee:name :employee:email :employee:salary :employee:department["?person", ":employee:name", "?name"]{ find: ["?name", "?deptName"], where: [ ["?person", ":employee:name", "?name"], ["?person", ":employee:department", "?dept"], // ?person appears twice ["?dept", ":department:name", "?deptName"] // ?dept used here too ]}[">=", "?age", 18]["!=", "?status", "inactive"]["<", "?salary", 100000]["not", ["?person", ":employee:terminated", true]]["or", [ ["?person", ":employee:role", "admin"], ["?person", ":employee:role", "superuser"]]]{ find: ["?name", "?salary"], where: [ ["?person", ":employee:name", "?name"], ["?person", ":employee:salary", "?salary"], ["?person", ":employee:department", "?dept"], ["?dept", ":department:name", "Engineering"], [">=", "?salary", 100000], ["not", ["?person", ":employee:terminated", true]] ], orderBy: [{ variable: "?salary", direction: "desc" }], limit: 10}{ find: ["?deptName", "?headCount", "?avgSalary"], where: [ ["?person", ":employee:department", "?dept"], ["?dept", ":department:name", "?deptName"], ["?person", ":employee:salary", "?salary"] ], aggregate: [ ["count", "?person", "?headCount"], ["avg", "?salary", "?avgSalary"] ]}const ns = yield* NamespaceManager// Create a namespaceyield* ns.create("salesforce", "Salesforce CRM data")// List all namespacesconst all = yield* ns.list()// Get services for a namespaceconst store = yield* ns.getStore("salesforce")const datalog = yield* ns.getDatalog("salesforce")// Delete a namespaceyield* ns.delete("salesforce")// In "salesforce" namespaceyield* store.assert({ entityId: "sf/account-123", attribute: ":account:name", value: string("Acme Corp")})// In "stripe" namespaceyield* store.assert({ entityId: "stripe/cus_789", attribute: ":same-as", value: ref("sf/account-123")}){ type: "Employee", version: 1, description: "An employee in the organization", attributes: { name: { type: "string", required: true, description: "Full legal name" }, email: { type: "string", required: true, validation: { format: "email" } }, salary: { type: "number", validation: { min: 0 } }, startDate: { type: "datetime", required: true } }, relationships: { department: { target: "Department", cardinality: "one" }, directReports: { target: "Employee", cardinality: "many" } }}const result = yield* objectTypeService.validateConformance( "emp:alice", "Employee", 1)// result.conforms: boolean// result.missingAttributes: string[]// result.invalidAttributes: ValidationIssue[]{ name: "manages", sourceType: "Employee", targetType: "Employee", cardinality: "one-to-many", description: "Manager supervises employees", properties: { since: { type: "datetime", required: true }, isPrimary: { type: "boolean", required: false } }}// Create a link instanceyield* linkService.create({ type: "manages", source: "emp:alice", target: "emp:bob", properties: { since: Date.now(), isPrimary: true }}){ find: ["?manager", "?employee"], where: [ ["link", "manages", "?manager", "?employee"] ]}createdAt <= T AND (retractedAt IS NULL OR retractedAt > T)// Get entity state at a specific point in timeconst pastState = yield* store.queryAsOf( { entityId: "emp:alice" }, new Date("2024-06-15").getTime())// Retrieve all facts ever recorded about an entityconst history = yield* store.history("emp:alice")// Includes both active and retracted facts// Sorted by createdAtconst result = yield* store.transact([ { entityId: "emp:alice", attribute: ":name", value: string("Alice Chen") }, { entityId: "emp:alice", attribute: ":email", value: string("[email protected]") }, { entityId: "emp:alice", attribute: ":department", value: ref("dept:eng") }], { user: "import-service" })// result.txId — Shared transaction ID// result.triples — All created triples{ find: ["?entity", "?attr", "?value"], where: [ ["?entity", "?attr", "?value", "?tx"], ["=", "?tx", "tx-abc123"] ]}import { Effect, Layer } from "effect"import { TripleStore, Datalog, NamespaceManager } from "@open-ontology/core"const program = Effect.gen(function* () { const store = yield* TripleStore const datalog = yield* Datalog yield* store.assert({...}) const results = yield* datalog.query({...}) return results})