Ontology Design
Practical guidance for naming subjects, predicates, and objects so your Gnosis graph stays queryable as it grows.
What this page covers
Gnosis is intentionally schema-less — it stores raw (subject, predicate, object) strings. The schema lives in your application. This page gives the practical rules for making that schema work at scale.
Naming conventions
Subjects: stable, opaque identifiers
Pick subject identifiers you will never want to change. URIs and DIDs work well; human-readable handles do not. A rename means inserting tombstone triples and migrating every caller — cheaper to design opaque identifiers up front.
Subjects should be identifiers that do not change. URIs and DIDs work well; human-readable handles do not. Renaming a subject means inserting tombstone triples and rebuilding callers.
| Good | Bad |
|---|---|
did:oas:agent:7f3a2b... | Alice |
urn:item:product:42 | the red one |
weave://strand/d7a3.../block/12 | block_12 |
Predicates: verb phrases, lowercase, snake_case
Predicates are the indexable axis you will reach for most. Choose a small, controlled vocabulary.
| Good | Bad |
|---|---|
trusts | Trusts |
has_role | hasRole |
authored_by | author (verb-form is clearer) |
mentioned_in | mentions (direction matters) |
Treat predicates like an enum: maintain a list in your code and resolve unknown predicates to a known set on ingest.
Objects: literals or identifiers, consistently
Within a predicate, keep object types uniform. has_role should always point at a role identifier; do not mix ("alice", "has_role", "admin") with ("alice", "has_role", "person who deploys things").
For numeric or boolean values, encode them as canonical strings:
| Logical value | Encoding |
|---|---|
| Integer | Decimal, no leading zeros: "42" |
| Float | Fixed precision: "42.000" |
| Boolean | "true" / "false" |
| Date | ISO-8601: "2026-05-14" |
| Timestamp | RFC 3339: "2026-05-14T10:00:00Z" |
Mixed encodings break pattern queries.
Reification: storing facts about facts
Gnosis triples have no built-in metadata. To attach provenance, time, or confidence to a fact, mint a synthetic subject for the fact and attach metadata to it:
// Fact: (alice, trusts, bob)
// Reified id: an opaque string you generate
let fact_id = format!("fact:{}", blake3::hash(b"alice|trusts|bob").to_hex());
g.add_triple(&fact_id, "rdf:subject", "alice").await?;
g.add_triple(&fact_id, "rdf:predicate", "trusts").await?;
g.add_triple(&fact_id, "rdf:object", "bob").await?;
g.add_triple(&fact_id, "asserted_by", "did:oas:agent:abc").await?;
g.add_triple(&fact_id, "asserted_at", "2026-05-14T10:00:00Z").await?;
g.add_triple(&fact_id, "confidence", "0.95").await?;This costs five extra triples per fact but unlocks "who said this and when?" queries.
Namespacing predicates
For a multi-tenant graph, prefix predicates with a domain namespace:
hr:has_role
hr:employs
billing:owes
billing:paid_by
trust:trusts
trust:vouches_forThis prevents collisions across teams and makes per-domain queries trivial: filter by predicate prefix in the caller.
Vocabulary as code
Keep predicates in code, not strings scattered across the codebase:
pub mod predicates {
pub const TRUSTS: &str = "trust:trusts";
pub const HAS_ROLE: &str = "hr:has_role";
pub const AUTHORED_BY: &str = "content:authored_by";
}
// Use it
g.add_triple("alice", predicates::TRUSTS, "bob").await?;Add a compile-time test that the vocabulary list is exhaustive against ingested data.
Anti-patterns
| Anti-pattern | Why it hurts | Fix |
|---|---|---|
| Mutable subject ids (handles, emails) | Breaks every existing triple when the id changes | Use stable opaque ids |
Variant predicate spellings (trusts / Trusts) | Index entries split; queries miss | Lowercase, snake_case constants |
| Free-form object strings | Pattern queries return nothing useful | Canonicalize encoding before insert |
| Storing huge text bodies in objects | Inflates the Strand, slows replication | Store a content hash in Gnosis; keep the body in Strand Blobs or Locus |
| Wildcard scans on production hot paths | O(N) cost | Add a more specific predicate or use Basis for similarity-based recall |
Composition with other primitives
Gnosis is the typed-relationship layer. Pair it with:
- Basis for semantic similarity. Gnosis tells you "Alice trusts Bob"; Basis tells you "documents like this one." Both can reference the same subject ids.
- Strand Blobs for the actual content. Gnosis stores
(post:42, has_body_hash, blake3:...); Strand Blobs stores the body. - Locus for filesystem-backed facts. Gnosis can declare
(file:/foo.txt, authored_by, alice)and Locus serves the bytes.
The shared id space is what makes the composition cheap; the same did:oas:agent:alice shows up in every primitive.
Migration
Schema changes happen. Gnosis has no built-in migration tool. The safe playbook:
- Add the new predicate alongside the old one with a backfill job.
- Switch readers to query both predicates and prefer the new one.
- After a deprecation period, stop writing the old predicate.
The Strand keeps the full history, so you can always rebuild a historical view.