Skip to content

Ontologies

An ontology is one YAML file that does three jobs for a knowledge base:

  1. It declares the topics — the validated first segment of every fact path.
  2. It describes them to the agent. Topic descriptions are shipped as part of the MCP server instructions on every session. This is prompt text, not documentation.
  3. It carries validation rules — small JavaScript expressions evaluated against every fact on write. A fact that fails one is rejected, and the agent is shown your error message.

The third job is what separates an ontology from a folder layout. A taxonomy tells an agent where to file a claim; a validation rule decides whether the claim may enter the corpus at all. The ontology is therefore where you encode what counts as a well-formed fact here — and it is enforced on write, not offered as advice.

Most knowledge bases never need a custom one. knomit ships two presets, and code in particular is a working answer for any codebase. Read on when you want topics the presets don’t have, or a standard of evidence they don’t enforce.

Inside the knowledge base repo, at .knomit/ontology.yaml — a private path, so it is configuration rather than a fact, and knomit reads it by name. Facts themselves land under the ontology root, kb/ by default and set by ontology_root in knomit.toml:

kb/<topic>/<category…>/<uuid8>.md

The ontology is parsed once, when the repo is opened — at server start, or when the repo is created or adopted. It is not re-read per request.

id: acme-support # required — stable identity; also controls preset upgrades
name: Acme Support Knowledge # required — human label
description: >- # optional
What the support team knows about customers, policy, and incidents.
validations: # optional — ROOT rules, run for every fact
- name: title-states-a-claim
message: "titles must state a claim, not ask a question"
rule: "!fact.title.trim().endsWith('?')"
topics: # required — at least one
policy:
description: Rules agents must follow — refunds, escalation, SLAs
validations: # optional — run for facts under this topic
- name: policy-must-anchor
message: "a policy fact must cite the document or code that enforces it"
rule: "fact.refs.length > 0"
children: # optional, and nestable
refunds:
description: Eligibility, limits, approvals
escalation:
description: When to escalate, and to whom
FieldWhereRequiredNotes
idrootStable slug. Matching an embedded preset’s id opts you into auto-upgrade.
namerootHuman label.
descriptionrootNot shown to the agent.
topicsrootMust be non-empty.
validationsroot, and any nodeA list of {name, message, rule}.
descriptionnodeTop-level topic descriptions are sent to the agent; child descriptions are not.
childrennodeRecursive, with no depth limit.

Key naming. Topic keys and their direct children must match ^[a-z0-9]+(-[a-z0-9]+)*$ — lowercase kebab-case. A violation is a parse error. Deeper keys are not regex-checked, but every path lookup lowercases each segment, so an uppercase grandchild key parses cleanly and is then unreachable. Keep the whole tree kebab-case.

ValidatePath walks a fact’s path against the tree:

  • The first segment must be a declared topic. An unknown topic is rejected.
  • Later segments are matched against declared children. At the first segment that does not match, the walk stops, and everything after it is accepted as freeform.
  • Lookups are case-insensitive, since keys are canonically lowercase.
  • category is required separately by knomit_learn, so the shallowest legal placement is topic/category.

Against the example above:

PathResult
policy/refunds✅ declared child
policy/refunds/eu/vateu/vat is a freeform tail
policy/anything/at/all✅ freeform from anything onward
Policy/Refunds✅ lookups are case-insensitive
marketing/campaignsunknown topic "marketing"
policy❌ rejected by knomit_learn — category required

The consequence is worth stating plainly: declaring children does not close the set. Children shape the agent’s mental model and give you somewhere to hang rules, but they never restrict what may be created below a topic. Only the top-level topic list is closed. If you want a genuinely constrained category, enforce it with a rule on fact.path — see the cookbook below.

Each rule is a JavaScript expression that passes by returning a truthy value. Rules are compiled once when the ontology is parsed, then evaluated in a sandboxed interpreter — a fresh one per fact.

Order. Root rules run first, then the topic’s rules, then each matched child’s rules, descending. The first failure wins, so the agent sees exactly one error:

validation "policy-must-anchor" at policy: a policy fact must cite the document or code that enforces it

Root rules report at the pseudo-topic <root>.

One global, fact:

FieldType
fact.kindstring — epistemic | pragmatic
fact.typestring — leaf type (observation, policy, hypothesis, …)
fact.domainstring[]
fact.entitiesstring[]
fact.refsstring[]
fact.titlestring
fact.bodystring
fact.pathstring — the full path, e.g. kb/policy/refunds/a1b2c3d4.md
fact.confidencenumber, 0–1
fact.sourcesnumber — count of independent corroborations
fact.originstring — authored | distilled | discovered
fact.evidence_weightnumber — provenance mass carried by a derived fact

