OTO
tutorials

From a document to a cited answer

This is the whole loop, once, by hand. A procedure document goes into a project, its facts are proposed and gated, the graph is built, and a question comes back with the sentence that answers it. Forty minutes. When you have done it once, the build-knowledge-base skill does the same loop with an agent, and you will know what it is doing at each gate.

Ingestinbox/ → processing/; the text extracted
→
Surveyheadings, dates, terms; what already resolves
→
Proposeone file per document, every fact with its sentence
→
The gatesdry run · merge · check · apply by name
→
Buildrefused if the graph says what the vocabulary does not
→
Askthe answer, with its status, date, source and sentence
The loop this tutorial runs once by hand, and the build-knowledge-base skill runs with an agent.

You need uv and the engine, as in getting started. The page writes oto; prefix with the uvx line if you did not install it. The project is a fictional motor insurer.

1. A project from an ontology

oto init --name "Meridian Claims" --ontology auto-claims --project claims
oto build --project claims
oto status --project claims

The ontology brings a vocabulary of seventeen classes, its recorded reasoning, one rule and a small sample graph, so the project builds before you add anything.

Meridian Claims (slug meridian-claims)
  sources     inbox 0, processing 0, errors 0, corpus 0 document(s), notes 0
  vocabulary  17 class(es), 26 relation(s), 7 typed attribute(s), 1 rule(s), not yet accepted
  ontology    auto-claims @1 (built-in)
  graph       14 node(s), 20 edge(s), 0 assertion(s)
  build       up to date
status

always ends with the next step. Keep running it whenever you are unsure where you are.

2. A document into the inbox

Save this as claims/inbox/total-loss-procedure.md. It is short on purpose; the loop is the same for a forty-page handbook.

# Total loss settlement procedure

Meridian Auto Insurance, Claims Operations. Effective 2026-02-01.

## 1. When a claim becomes a total loss

A vehicle is declared a total loss when the repair estimate exceeds 70 percent
of its actual cash value. The claims adjuster makes the declaration after the
repair estimate is on file.

## 2. The salvage coordinator

The salvage coordinator takes over a total loss claim once it is declared. The
salvage coordinator obtains the settlement valuation, arranges the salvage sale,
and closes the recovery.

## 3. Settlement

The settlement valuation is the actual cash value on the day of the incident,
less the collision deductible. The collision deductible is 500 dollars on every
policy issued after 2025-01-01.

## 4. Recovery

Salvage proceeds are recorded as a recovery against the claim within thirty days
of the sale.
oto ingest --project claims
run 20260925-201514: claimed 1 file(s) into processing/
  total-loss-procedure.md -> documents/total-loss-procedure.md (text)

run 20260925-201514: extracted 1, blocked 0, unsupported 0, missing parser 0, failed 0
1 file(s) wait in processing/ until the graph holds their facts.

The file moved from inbox/ to processing/, and its text is now at build/documents/total-loss-procedure.md. It will reach archive/ only when the graph holds its facts. That is the spool: a raw file is in exactly one place, and an empty processing/ means nothing is owed.

3. Survey before naming anything

oto survey --project claims
oto survey --project claims --doc total-loss-procedure
total-loss-procedure: Total loss settlement procedure  (125 words, 4 heading(s); dates 2025-01-01, 2026-02-01)
    - 1. When a claim becomes a total loss
    - 2. The salvage coordinator
    - 3. Settlement
    - 4. Recovery

Terms in this document, and what the live graph already holds for them:
    1  Claims Operations          new: derive an id from the label if it is an entity
    1  Meridian Auto Insurance    new: derive an id from the label if it is an entity
    ...
Reuse a matched id. A hit is a hint, not a verdict: read the sentence before deciding.

The survey is a map, not a verdict. It lists headings, dates and recurring terms, and for each term whether the graph already holds an entity for it. Read the document before deciding what is an entity. Here, two things are: a procedure and a role.

4. One proposal per document

A proposal is a file of nodes and edges, each citing the document. This is the step an agent does for you on a real corpus: the build-knowledge-base skill has Claude read each document and write this file, one per document, to the rules below. Here you write it by hand once, so you know what the agent produces. Save this as claims/proposals/total-loss-procedure.json.

{"source_doc": "total-loss-procedure",
 "as_of": "2026-02-01",
 "nodes": [
  {"id": "procedure.total-loss", "type": "Procedure", "label": "Total loss settlement",
   "aliases": ["total loss"],
   "summary": "A vehicle is declared a total loss when the repair estimate exceeds 70 percent of its actual cash value; the claims adjuster makes the declaration after the repair estimate is on file.",
   "valid_from": "2026-02-01",
   "evidence": [{"doc": "total-loss-procedure", "where": "section 1",
                 "quote": "A vehicle is declared a total loss when the repair estimate exceeds 70 percent of its actual cash value."}]},
  {"id": "role.salvage-coordinator", "type": "Role", "label": "Salvage coordinator",
   "summary": "Takes over a total loss claim once it is declared: obtains the settlement valuation, arranges the salvage sale, and closes the recovery.",
   "evidence": [{"doc": "total-loss-procedure", "where": "section 2",
                 "quote": "The salvage coordinator takes over a total loss claim once it is declared."}]}
 ],
 "edges": [
  {"from": "role.salvage-coordinator", "rel": "performs", "to": "procedure.total-loss"},
  {"from": "role.adjuster", "rel": "performs", "to": "procedure.total-loss"}
 ]}

