Ontologies
Read Knowledge graph & ontology first for the background. This page covers the practical side.
Seed at construction time
Section titled “Seed at construction time”createWiki(db, { llmProvider, config: { ontology: { mode: 'strict', // global default for entities with no override seedManifests: { 'team-alpha': { mode: 'emergent', // per-entity override manifest: { node_types: [ { type: 'person', description: 'An individual or user.' }, { type: 'project', description: 'An ongoing initiative.' }, ], edge_types: [ { type: 'contributes_to', source_type: 'person', target_type: 'project', description: 'Works on a project.' }, ], }, }, }, }, },});A seed is written to the database the first time the entity’s ontology is resolved inside a
transaction (on ingest or heal). Until then, getOntologyManifest() still reports it.
Read and set at runtime
Section titled “Read and set at runtime”await wiki.getOntologyManifest('team-alpha'); // { mode, manifest } or null
await wiki.setOntologyManifest('team-alpha', manifest, { mode: 'strict' }); // upsert
// Many entities in one transaction. With ifAbsent, an existing row is left alone.const { written, skipped } = await wiki.setOntologyManifests( [ { entityId: 'tier_fact', manifest, mode: 'strict' }, { entityId: 'tier_wisdom', manifest, mode: 'strict' }, ], { ifAbsent: true },);validateManifest(manifest) is exported if you want to check a manifest before saving it.
What the model returns
Section titled “What the model returns”In strict and emergent modes the extraction JSON may include a type and inline edges:
{ "facts": [{ "title": "Jane reports to Bob", "body": "Jane reports to Bob Smith.", "tags": [], "confidence": "certain", "okf_type": "person", "edges": [{ "edge_type": "reports_to", "target_title": "Bob Smith" }] }]}okf_typeis matched case-insensitively and stored with the manifest’s casing.- Edges are resolved by
target_titlewithin the same transaction. - An invalid
okf_typestores the fact untyped and drops its edges. An invalid individual edge is dropped (and reported as anedge_droppeddiagnostic). - In emergent mode the model may add
ontology_updates.node_typesandedge_types. These are append-only and deduplicated by type. A malformedparent_typein a proposal is dropped, not thrown.
Inheritance
Section titled “Inheritance”node_types: [ { type: 'creativework', description: 'Books, articles, specs…' }, { type: 'design_spec', parent_type: 'creativework', description: 'A technical design.' },]Edges declared on creativework now also match design_spec. When several edges could match a
target, the exact type wins over the parent. Validation rejects any of the following:
| Problem | Error message |
|---|---|
| Unknown parent | Parent type not found: <slug> |
| Self-parent | Self-parent: <type> |
| Two or more levels | Parent chain too deep: a → b → c |
Blank or non-string parent_type |
Ontology parent_type must be a non-empty string when present |
How it behaves in detail:
- One level, checked everywhere. The manifest is validated whenever it’s saved and whenever
it’s read.
setOntologyManifestrejects a chain up front. The depth check also catches 2-cycles (a → b → a). - Absent is fine, blank isn’t. Leaving out
parent_typemeans “no parent”. A key that is present must be a non-empty string. - Parents are real types. A fact can be classified as the bare parent (
creativework). There’s noabstractflag. To steer the model toward the children, say so in the parent’s description. - Matching works on both ends, one hop. A child satisfies an edge declared on its parent as either the source or the target. Matching never walks further than one level.
- The trade-off. An edge declared to
creativeworkadmits every child ofcreativework, and there’s no way to say “the parent only”. A type that needs exact-only targets must not have children. - Emergent mode. The model may propose
parent_typeinontology_updates. Those proposals are treated as untrusted: an invalidparent_typeis dropped rather than failing the ingest. To change an existing type’s parent, callsetOntologyManifest. - No migration needed.
parent_typeis optional, and manifests without it validate exactly as before.
The full design is in the ontology parent field spec.
In React
Section titled “In React”import { useOntologyManifest, useSetOntologyManifest } from '@equationalapplications/react-llm-wiki';
function OntologySettings({ entityId }: { entityId: string }) { const { manifest, mode, refetch } = useOntologyManifest(entityId); const { execute, isPending } = useSetOntologyManifest();
const save = async (next) => { await execute(entityId, next, { mode: 'strict' }); refetch(); // the read hook doesn't refresh on its own }; // …}Backfill untyped facts
Section titled “Backfill untyped facts”Facts that bypassed the librarian (imports, or data from before ontologies) have no type and no
edges. runOntologyBackfill() types them in place:
let r;let previousRemaining = Infinity;do { r = await wiki.runOntologyBackfill(entityId); // { scanned, typed, failedValidation, edgesAdded, remaining, deferred, skipped } if (r.remaining >= previousRemaining) break; // no progress: stop, retry on the next trigger previousRemaining = r.remaining;} while (r.remaining > 0);- Stop when a pass doesn’t lower
remaining. A fact whose provider call keeps throwing (acall_error, or a thrownclassify) gets no cooldown stamp and stays eligible, so a barewhile (remaining > 0)loop can spin on it forever. The guard above stops on no progress; the next trigger retries. - You own the trigger. Running it after each sync or import is a good default. It’s safe to
swallow a
WikiBusyError, since the next run retries. - It only adds. It never creates, deletes, or rewrites facts, never overwrites an existing
type, and never changes
updated_at. - Cost: a single
SELECTwhen there’s nothing to do. Otherwise it scans up to 25 facts per run (batchSize), in sub-batches sized frommaxOutputTokens. Sub-batches that fail are halved and retried. - Facts it can’t classify are retried after 7 days (reported as
deferred). - Customize the prompt with
options.promptOverrideorconfig.prompts.ontologyBackfillSystemPrompt.
Classifier mode
Section titled “Classifier mode”A non-generative classifier can do the typing at lower cost. Add classify to the provider and opt in:
createWiki(db, { llmProvider: { generateText, classify }, config: { ontology: { backfillClassifier: 'auto', classifyMinConfidence: 0.5 } },});
await wiki.runOntologyBackfill('user-1'); // uses classifyawait wiki.runOntologyBackfill('user-1', { classifier: 'llm' }); // force the generative path- Each untyped fact gets one
choicequestion over the manifest’s node types. - Answers below
classifyMinConfidencestay untyped. - Classifier mode proposes no edges. Use
classifier: 'llm'when you want edges. - More than 255 node types, or no
classifyfunction, means the generative path is used.
Curated Thoughts uses this with a
Jev classifier on Cloudflare Workers AI (typesafe/jev), behind its Type untyped facts button.
classify receives { state, questions }, where each question is choice, binary, or
score. It returns { answers }, which the engine validates as untrusted input: off-list choices
and out-of-range probabilities count toward failedValidation, and a classify call that throws
counts toward skipped and is retried on the next pass. Answers below classifyMinConfidence are
retried after the cooldown.