The last three are the provenance axes, and they are what let an ontology enforce a standard of evidence rather than a shape. sources counts independent corroborations and defaults to 1. origin records which pipeline minted the fact: authored for anything a human or agent asserted, distilled for synthesis-pipeline output, discovered for the emergent facts the discovery engine surfaces. evidence_weight is the mass a derived fact inherits from the facts it was built on.

Rules run at three moments, and two of them hand a rule values no single caller supplied. Worth knowing before you write a provenance rule:

FieldOn a fresh knomit_learn writeOn the re-run after a subsumption merge
sourcesas supplied, default 1summed — new + existing
confidenceas supplied, default 0.7max of the two
domain, entitiesas suppliedunion of both
origin, type, kindas supplied, resolvedinherited from whichever fact wins the merge
evidence_weightalways 0always 0

Two consequences:

  • fact.sources >= 2 gates a claim’s first write, then passes on every one after it, because the merge sums. That is usually the semantics you want from a corroboration rule, but it is not a per-write floor.
  • evidence_weight is only meaningful on knomit_update. On knomit_learn it is not a caller-supplied field at all — it is computed and stamped after validation has already run, so a rule always sees 0 and fact.evidence_weight > 0 rejects every learn write. knomit_update validates a fact parsed from disk, so there the stored weight is real.
  • A fresh interpreter per fact, so no state leaks between evaluations.
  • Plain JavaScript, not Node: no require, no process. The standard library (Array, String, JSON, RegExp, …) is available.
  • fact is bound non-enumerably, so Object.keys(globalThis) is empty and a rule cannot discover what else exists.
  • A 100 ms interrupt per rule.
  • No I/O, no network, no access to other facts. A rule is a pure function of one fact — cross-fact checks like “is this a duplicate of…” are out of scope by design, and belong to subsumption.
  • knomit_learn, on every fact written — and again on the merged fact after a subsumption merge.
  • knomit_update, on the reassembled fact.
  • Skipped entirely for private state — facts written to an explicit .knomit/<area>/… path — including root rules. That exemption is deliberate: a background job’s bookkeeping has no ontology placement, and without it any root rule would let a job create its state slot and then refuse every update to it.
# Require a citation
rule: "fact.refs.length > 0"
# Conditional — "if type is X then Y must hold". The idiom is !isX || Y.
rule: "fact.type !== 'hypothesis' || fact.confidence <= 0.5"
# Constrain kind and type for a topic
rule: "fact.kind === 'pragmatic' && fact.type === 'policy'"
# Require a tag from a fixed set
rule: "['billing','auth','api'].some(function(d){ return fact.domain.indexOf(d) >= 0 })"
# Mutually exclusive tags
rule: "!(fact.domain.includes('global') && fact.domain.length > 1)"
# Enforce category depth — children cannot do this, because of the freeform tail
rule: "fact.path.split('/').length >= 5"
# A body naming a dollar threshold must also name the account-age condition,
# so the fact cannot compress into a false slogan
rule: "!/\\$\\d/.test(fact.body) || /\\bdays?\\b/.test(fact.body)"
# Confidence floor for a topic
rule: "fact.confidence >= 0.8"
# Require independent corroboration before a claim may land here
rule: "fact.sources >= 2"
# Keep a topic for human and agent assertions only — no pipeline output
rule: "fact.origin === 'authored'"
# Tie confidence to provenance: an emergent fact stays provisional until
# someone corroborates it
rule: "fact.origin !== 'discovered' || fact.confidence <= 0.5 || fact.sources >= 2"

Note the escaping in the regex example: \\$ and \\b, because the rule is a double-quoted YAML scalar before it is JavaScript.

This is the ontology behind a research pack that reads arXiv papers. It is worth reading in full, because it shows the thing that distinguishes an ontology from a folder list: the topics encode an epistemic stance, and the rules encode a standard of evidence.

