Five epistemic tiers. Three Turtle modules. One storage-agnostic model.
weo: — prefix weo:, namespace https://inboundfound.github.io/weo-ontology/weo#, status v0.2 (open draft). This page documents the model itself: what a term means, where it's stored, and how it's allowed to relate to everything else.
FETCHED ⊳ — the signature hierarchy, in full
The five epistemic tiers
- judgment — a strategist's claim, labeled as one
- derivation — a model's output — method, model, confidence
- observation — a measured fact
- episode — happened at a time, immutable
- entity — a durable identity
Every term in weo sits at exactly one layer, and the layers are ordered. This ordering is the ontology's central discipline: it lets an agent assembling context filter to observations only, ignoring anything upstream of measurement.
- Entity — durable identity. A Website, URL, Term, Topic, Engine, Brand, Prompt, ConversionPoint, or SearchIntent — something that persists and can be referred to again tomorrow.
- Episode — something that happened at a time, and is immutable once it has happened. A Crawl, a SerpSnapshot, an LLMResponse, a ConversionEvent. Episodes don't get edited; a new episode gets appended.
- Observation — a measured fact attached to an entity or episode.
FETCHED,CITES,MENTIONS— these are recordings of what was seen, not interpretations of it. - Derivation — a model's output. Anything carrying
method,model, orconfidencelives here —IN_TOPICclassification, anembeddingRef. Derivations are traceable to the process that produced them. - Judgment — a strategist's claim, labeled as exactly that.
targetsIntent,attributedResponse— pre-click attribution is a judgment, not an observation, and the ontology insists on saying so.
An entity is what exists, an episode is what happened, an observation is what was measured about it, a derivation is what a model concluded, and a judgment is what a strategist claims. Nothing skips the queue.
Three modules, one schema file
weo ships as three Turtle modules plus one property-graph schema, each with a distinct job.
weo-core.ttl — the SEO substrate. Classes: Website, URL, Term, Crawl, SerpSnapshot, Topic, SearchPerformanceFact. Relations: FETCHED, LINKS_TO, REDIRECTS_TO, HAS_CANONICAL, RANKS_FOR (windowed rollups carrying datasetUri provenance), HAS_RESULT, IN_TOPIC.
weo-visibility.ttl — the xEO layer. Classes: Engine (with subclasses SearchEngine, GenerativeEngine, AnswerEngine, ConversationalAgent), Brand, Prompt, LLMResponse. Relations: CITES, MENTIONS (carrying mentionRank), FANS_OUT_TO — fan-out queries are themselves Terms, the join back to rank data — and VISIBILITY_FOR rollups (mentionRate, citationRate).
weo-engagement.ttl — draft v0. SearchIntent individuals (from Broder 2002, extended), ConversionPoint (with subclasses CallToAction, LeadCaptureForm, GatedAsset), ConversionEvent, crmRecordRef as the CRM join key, attributedResponse as a labeled, pre-click judgment.
schema.cypher — Neo4j 5.x constraints and indexes for all three modules.
Maturity, stated plainly: the core module is the stable substrate. Visibility is field-tested against a working tracker/response-capture/Neo4j implementation. Engagement is an early draft published for discussion — the pre-click funnel seam that engine-side data has been missing.
The mental model
Read across the three modules and the entity/episode split repeats:
- core — entities: Website, URL, Term, Topic. Episodes: Crawl, SerpSnapshot.
- visibility — entities: Engine, Brand, Prompt. Episodes: LLMResponse.
- engagement — entities: ConversionPoint, SearchIntent. Episodes: ConversionEvent.
Observations attach facts to entities or episodes (FETCHED, CITES, MENTIONS); derivations carry method, model, and confidence (IN_TOPIC, embeddingRef); judgments are labeled claims (targetsIntent, attributedResponse). High-cardinality facts live in column stores; the graph keeps windowed rollups with datasetUri pointing back at the authoritative table.
Two rollup patterns rhyme on purpose. (:URL)-[:RANKS_FOR {clicks, impressions, avgPosition, periodStart, periodEnd, datasetUri}]->(:Term) summarizes the SEO world. (:Prompt)-[:VISIBILITY_FOR {mentionRate, citationRate, responses, periodStart, periodEnd, datasetUri}]->(:Brand) summarizes the xEO world. Same discipline, new engine — and tenancy is data, not schema: scope lives in a websiteId property, never a dynamic label.
Example queries
Two queries from the working implementation, unmodified.
"Cited but not named" — pages that ground an engine's answer while the engine credits someone else:
MATCH (r:LLMResponse)-[:CITES]->(u:URL {websiteId: $tenant})
WHERE NOT EXISTS { MATCH (r)-[:MENTIONS]->(:Brand {websiteId: $tenant}) }
RETURN u.address, count(r) AS ghost_citations ORDER BY ghost_citations DESC;
The fan-out join — engine retrieval queries you already rank for, joined through the Term that a Prompt fanned out to:
MATCH (p:Prompt)-[:FANS_OUT_TO]->(t:Term)<-[rf:RANKS_FOR]-(u:URL)
WHERE rf.avgPosition <= 10
RETURN p.text, t.name, u.address, rf.avgPosition;What's deliberately not here
The ontology draws its boundary as carefully as it draws its terms.
Judgment machinery. Recommendations, playbooks, experiments, and outcomes are not modeled here. That layer references weo's terms but lives above them.
Vendor vocabularies. CRM and analytics stacks are never imported as schemas. They join via identity keys — crmRecordRef, datasetUri — and nothing more.
Unfalsifiable constructs. Quality scores and similar constructs don't get a term. If it isn't an observation, a provenance-carrying derivation, or a labeled judgment, it stays out.