Three rules make a proposal worth merging. The id is lowercase, dotted, derived from the label, and stable. The summary is in the document's words. Every node carries evidence: where in the document, and the sentence. The second edge reuses role.adjuster, which the sample graph already holds; a second document that mentions an existing entity uses the same id.

What the merge refuses. Had the proposal restated coverage.collision with a new summary, the merge would have stopped: "coverage.collision: summary differ from the candidate. A changed fact is a supersession: give the new fact a new id and retire the old one." Nothing is overwritten in this system. The next tutorial does a supersession properly.

5. The gates

Edits go into a candidate, never into the live graph.

oto curate start --project claims
oto curate add --project claims --from claims/proposals/total-loss-procedure.json --dry-run
total-loss-procedure.json: would add 2 node(s), 0 merged, 0 updated, would add 2 edge(s) (0 already present)
dry run: the candidate is unchanged. Drop --dry-run to merge.

The dry run says exactly what the merge would do and refuses what it would refuse, without writing. Merge, then check.

oto curate add --project claims --from claims/proposals/total-loss-procedure.json
oto curate check --project claims
candidate would change the graph:
  nodes: +2  -0  ~0
  edges: +2  -0
    node added    procedure.total-loss
    node added    role.salvage-coordinator
    edge added    role.adjuster -performs-> procedure.total-loss
    edge added    role.salvage-coordinator -performs-> procedure.total-loss

blocking (2):
  role.adjuster -performs-> procedure.total-loss relation 'performs' is not declared in the vocabulary
  role.salvage-coordinator -performs-> procedure.total-loss relation 'performs' is not declared in the vocabulary

2 blocking problem(s). Fix them, then check again.

The check refused. The document says a role performs a procedure, and the vocabulary has no such relation. This is the point of the ontology: the graph cannot say what the vocabulary does not declare.

6. The vocabulary is the contract

The honest fix is to declare the relation, with its domain, its range, its inverse and one line saying what it means. Add it under properties in claims/ontology.config.json:

"performs": ["Role", "Procedure", "performed_by", "The role that carries out this procedure."]
oto ontology check --project claims
oto curate check --project claims
vocabulary: version 1, 17 classes, 27 relations
domain and range conformance over 20 declared edge(s):
  domain violations: 0
  range violations : 0
    edge added    role.adjuster -performs-> procedure.total-loss
    edge added    role.salvage-coordinator -performs-> procedure.total-loss

ready to apply

A vocabulary change is itself checked: oto ontology check reports whether it breaks any existing edge, and oto ontology accept records it as the baseline so the next change can be diffed against it.

7. Apply, by name

oto curate apply --project claims --by "A. Reader, claims operations" \
    --note "Total loss settlement and the salvage coordinator, from the February procedure"
oto build --project claims
oto ingest complete --project claims
oto curate log --project claims
applied. Previous graph kept at graph.previous.json; `oto curate undo` restores it.
ledger: changelog.jsonl (0 retired). `oto curate log` reads it.
run 20260925-201514 complete: 1 file(s) archived
processing/ is empty: nothing is owed
2026-09-25T20:16:17  nodes +2 -0 ~0  edges +2 -0  by A. Reader, claims operations
    from: total-loss-procedure
    Total loss settlement and the salvage coordinator, from the February procedure

Apply needs a name, because an unattributed change cannot be followed up. The build compiles every layer. ingest complete archives the document only now, because the graph holds its facts. The ledger says who, when, from which document, and what became answerable.

8. Ask

oto query --project claims entity "Total loss settlement"
=== Total loss settlement  [Procedure]  (procedure.total-loss) ===
status=current  ·  as_of=2026-02-01  ·  valid_from=2026-02-01  ·  source_doc=total-loss-procedure
aka: total loss

A vehicle is declared a total loss when the repair estimate exceeds 70 percent of its
actual cash value; the claims adjuster makes the declaration after the repair estimate is on file.

Relationships (incoming):
  Claims adjuster → performs
  Salvage coordinator → performs

Sources: total-loss-procedure

Evidence:
  - total-loss-procedure section 1: "A vehicle is declared a total loss when the repair
    estimate exceeds 70 percent of its actual cash value."

The answer carries its status, its dates, its source, and the sentence. A reader can open the document and check. Three more questions:

oto query --project claims neighbors "Salvage coordinator"
oto query --project claims search "salvage" 3
oto query --project claims explain procedure.total-loss
Nothing about Total loss settlement (procedure.total-loss) is derived; every fact shown is asserted by a document.

9. See it, and give it to an agent

oto serve --project claims --http 8765

Open http://127.0.0.1:8765/ for the explorer. The same server answers agents over JSON-RPC with fourteen tools:

curl -s http://127.0.0.1:8765/rpc -H 'content-type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
kg_entity, kg_neighbors, kg_count, kg_group_by, kg_search, kg_by_type, kg_explain,
kg_policy, kg_overview, kg_stale, kg_resolve, kg_pending, kg_actions, kg_docs
oto serve

with no --http speaks the same tools over stdio, which is how an MCP client such as Claude Code connects. Install in Claude Code wires it.

  • Ingest claimed the document into a run and extracted it; survey mapped it.
  • A proposal stated its facts with evidence; the dry run and the check gated them.
  • The vocabulary refused a relation it did not declare, and you declared it.
  • Apply recorded who and why; build compiled; complete archived; query answered with the sentence.

Next: a correction made in conversation supersedes a fact without losing the old one. Ask the graph goes through every question the graph answers. To do this loop with an agent, install the plugin and ask for the build-knowledge-base skill: it stops at the same gates.