id: research-literature
name: Research Literature Knowledge
description: >-
Claims extracted from research papers, together with the experimental setup
that bounds them. Written for a reader at a build decision, not for a
literature review.
validations:
- name: refs-non-empty
message: "every fact must cite at least one source"
rule: "fact.refs.length > 0"
- name: hypothesis-stays-provisional
message: "a hypothesis is a single-source claim: confidence must be <= 0.5 until independent replication graduates it to an observation"
rule: "fact.type !== 'hypothesis' || fact.confidence <= 0.5"
topics:
findings:
description: What a paper established, and the setup that bounds it
children:
empirical:
description: Measured results, stated with models, scale and benchmark
negative:
description: What did not work, and the cause the authors give
replicated:
description: Confirmed by independent work — graduated from hypothesis
scaling:
description: How a result moves with model size, data or compute
methods:
description: Techniques and architectures, and the rationale behind their design
children:
architectures:
description: Structural designs and what they buy
training:
description: Objectives, curricula, data construction
inference:
description: Decoding, sampling, test-time compute
evaluation:
description: How a method measures itself
contested:
description: Where credible papers disagree, and the conditions that decide it
children:
unresolved:
description: Live disagreement, no condition yet separates the results
conditional:
description: Both hold — the conditions separating them are known
limits:
description: Boundary conditions, scaling walls, impossibility results
children:
empirical:
description: Observed ceilings and where a technique stops paying
theoretical:
description: Proved bounds and impossibility results
benchmarks:
description: What a benchmark measures — and how it misleads
children:
construction:
description: How it was built, and what that bakes in
saturation:
description: Headroom, contamination, ceiling effects
open-problems:
description: Gaps the literature itself names as unsolved

Four things in it are worth copying, whatever your subject.

The root rules encode a research standard, not a schema. refs-non-empty means an uncited claim cannot enter the corpus at all. hypothesis-stays-provisional is the more interesting one: it ties a fact’s type to a confidence ceiling, so a single-source claim structurally cannot masquerade as settled. Confidence 0.8 on a hypothesis is rejected; the same 0.8 on an observation passes. The ontology enforces an epistemic norm that a human reviewer would otherwise have to police by hand.

findings/replicated is a promotion target. The taxonomy names the state change — hypothesis becomes replicated observation — so graduating a claim is a move within the tree rather than an ad-hoc edit. Topics can model a lifecycle.

contested splits on the reason for the disagreement. unresolved (no condition separates the results) versus conditional (both hold, and the separating condition is known) changes what a reader does next. That is the test for whether a child earns its place: does landing here rather than there change the reader’s next action?

open-problems declares no children at all. The shape of that space is not known in advance, so it is left freeform. Not every topic needs a fixed substructure.

Two ontologies ship embedded. Both are good starting points to copy and edit.

A broad taxonomy aligned with Wikipedia’s main topic classifications: people, technology, science, society, culture, geography, history, health, philosophy, religion, business, reference, plus meta. Each carries three to five children — technology has software, hardware, networking, data; science has formal, natural, applied. There are no validation rules at all; it is deliberately permissive.

Use it for personal knowledge bases, chat-derived knowledge, or anything whose subject matter isn’t known in advance. It is also what mode: "preset" picks when you omit ontology_preset — it is not a fallback for a repo whose own ontology failed to load, which opens read-only instead.

Knowledge categories for an agent working in a codebase. The topics are epistemic roles, not subject areas — which is the real contrast with default:

TopicPurposeChildren
invariantsLoad-bearing rules that must never be violatedarchitecture, concurrency, data, protocol
architectureWhat lives where, and how it connectsmodules, flows, integrations
conventionsHouse style — idioms, patternstesting, logging, errors, naming, git
decisionsDesign choices with rationaleaccepted, superseded, rejected
gotchasSharp edges and surprisestools, libraries, runtime
incidentsPast bugs and their fixesbugs, near-misses
metaKnowledge about the knowledgereasoning, ontology
principlesDesigner-authored intentmission, philosophy, anti-patterns, ux

principles is the only topic carrying rules, and they are a good template for your own: entities must include designer, kind and type must be pragmatic and policy, and domain must be non-empty and must not mix global with an area path. Together they make the topic writable only through the /knomit-principle flow.

default organises by subject. code and the research example above organise by epistemic role. For an agent-facing knowledge base, prefer the second: “show me the invariants for this area” is a question an agent can act on, in a way “show me the technology facts” is not. Subject is better carried by domain tags and entities, which cut across the tree instead of partitioning it.

Presets evolve. On every repo open, knomit checks whether the stored ontology’s id matches an embedded preset — general maps to default, source-code to code. If it does and the stored ontology is a strict subset of that preset, the stored file is overwritten in place with the newer preset, so repos created against an older version pick up added topics automatically. Subset comparison matches validations by name only, which is how rule wording fixes ship this way too.

If the stored ontology has added anything the preset lacks, it has diverged: knomit logs that an upgrade is available and leaves your file alone.

The ontology is chosen when the knowledge base is created, via POST /api/v1/repos:

