Skip to content

Relations

A relation is a rule that must hold between fields, such as “the total is net plus VAT”. You write it as an expression that is either true or false:

{ "id": "sum", "expr": "total == net + vat" }

A relation says what must hold, not which field to compute, and Tickmarks uses it in every direction it can. The rule above computes total from net and vat, but if an invoice shows a total and a net amount and leaves out the VAT, the same rule computes vat instead.

The obvious use is filling in values the document doesn’t state, as in the VAT example. Relations also help when the document says too much. If the model finds two amounts that could each be the total, the one that equals net + vat wins. And if the document’s confident values can’t all be true at once, say a VAT line that doesn’t add up, those fields get status conflict. The response then explains which pieces of evidence disagree and what each possible fix would give, so a person can decide.

Only == computes values. A comparison such as <, or logic such as and, can rule candidates out but never fills anything in.

Key Type Default Description
id string required Unique name. Responses use it to say which relation derived a value or caused a conflict.
expr expression required The rule. Must be true or false.
eom "clamp" | "strict" defaults.eom End-of-month rule for date arithmetic in this relation.
end "exclusive" | "inclusive" defaults.end End-date convention for this relation.
null "vacuous" | "strict" "vacuous" What happens when an operand has no value.
soft true or a decimal string in (0, 1) off Make the relation a preference instead of a rule.

These override the schema’s defaults for a single relation, which helps when one date in a document follows a different convention from the rest. Calendar and money rules explains both settings.

{ "id": "term", "expr": "lastDay == startDate + duration", "end": "inclusive" }
{ "id": "renewal", "expr": "renewalDate == startDate + dur'P1M'", "eom": "strict" }

With end: inclusive, a 12-month lease starting on 2027-03-01 gives lastDay as 2028-02-29 instead of 2028-03-01.

Documents often leave fields out, so by default a relation with a missing, null or not-applicable operand is vacuous: it counts as satisfied and computes nothing. If a relation only makes sense when every value is present, set "null": "strict" and it’s violated instead.

{ "id": "sum", "expr": "total == net + vat", "null": "strict" }

With "strict", an invoice without a VAT amount violates sum and the response reports it, where by default sum would simply be skipped.

Some rules are usually true but not always, such as “leases rarely run longer than five years”. A soft relation doesn’t rule out values that break it; it makes them less likely. With "soft": true, an assignment that violates it keeps 5% of its weight (a factor of 0.05). A string such as "0.2" sets your own factor. Soft relations never compute values.

{ "id": "usualTerm", "expr": "duration <= dur'P60M'", "soft": "0.2" }

when(condition, rule) applies rule only once condition is known to be true. It must be the outermost part of the relation, or the body of another when.

{ "id": "cut", "expr": "when(status == 'terminated', terminationDate <= endDate)" }
[
{ "id": "term", "expr": "endDate == startDate + duration" },
{ "id": "order", "expr": "startDate < endDate" },
{ "id": "sum", "expr": "total == net + vat" },
{ "id": "vat", "expr": "vat == round(net * rate / 100, 2)" },
{ "id": "line", "expr": "lineTotal == unitPrice * quantity" },
{ "id": "deposit", "expr": "deposit <= rent * 3" },
{ "id": "noticeGiven", "expr": "when(status == 'terminated', is_present(noticeDate))" }
]

Tickmarks catches relations that can’t do anything useful. One that’s always true, such as 1 == 1, is dropped with the warning W-REL-TRIVIAL. One that can never be true, such as 1 == 2, is a compile error, E-REL-FALSE, because it would rule out every possible answer.