A specification nobody can read is just a document. These are the reference implementations: they turn AODM into data your program already knows how to handle, and they check every rule in the specification while doing it.
All of it is Apache 2.0 licensed and ships in the download package. The parsers and converters have no dependencies in any language — each uses only its standard library. The RAG readers depend only on the framework they plug into.
Reference parsers
A parser reads AODM’s XML form and hands back the JSON model defined by
aodm-core-1.3.schema.json. Both are AODM: the specification
defines two serialisations of one model, and the parsers convert between
them in either direction.
| Language | File | Requires |
|---|---|---|
| Python | parsers/python/aodm.py | Python 3.8+, standard library |
| JavaScript | parsers/javascript/aodm.js | Node 18+ or any browser |
| Java | parsers/java/Aodm.java | JDK 11+, standard library |
| C# | parsers/csharp/Aodm.cs | .NET 6+, base class library |
Python
from aodm import parse_file
doc = parse_file("knowledge.xml")
doc.facts # list of facts
doc.by_id("engine") # look up any element
doc.to_json() # JSON matching the published schema
doc.to_xml() # back to AODM XML
doc.is_valid() # False if any MUST-level rule is broken
for issue in doc.validate():
print(issue) # ERROR R1: The object "ghost" does not match…
JavaScript
const { parse } = require('./aodm');
const doc = parse(xmlString);
doc.facts; doc.byId('engine');
doc.toJSON(); doc.toXml();
doc.isValid(); doc.validate();
In a browser, load it with a <script> tag and use
AODMParser.parse(…). It uses the browser’s own XML parser
where available and falls back to a built-in reader under Node, so there is
no dependency either way.
Java
Aodm.Document doc = Aodm.parseFile("knowledge.xml");
doc.facts(); doc.byId("engine");
doc.toJson(); doc.toXml();
doc.isValid(); doc.validate();
C#
var doc = Aodm.ParseFile("knowledge.xml");
doc.Facts; doc.ById("engine");
doc.ToJson(); doc.ToXml();
doc.IsValid(); doc.Validate();
Each also runs from the command line:
python3 aodm.py knowledge.xml # print the JSON model
python3 aodm.py --validate knowledge.xml # report rule violations
What the parsers check
Validation is not an optional extra bolted on afterwards — every parser
enforces the full rule set from VALIDATION-RULES.md, including
the constraints that no schema language can express:
referential integrity, derivation cycles, duplicate ids, cardinality,
measurement coherence, temporal ordering, polarity handling and digest
format. Errors are MUST-level violations; warnings are SHOULD-level.
Why four implementations rather than one. Beyond covering more stacks, independent implementations are how a specification gets tested. If two of them disagreed about the same document, the fault would be in the specification, not the code. A conformance suite runs all four over the same inputs and requires identical output — currently 18 rule cases across four languages, alongside 21 schema checks, 24 XBRL constructs and 33 graph and reasoning checks.
Converters
Most organisations already hold structured data in a standard format. A converter turns that into AODM without re-authoring anything, which is usually the difference between adopting a format and reading about it.
XBRL
XBRL is mandatory for financial reporting to the SEC, ESMA, HMRC and most other regulators, so essentially every listed company already produces it. The converter implements the whole of the XBRL 2.1 instance document syntax.
python3 xbrl_to_aodm.py instance.xml # AODM JSON
python3 xbrl_to_aodm.py instance.xml --xml # AODM XML
python3 xbrl_to_aodm.py instance.xml --validate
python3 xbrl_to_aodm.py instance.xml --labels=filer_lab.xml --uri=https://www.sec.gov/...
Pass the filing’s label file with --labels and every fact is named the
way the filer names it, with its segment and period, for example
“Net sales [Operating segments; Greater China] (Apple Inc.; period)”, instead
of a raw element name. --uri records the filing’s address as each
fact’s source.
| XBRL | Becomes |
|---|---|
| Reporting entity identifier | entity type="reporting-entity" |
| Reported fact | fact, statement = element name |
| Numeric content + unit | value/@number + @unit |
| Divided unit (USD/share) | @unit="USD/shares" |
| Fraction | @number, numerator ÷ denominator |
| Duration period | valid-from / valid-to |
| Instant period | valid-from = valid-to |
| Forever period | no temporal bounds |
@decimals / @precision | value/@tolerance |
| Explicit & typed dimensions | relationship to a member entity |
| Tuples, nested | entity type="fact-group" + composition |
| Footnotes | appended to the fact statement |
A stated accuracy of decimals="-6" means the figure is rounded
to millions, so the real value sits within half a million either way. That
becomes an AODM tolerance rather than being discarded — which is the
whole point: the precision of a reported number is information, and most
pipelines throw it away.
Scope, stated precisely. This covers the XBRL 2.1
instance document syntax completely. It does not interpret the
taxonomy — linkbases, label resolution and calculation
trees live in separate files an instance only references. The one
exception is the label linkbase, which --labels reads so facts
carry the filer’s own names; without it, element names appear verbatim.
Inline XBRL (iXBRL embedded in HTML) is a different serialisation and is
not read.
Spreadsheets and CMS exports
Turns a product list, price sheet or CMS export (CSV, TSV or XLSX) into AODM. No libraries are needed, Excel included.
python3 converters/sheet_to_aodm.py products.xlsx --validate
- The first column names the thing each row describes. Every other filled cell becomes a fact about it.
- A unit in the header, such as
Weight (kg)orPrice (EUR), becomes the fact’s unit. - Columns headed URL or Link, Published and Modified become each fact’s source and date. Valid from and Valid to columns become its validity window.
- An Excel formula becomes a derived fact that points at the cells it was calculated from.
- An AI-written column marks rows a model wrote. Those rows need a value in a Confidence column, or the converter stops and names the cells.
Schema.org JSON-LD
Reads the Schema.org JSON-LD already in a page, or a .jsonld file, and
writes it as AODM: products, offers, articles, events, recipes and product variants.
python3 converters/jsonld_to_aodm.py page.html --validate
Prices keep their currency and validity dates, ratings become measured values, and the page’s publisher and dates become the source. Output is a draft to review, because JSON-LD often says less than the page does.
Product feeds
Converts a Google Merchant Center product feed (RSS or Atom XML, CSV or TSV): prices, sale prices with their dates, availability, sizes, weights, shipping costs, brands and product variants.
python3 converters/feed_to_aodm.py feed.xml --currency=USD --validate
python3 converters/feed_to_aodm.py feed.tsv --uri=https://shop.example/feed.tsv --modified=2026-10-01
--currency sets the currency for prices written without one,
--uri records the feed’s address as the source, and
--modified sets the date the feed was current. Unit prices are worked out
from price and size and marked as derived.
Page-level defaults
AODM 1.3 lets a page declare once what all its facts share, such as the source,
the date or the unit, in a data-aodm-defaults attribute. This tool moves
shared values out of each tag and into that attribute, or expands them back for
tools that read HTML without a browser.
python3 converters/aodm_defaults.py compact page.html > smaller.html
python3 converters/aodm_defaults.py expand page.html > expanded.html
The rules readers follow are in section 3.9 of the HTML profile. The online validator and the Python readers below expand defaults automatically.
Graph compiler
Compiles AODM into graph structures for reasoning engines and graph databases.
Engine ──requires──▶ Spark becomes node, edge, node.
python3 aodm_graph.py doc.xml --format cypher # Neo4j
python3 aodm_graph.py doc.xml --format turtle # RDF, for SPARQL/OWL
python3 aodm_graph.py doc.xml --format graphml # Gephi, yEd
python3 aodm_graph.py doc.xml --format dot # Graphviz
Facts, rules and sources become nodes rather than properties. That is not a stylistic choice: with sources as nodes, “what rests on this source?” is one traversal, and retraction becomes possible. Flattened into text properties it is a full scan with string matching.
Inference engine
Forward chaining over the compiled graph, implementing the inference section of the specification (section 10), so its conclusions are reproducible by any other conformant engine.
$ python3 reasoner.py doc.xml
Derived 2 conclusion(s):
step 1: failure-risk confidence 0.5985
by risk-rule from temp-rise, pressure-nominal
step 1: shorten-service confidence 0.53865
by service-rule from failure-risk
Confidence multiplies along the chain, so a conclusion is never more certain than the weakest premise supporting it. It also answers two questions a reasoner usually cannot:
- Why is this believed?
--explainwalks the chain back to the observed facts and their sources. - What falls if this source is wrong?
--retractfollows the derivations forward and lists every conclusion that depended on it. - What was true then?
--at 2025-06-01reasons as at a date; a rule outside its validity window does not fire.
LLM context
Renders a document into prompt-ready context for any model. Dropping raw facts into a prompt is easy and mostly useless: the model gets a pile of assertions with no way to tell a measurement from a guess, and states them all with equal confidence.
python3 context.py doc.xml # markdown
python3 context.py doc.xml --format text # compact, for tight budgets
python3 context.py doc.xml --format json # structured, for tool use
python3 context.py doc.xml --max-chars 4000 # budget-aware
The renderer states confidence in words as well as numbers, marks known-false facts explicitly rather than omitting them, excludes superseded knowledge while saying that it did, and labels inferred facts as inferred. Those are the signals that let a model hedge accurately instead of asserting everything flatly.
Use AODM in RAG pipelines and AI agents
Most retrieval pipelines turn HTML into plain text first, and that step throws the
data-aodm attributes away. These readers parse the page before any text
conversion, split it into search passages that never cut a tagged element in half,
and keep each passage’s facts, sources and dates attached.
They ship in the package as source. Install them with pip from the unpacked package’s top folder:
pip install ./integrations/aodm-reader # the reader on its own
pip install ./integrations/aodm-reader ./integrations/langchain-aodm # LangChain
pip install ./integrations/aodm-reader ./integrations/llama-index-readers-aodm # LlamaIndex
pip install ./integrations/aodm-reader ./integrations/aodm-mcp # MCP server
The reader
from aodm_reader import load_passages, format_passage
docs = load_passages(["site/"]) # passages with their AODM records
print(format_passage(docs[0])) # the passage plus 1 line per fact
By default it leaves out text a page marks as AI-written, and it can hide facts that are outside their validity window on a given date or have been superseded.
LangChain
from langchain_aodm import AODMLoader, AODMRetriever, format_document
docs = AODMLoader(["site/"]).load()
retriever = AODMRetriever(base_retriever=store.as_retriever(), as_of="2026-10-06", k=5)
LlamaIndex
from llama_index.readers.aodm import AODMReader
documents = AODMReader().load_data(["site/"])
MCP server
Lets an AI model such as Claude search your tagged pages directly, with filters for date, AI-written text and source. It is read-only.
claude mcp add aodm -- aodm-mcp /path/to/your/site
For Claude Desktop and other clients, the configuration is in
integrations/aodm-mcp/README.md in the package.