Skip to content

Quickstart

One canon, one question, one computed answer — from a Node application, with nothing but npm. About ten minutes.

The German Civil Code fixes how a period is counted: when an event starts the period, the day of the event is not counted (section 187), and a period measured in days ends with the last of those days (section 188). The question: an event on 6 March 2026 starts a period of 14 calendar days — does the period end on 20 March?

The canon de.bgb.fristen formalizes those provisions. Your application supplies the facts and the question; Arxo Law executes the canon and returns the answer together with its grounds.

You need Node.js 20 or newer and npm. You get a project whose one file asks the question and prints the answer document: the statuses, the rule that fired, and the hashes that reproduce the answer.

Terminal window
npm create arxo@latest my-deadline
cd my-deadline
npm install
npm start

src/ask.ts is the whole application. To do the same inside an existing project, install the facade and the canon next to it — no network is needed after that:

Terminal window
npm i @arxo/law @arxo/canon-bgb-fristen
import { open } from '@arxo/law';
const fristen = await open('de.bgb.fristen@0.1.0');
const r = await fristen.truth('frist_ende', ['frist', '2026-03-20'], {
legalTime: '2026-09-17',
timezone: 'Europe/Berlin',
deadlinePolicy: 'urn:de:corpus:clir:bgb-fristen#BGB_FRISTEN_TAG',
answers: [
{ predicate: 'frist_ereignis', args: ['frist', '2026-03-06'] },
{ predicate: 'frist_dauer_tage', args: ['frist', '14 calendar_day'] },
],
});
console.log(r.evaluationStatus, r.truthStatus);

The names are the canon’s own, in German, and each one has a plain reading:

Name Reads as Role in the call
frist_ereignis the event that starts the period a fact: period frist starts with an event on 2026-03-06
frist_dauer_tage the length of the period in days a fact: 14 calendar_day
frist_ende does the period end on this date the question: frist, 2026-03-20

open('de.bgb.fristen@0.1.0') names the canon and its exact version: an answer without a pinned version is not reproducible. legalTime is the date the law is projected on, timezone the calendar the dates live in, and deadlinePolicy names how the days are counted — that policy is declared by the canon itself, not by your code.

What went in and what came back (5–8 min)

Section titled “What went in and what came back (5–8 min)”

The call carries four things: the canon, the facts (answers), the legal time, and the question (truth — is frist_ende(frist, 2026-03-20) established). The answer is a document. Printed field by field on this canon, it reads:

{
"evaluationStatus": "COMPUTED",
"truthStatus": "TRUE_ONLY",
"sources": [],
"rulesApplied": ["urn:de:corpus:clir:bgb-fristen#TagesfristEnde"],
"hashes": {
"program": "sha256:d1fbe22…",
"semantic": "sha256:e679284…",
"result": "sha256:e352d71…"
},
"via": "local"
}
Field What it says
evaluationStatus whether the answer was computed at all: COMPUTED here; other values say that data is missing, that a court must decide, or that a reading must be chosen
truthStatus what the computation supports: TRUE_ONLY — established, the period ends on 20 March; the other values are FALSE_ONLY, BOTH, NEITHER
proof the proof graph: the facts as assertions and the rule that fired; rulesApplied above is read from it — the rule TagesfristEnde, whose label in the canon cites sections 187 (1) and 188 (1)
sources the source anchors of the applied rules when the canon attaches them to the answer; this canon build returns an empty list, and the rule identifier in proof remains the address
hashes program — the canon that answered; semantic — the case as evaluated; result — the outcome; the same inputs give the same bytes
issues the engine’s messages, by code; on this call there are only defaulted context fields
document the canonical bytes of the whole evaluation document — what you archive or hand to a reviewer

src/ask.ts of the generated project asks this question and prints the same fields.

Remove frist_dauer_tage from answers and ask again:

{ "evaluationStatus": "COMPUTED", "truthStatus": "NEITHER" }

NEITHER is not “no”. It says that this computation supports neither the claim nor its negation. To learn why, ask the same question through whyNot — it returns the blockers: each candidate rule and the premises it reads:

const w = await fristen.whyNot('frist_ende', ['frist', '2026-03-20'], partial);
w.value.value.blockers;
[
{
"rule": "urn:de:corpus:clir:bgb-fristen#TagesfristEnde",
"trigger": "UNDETERMINED",
"conjuncts": [
{ "literal": { "predicate": "urn:de:corpus:clir:bgb-fristen#frist_ereignis" }, "trigger": "UNDETERMINED" },
{ "literal": { "predicate": "urn:de:corpus:clir:bgb-fristen#frist_dauer_tage" }, "trigger": "UNDETERMINED" }
]
},
{
"rule": "urn:de:corpus:clir:bgb-fristen#Verlegung193",
"trigger": "UNDETERMINED",
"conjuncts": [
{ "literal": { "predicate": "urn:de:corpus:clir:bgb-fristen#frist_ende_kalender" }, "trigger": "UNDETERMINED" }
]
}
]

