WeaveDocs
Gnosis

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

Tip

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.

GoodBad
did:oas:agent:7f3a2b...Alice
urn:item:product:42the red one
weave://strand/d7a3.../block/12block_12

Predicates: verb phrases, lowercase, snake_case

Predicates are the indexable axis you will reach for most. Choose a small, controlled vocabulary.

GoodBad
trustsTrusts
has_rolehasRole
authored_byauthor (verb-form is clearer)
mentioned_inmentions (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 valueEncoding
IntegerDecimal, no leading zeros: "42"
FloatFixed precision: "42.000"
Boolean"true" / "false"
DateISO-8601: "2026-05-14"
TimestampRFC 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_for

This 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-patternWhy it hurtsFix
Mutable subject ids (handles, emails)Breaks every existing triple when the id changesUse stable opaque ids
Variant predicate spellings (trusts / Trusts)Index entries split; queries missLowercase, snake_case constants
Free-form object stringsPattern queries return nothing usefulCanonicalize encoding before insert
Storing huge text bodies in objectsInflates the Strand, slows replicationStore a content hash in Gnosis; keep the body in Strand Blobs or Locus
Wildcard scans on production hot pathsO(N) costAdd 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:

  1. Add the new predicate alongside the old one with a backfill job.
  2. Switch readers to query both predicates and prefer the new one.
  3. After a deprecation period, stop writing the old predicate.

The Strand keeps the full history, so you can always rebuild a historical view.