Terminal window
curl -X POST http://localhost:19278/api/v1/repos \
-H 'Content-Type: application/json' \
-d '{
"name": "research",
"mode": "custom",
"ontology_yaml": "id: research-literature\nname: Research Literature Knowledge\ntopics:\n findings:\n description: What a paper established\n"
}'
modeRemote?Ontology source
presetlocal onlyontology_preset: "default" or "code". Omitted, it uses default.
customlocal onlyontology_yaml: the full YAML, inline.
cloneremoteTaken from the chosen branch of the origin. Passing ontology_preset or ontology_yaml is rejected, not ignored — a clone would otherwise drop it silently. 409 if that branch has no ontology.
initializeremoteontology_preset or ontology_yamlrequired, since writing it is the act that makes the branch a knowledge base — committed onto knomit’s own branch. 409 if that branch already has one.

The two remote modes are the two halves of one question about the branch you chose — does it already carry .knomit/ontology.yaml? — answered by POST /api/v1/repos:probe-initialized. clone joins a branch that is already a knowledge base and lets its ontology govern; initialize turns a branch that isn’t one into one.

The response is newline-delimited JSON progress ending in {"type":"done"} or {"type":"error"}. Preflight establishes shape for both remote modes before the stream opens, so a mismatched mode arrives as a problem+json 409 rather than as an error line inside a committed 200. The web UI wraps all four modes in a guided wizard that derives the mode from what it probed, rather than asking you to classify the remote yourself.

Validating a draft before you commit to it

Section titled “Validating a draft before you commit to it”

Three endpoints exist so a draft can be checked without creating anything. The wizard’s editor is built on them, and they are equally usable from curl:

EndpointWhat it gives you
POST /api/v1/ontologies:validateParses a raw YAML body and returns every diagnostic with its line position, not just the first. Always 200 — a document that fails validation answers ok: false; a 4xx is reserved for a body the server could not read at all.
GET /api/v1/ontologies/presetsThe built-in presets: each one’s preset name, ontology id, title, description and topic list.
GET /api/v1/ontologies/presets/{name}A preset’s YAML in exactly the form create would commit — the right starting point for a custom ontology.
GET /api/v1/ontologies/schemaThe field reference for the YAML above — every key with a one-line description. A reflection test over the parser’s own structs fails if a key it accepts is missing here, so the reference cannot quietly fall behind the parser.
Terminal window
curl -X POST http://localhost:19278/api/v1/ontologies:validate \
-H 'Content-Type: text/yaml' --data-binary @ontology.yaml

Use this rather than repo creation as your validation step: unlike a create, it costs nothing and reports all the problems at once.

MCP server instructions are generated per session from the write repo’s ontology. The top-level topic list, with each topic’s description, is interpolated straight into the section the model reads before it does anything:

- **topic**: A validated top-level folder. Available topics:
benchmarks — What a benchmark measures — and how it misleads
contested — Where credible papers disagree, and the conditions that decide it
findings — What a paper established, and the setup that bounds it
...

That has four practical consequences:

  • Descriptions are prompt engineering. “Sharp edges and surprises” routes facts better than “gotchas”. Write them as routing instructions addressed to the agent, not as glossary entries.
  • Only top-level descriptions are shipped. Child descriptions shape the file tree and the UI, and a human browsing will read them, but the model never sees them in its instructions. Front-load the meaning into the topic line.
  • Topic names affect federation: a lens skips read mounts whose ontology lacks the topic being browsed, so a shared topic vocabulary across repos is what makes cross-repo browsing coherent.
  • Topics feed the OKF export, where the OKF type is derived from the topic, singularised.
  1. A missing or unparseable ontology makes the repo read-only. It is not replaced by the default; refused writes and an error log are the signal.
  2. Deleting topics from a preset while keeping its id is silently undone on the next repo open. Change the id.
  3. children are suggestions. The freeform tail means they never restrict what gets created; use a fact.path rule for a real constraint.
  4. fact.sources is summed on a subsumption merge, so a corroboration floor gates a claim’s first write and passes on every one after it — and fact.evidence_weight is always 0 on knomit_learn, because it is stamped after validation.
  5. Only the first failing rule is reported per write.
  6. Comments and key order are not preserved — the stored file is re-serialised with sorted keys.
  7. Grandchild keys skip the kebab-case check, and are then unreachable if they aren’t lowercase.
  8. Rules are compiled at parse time, so a syntax error takes down the whole ontology, not just that rule — and then gotcha 1 applies.
  9. Private-state facts bypass ontology validation entirely, root rules included.
  10. Regex rules need doubled backslashes inside a double-quoted YAML scalar.