(The literals’ arguments and the engine’s detail strings are omitted here.) Two rules could have concluded frist_ende. The first needs frist_ereignis and frist_dauer_tage; the second needs frist_ende_kalender, a date already computed by another rule. Compare the premises with the facts you supplied and the gap is named: frist_dauer_tage is what the application must ask the user for.

const have = new Set(partial.answers.map((f) => f.predicate));
const missing = blockers
.flatMap((b) => b.conjuncts.map((c) => c.literal.predicate.split('#')[1]))
.filter((p) => !have.has(p));
// ["frist_dauer_tage", "frist_ende_kalender"]

That is how an application asks for more: it does not guess a value and does not fall back to “no”. It shows the missing premise, collects it, and asks again — and the new answer carries its own hashes.

frist_ende, frist_ereignis, frist_dauer_tage were given to you above. In your own project you will need names nobody has handed you, and there are four places they live. None of them is a document you keep in sync by hand: all four are derived from the canon you installed.

The editor. The canon package ships generated declarations (index.d.ts) and registers them with @arxo/law, so after open(...) the predicate argument is typed by the canon: your editor completes frist_ende, and a name the canon does not know fails type checking before anything runs.

The manifest. questions() returns the canon’s own inventory: questions you can ask, facts you can supply, their parameters, and the labels the author wrote. It is what a form or a picker is built from.

const m = fristen.questions('en');
const show = (r) =>
`${r.name}(${r.parameters.map((p) => `${p.name}: ${p.typeShort}`).join(', ')}) — ${r.label}`;
show(m.facts.find((f) => f.name === 'frist_ereignis'));
show(m.questions.find((q) => q.name === 'frist_ende'));
frist_ereignis(f: Frist, tag: Date) — Ereignis oder Zeitpunkt, der für den Anfang der Frist maßgebend ist (§ 187 Abs. 1)
frist_ende(f: Frist, tag: Date) — Tag, mit dessen Ablauf die Frist endet (§§ 187, 188, 193)

Labels come in the languages the canon’s author wrote them in; this canon labels its relations in German, so 'en' falls back to that. Each question also lists relevantFacts: the facts its rules read, which for frist_ende is where frist_ereignis and frist_dauer_tage appear. Each parameter carries a type and a widget (entity, date, quantity, …), so the value you pass as a plain string — '2026-03-06', '14 calendar_day' — is wrapped by the declared type, not guessed from its shape.

The passport. passport().inModel('frist_ende') says whether the question is executable in this version of the canon at all. Build on a provision the model carries only as text, and every answer will be NEITHER.

The error. A misspelt name is refused with the nearest real ones:

await fristen.truth('frist_endee', ['frist', '2026-03-20'], caseInput);
// FactError: unknown relation "frist_endee"
// path: "query", nearest: ["frist_ende", "frist_ende_kalender", "frist_ereignis", ...]

And when the name is right but a fact is missing, the previous step already showed the fifth place: whyNot names the premises the rules were waiting for.

If you are exploring before writing code, the same inventory is available to an assistant over MCP: law_search takes a question in your own words and returns predicate addresses, law_rules lists the facts a rule reads. That path is Connect an AI assistant.

  • Understand the answer — every field, the three axes your code must keep apart, and why NEITHER and FALSE_ONLY differ: How to read an answer.
  • Connect an AI assistant — the same canons over MCP, for an agent or an editor, with one curl to the public server: Connect an AI assistant (MCP).
  • Write your own rules — the language the canon is written in, from a first rule to a package: https://docs.arxo.io/language/.
  • Python — the same canon, the same answer:
Terminal window
pip install arxo arxo-canon-bgb-fristen
from arxo import open
fristen = open("de.bgb.fristen@0.1.0")
r = fristen.truth("frist_ende", ["frist", "2026-03-20"], {
"legalTime": "2026-09-17",
"timezone": "Europe/Berlin",
"deadlinePolicy": "urn:de:corpus:clir:bgb-fristen#BGB_FRISTEN_TAG",
"answers": [
{"predicate": "frist_ereignis", "args": ["frist", "2026-03-06"]},
{"predicate": "frist_dauer_tage", "args": ["frist", "14 calendar_day"]},
],
})
r.evaluationStatus # "COMPUTED"
r.truthStatus # "TRUE_ONLY"
r.sources; r.proof; r.hashes; r.document

fristen.why_not(...) is the Python name of whyNot.

  • lens.arxo.io — a published answer with its proof, hashes, and the source provisions.
  • zan.arxo.io — the Kazakhstan slice of the corpus: what is formalized and how it answers on worked cases.

Documentation for Arxo. Writings — blog.arxo.io.

Anonymous visit counts on stats.arxo.io, no cookies.