Onya is a knowledge graph data model and expression format, with a Python implementation. This repository combines the data model and format spec with the Python implementation. Onya grew out of a long-standing observation from the RDF community: the triple is too expressively weak in practice. Quads and named graphs are insufficient RDF-based workarounds; property graphs solve it for relationships but abandon the web: no IRIs, no shared vocabularies, no dereferenceability.
Onya takes annotatable, first-class relationships from the property graph world and keeps what RDF got right: IRIs throughout, and vocabulary reuse (schema.org works out of the box). Its key structural idea is recursive assertion: every edge and property can itself carry edges and properties, uniformly, with no RDF-style reification ceremony. Qualified values, relationship metadata, and n-ary relations work well through the same mechanism.
The Onya Literate format is Markdown-native, so easy for humans to write, and easy for LLMs to emit and consume, which makes Onya a natural interchange for LLMOps (extracted knowledge, agent context and more).
Note: Using our provided onya-graph.SKILL.md with your favorite AI coding agent is a good way to automate Onya authoring and explore the format. Try providing a text document and asking it to generate a graph therefrom.
The model in thirty seconds
Here's a complete, valid knowledge graph, first visualized:
Then in Onya Literate format:
# @docheader
* @document: http://example.org/friendship-graph
* @nodebase: http://example.org/people/
* @schema: https://schema.org/
# Chuks [Person]
* name: Chukwuemeka Okafor
* knows -> Ify
* startDate: 2018-03-15
* description: Met at university
# Ify [Person]
* name: Ifeoma Obasi
* jobTitle: Software Engineer
Notice how the knows edge carries its
own assertions: a start date and a description of the relationship itself.
No reification ceremony, no separate "edge properties" mechanism with its own
rules. Every edge and every property can carry further edges and properties,
recursively, through one uniform mechanism. Node IDs resolve against
@nodebase; bare labels and types resolve against @schema, so schema.org
(or any vocabulary) works out of the box.
How Onya relates to its conceptual neighbors
| RDF 1.1 | RDF 1.2 / RDF-star | Property graphs (Neo4j, GQL) | Onya | |
|---|---|---|---|---|
| Identifiers | IRIs | IRIs | Opaque internal IDs | IRIs throughout |
| Shared vocabularies (e.g. schema.org) | Native | Native | Ad hoc strings | Native |
| Metadata on relationships | Reification ceremony | Triple terms | Edge properties | Nested assertions |
| Metadata on property values | Reification ceremony | Awkward | Not expressible | Same nested assertions |
| Edges from a property/edge | No | No | No | Yes — the mechanism is uniform |
| Assertion identity | Statements are types (set semantics) | Contested (type vs. occurrence) | Edges are instances | Assertions are instances |
| Blank nodes | Yes | Yes | — | None |
| Value types in core | XSD literal system | XSD literal system | Implementation-defined | Strings; data contracts layered above (@as) |
| Human-writable serialization | Turtle | Turtle-star | None standard | Markdown-native |
| Query orientation | SPARQL (aggregate/pattern) | SPARQL-star | Cypher/GQL (path-first) | Traversal API; path language planned |
Two rows carry the heart of the distinction. Onya assertions are occurrences
by construction — each edge or property is an instance, sidestepping the
type-versus-occurrence ambiguity that complicated RDF-star standardization.
And the mechanism is uniform all the way down: a property value can carry
edges (a temperature with a measurementMethod link), which has no clean
analogue in either the RDF or property-graph lineage.
Values are strings, with data contracts layered above
Every Onya value is a string, and the string layer is unconditionally valid:
birthDate: spring 1958 is welcome exactly as written. Onya deliberately has
no built-in type system — in the XSD/OWL lineage, types describe how computers
store things, not what the things are, and data models built on them end up
rejecting true statements that don't fit the machinery.
Instead, an author may attach an interpretation: a recorded promise about
how a value is meant to be read, declared inline with @as or once per
property label in the docheader. Contracts are honored at boundaries, on
demand — never enforced ambiently by the model:
from onya.graph import graph
from onya.serial.literate import LiterateParser
from onya import interp
onya_text = '''
# @docheader
* @document: http://example.org/people-graph
* @nodebase: http://example.org/people/
* @schema: https://schema.org/
* @interpretations:
* age: number
# Chuks [Person]
* age: 28
* birthDate: spring 1958
* @as: datetime
'''
g = graph()
LiterateParser().parse(onya_text, g)
chuks = g['http://example.org/people/Chuks']
age = next(chuks.getprop('https://schema.org/age'))
age.value # '28' — the model always stores the string
interp.value_of(age) # 28 — the contract, honored where you ask
report = interp.validate(g)
report.ok # False: 'spring 1958' fails its datetime contract —
print(report) # a reported finding, never a parse error or rejection
An interpretation your software doesn't recognize is data, not an error: it
parses, merges, and round-trips untouched. The built-in starter set (number,
datetime, boolean, iri, text) is deliberately modest, and the plugin
registry lets any community define its own. If you're arriving from data
engineering, note that this is the value-level slice of "data contract" —
shape, ownership, and SLAs would be further layers, deliberately not this one.
The design rationale is in
doc/design-interpretations-strings-vs-typing.md.
Python quick start
Requires Python 3.12 or later. The package is still in active development, so you can install directly from source:
git clone https://github.com/OoriData/Onya.git
cd Onya
pip install -U .
The base install is dependency-light. Optional extras pull in peripherals only when you need
them: pip install "onya[nx]" for the networkx projection (analytics) and
pip install "onya[postgres]" for the PostgreSQL store backend.
Parse a small graph in Onya Literate format, then work with it, including adding assertions on an edge:
from onya.graph import graph
from onya.serial.literate import LiterateParser, write
onya_text = '''
# @docheader
* @document: http://example.org/friendship-graph
* @nodebase: http://example.org/people/
* @schema: https://schema.org/
# Chuks [Person]
* name: Chukwuemeka Okafor
# Ify [Person]
* name: Ifeoma Obasi
'''
g = graph()
LiterateParser().parse(onya_text, g)
chuks = g['http://example.org/people/Chuks']
ify = g['http://example.org/people/Ify']
# Edges are first-class: they can carry their own assertions
friendship = chuks.add_edge('https://schema.org/knows', ify)
friendship.add_property('https://schema.org/startDate', '2018-03-15')
# Traverse, reading nested assertions along the way
for edge in chuks.traverse('https://schema.org/knows'):
for name in edge.target.getprop('https://schema.org/name'):
print(f'Chuks knows: {name.value}')
for since in edge.getprop('https://schema.org/startDate'):
print(f' Friends since: {since.value}')
# Round-trip back to Onya Literate
write(g)
Finding the node a user means
Label arguments take a full IRI, a CURIE, or a bare @schema name, resolved against the
prefixes the parsed document declared (g.prefixes; an undeclared prefix raises rather than
guessing). g.search() is a ranked, typo-tolerant lookup by property value, and g.inbound()
answers "what points here?" from a reverse index instead of a scan:
chuks.any_prop_value('name') # -> 'Chukwuemeka Okafor' (same as 'schema:name')
hits = g.search('chukwuemka', types=['Person']) # tiers: exact > prefix > word > fuzzy
hits[0].node_id, hits[0].tier, hits[0].score # ranked list; ambiguity is the caller's call
[e.origin.id for e in g.inbound(ify, label='knows')] # -> ['http://example.org/people/Chuks']
Once you've found the node, onya.view turns "show me this thing" into data — which
fields, in what order, which links to follow (outbound, inbound, or edges on the link itself),
and what to show of each — leaving the rendering to you:
from onya import view
specs = view.load([{'type': 'Person', 'fields': ['name'],
'follow': [{'edge': 'knows', 'show': ['name']}]}])
view.project(g, chuks, specs)
# {'id': '.../Chuks', 'type': 'Person', 'label': 'Chukwuemeka Okafor',
# 'fields': [('name', 'Chukwuemeka Okafor')],
# 'knows': [{'id': '.../Ify', 'label': 'Ifeoma Obasi', 'fields': [('name', 'Ifeoma Obasi')]}]}
Stores offer the same lookup without loading a graph (await store.search(name, 'chukwuemka', labels=[...]), store.nodes_by_type(name, type_iri)); PostgreSQL serves it from a pg_trgm
trigram index. await view.project_from_store(store, name, node_id, specs)
fetches just the neighborhood a view needs, so "search → project" is a handful of small queries.
Persistence
onya.store keeps graphs across sessions and processes. It is a peripheral, not
an organ: the core model never imports it, and a store is correct exactly when
a round trip through it is indistinguishable from an in-memory graph union —
put(merge=True) unions with any stored graph under the SPEC merge rules. Three
backends ship, all behind one async protocol and selected by URL scheme:
import asyncio
from onya.store import connect
from onya.serial.literate import read
r = read(open('test/resource/schemaorg/thingsfallapart.onya'))
g, name = r.graph, r.doc_iri
async def main():
# Filesystem — one Onya Literate file per graph; the default, and the testing fake.
async with await connect('file:/tmp/onya-graphs') as store:
await store.put(name, g) # merge=True by default
again = await store.get(name) # -> onya.graph.graph
print([str(n) async for n in store.names()])
# SQLite — stdlib, zero added dependencies; also an AssertionStore.
async with await connect('sqlite:/tmp/onya.db') as store:
await store.put(name, g)
async for origin, rel, target, ann in store.match(name, 'http://example.org/classics/CAchebe'):
print(origin, rel, target)
# PostgreSQL — pip install "onya[postgres]"; SQL/PGQ property graphs on PG >= 19.
async with await connect('postgresql://user:pass@localhost/onya') as store:
await store.put(name, g)
asyncio.run(main())
A blocking facade (from onya.store.sync import connect) mirrors the API with
with/plain calls for scripts and REPL use. Design and schema details are in
doc/design-persistence-architecture.md.
Visualize / export
The CLI converts Onya Literate (.onya) files to diagram formats:
onya convert test/resource/schemaorg/thingsfallapart.onya > out.mmd # Mermaid (default)
onya convert test/resource/schemaorg/thingsfallapart.onya --dot > out.dot # Graphviz DOT
View Mermaid output instantly at mermaid.live, producing e.g.:
networkx projection + analytics round trip
For graph analytics, onya.viz.nx (extras-gated: pip install "onya[nx]") projects a graph
into a networkx.MultiDiGraph, and write_back records results — centrality, community, any
networkx output — back into the Onya graph as typed, merge-safe assertions. The projection is
lossy by design (first-level structure) and reflects the graph as-is; call g.merge() first
for a normalized view.
import networkx
from onya.viz import nx
from onya.terms import ONYA_INTERP
mg = nx.to_networkx(g) # -> networkx.MultiDiGraph
scores = networkx.betweenness_centrality(mg)
nx.write_back(g, 'https://example.org/betweenness', scores, interp=ONYA_INTERP('number'))
# scores are now first-class Onya assertions: queryable via g.select, merge-safe on store.put
See demo/nx_analytics/ for a runnable end-to-end example.
For the full API walkthrough — modifying and removing properties, querying by type, data contracts and validation, explicit graph merge, the serializer modules and their options — see doc/python-tutorial.md.
Acknowledgments
| Onya is primarily developed by the crew at Oori Data. We offer LLMOps, data pipelines and software engineering services around AI/LLM applications. |
Background
Onya is based on experience from developing Versa and also working on the MicroXML spec and implementations thereof.
The URL used for metavocabulary is managed via purl.org.
The name is from Igbo "ọ́nyà", web, snare, trap, and by extension, network. The expanded sense is ọ́nyà úchè, web of knowledge.
Contributing
Contributions welcome! We're interested in feedback from the community about what works and what doesn't in real-world usage. To get help with the code implementation, read CONTRIBUTING.md.
License
- Code (Python library): Apache 2.0 - See LICENSE
- Specification (SPEC.md): Creative Commons Attribution 4.0 International (CC BY 4.0) - See LICENSE-spec
The specification is under CC BY 4.0 to encourage broad adoption and derivative work while ensuring attribution. We want the format itself to be as open and reusable as possible, allowing anyone to create implementations in any language or adapt the format for their specific needs.
Related Work
- networkx: Network Analysis in Python — Onya bridges to it directly via
onya.viz.nx(projection + analytics write-back) - Apache AGE: PostgreSQL Extension for graphs. ANSI SQL & openCypher over the same DB.
Release files for Onya 0.5.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| onya-0.5.0.tar.gz | 373.1 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| onya-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 504.8 kB
Release files / onya-0.5.0.tar.gz
| Download URL | onya-0.5.0.tar.gz |
|---|---|
| Size | 373.1 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
ae0abe1637b8c66a32dab121b176ded8c8a3c9bb125d78c95d68d081877c943b
|
|
BLAKE2b-256 checksum How to use checksums |
78d67ce34dca56e13485b80b9df523e54902a3e7acc56a1601c131820610b6d8
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency logRelease files / onya-0.5.0-py3-none-any.whl
| Download URL | onya-0.5.0-py3-none-any.whl |
|---|---|
| Size | 131.7 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
64f00744c9375b94b9121f2b6715b2cf985389b2256a0932727e5e6ba37bc109
|
|
BLAKE2b-256 checksum How to use checksums |
408c69131ff3d55ecc77d3caed8b43a5fffa2b83756fbf2da37352857f6dda84
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Sep 25, 2026.
Transparency log