What is this?
Admin note: Rulebooks decide what the application does on its own: which invoices get a reminder, who approves a received document, how a line of a received invoice finds its product, how much is ordered from a supplier, what an automation does when an order is paid. Most organisations only switch the ready-made rules on and fill in their settings (Settings > the area of the rules). Writing rules of your own in this language is for a consultant or a developer – or describe the rule in a sentence and let the AI assistant in the rules dialog write it.
Key points:
- One language everywhere: dunning, approval, receiving, purchasing, payment pairing, paying, fulfillment, shipping, the warehouse and automations all use the same rulebooks.
- Safe by design: a rule reads only the facts its area offers and does only the actions its area allows; every result says which rule produced it (“Why”).
- Try before saving: the rulebook tester checks a rulebook and runs it on facts you type in.
Rulebooks
A rulebook is a Markdown text of rules for the rule engine. One rulebook belongs to one domain: dunning, approving
documents, receiving, purchasing, payment pairing, paying, fulfillment, shipping, the warehouse domains and automations.
The domain decides what the rules may read (facts), decide (targets), look up (resolvers) and do
(actions), and when they run. This page describes the language; what each domain offers (its facts, targets, resolvers, actions and triggers) is
listed in the rulebook tester for the domain you pick, and on the MCP server as
doc://rulebook-domains. A rule may use only what its domain declares.
1. A rulebook
# Upomínky
domain: dunning
const minimum = 50
const firstTemplate = 'UPOMINKA-1'
## Neupomínat malé částky
> Faktura, na které zbývá zaplatit méně než nastavená částka, se neupomíná.
id: minimum
when: remaining < minimum
stop
## 1. upomínka
> Několik dní po splatnosti dostane partner první upomínku se všemi fakturami po splatnosti.
id: first
level: 1
when: daysOverdue >= 3 and reminderLevel < 1
do sendMessage firstTemplate group by partner.id once
- The first line
# <title>is the name of the rulebook. Only one#heading is allowed. - Header lines come before the first rule:
domain: <domain>(required);const <name> = <value>(settings the rules use by name);scope: <entities>orscope: *,language: <code>,disable: <rule ids>,secret: true|false.
## <rule title>starts a rule (a node).### <rule>under it is a more specific rule: it applies only when the condition of its parent holds too. A heading may not skip a level.- Lines under a rule, in this order by convention:
| Line | Meaning |
|---|---|
> <text> |
the description of the rule (one sentence, in the language of the users) |
id: <id> |
a stable id: letters, digits, -, _; unique in the rulebook. Without it the id is made from the title |
on: <triggers> |
when the rule runs (only domains with triggers, section 6) |
when: <condition> |
the condition of the rule (section 3); a rule without when: applies to every subject |
default: on|off |
a ready-made rule switched on or off until the organisation decides |
level: <n> |
the level of the rule (a reminder level, an approval level) |
automation: off|suggestOnly|assisted|full |
how automatically the rule acts, replacing the domain’s level |
excludes: <ids> |
ready-made rules that cannot be active together with this one |
validFrom: <year>, validTo: <year> |
the years the rule is valid |
secret: true |
the text of the rule is never sent to the browser |
param <name>: <type> [= <default>] ["<label>"] |
a parameter of a ready-made rule (section 7) |
| statements | what the rule does (section 2) |
//starts a comment line. Any other line that is not a directive or a statement is prose and is ignored.
2. Statements
A domain allows only some of the statements: the validator reports a statement the domain does not list.
| Statement | Meaning |
|---|---|
set <target> = <value> [when <condition>] |
decides the value of a target |
suggest <target> = <value> [confidence <0–1>] [when <condition>] |
recommends a value; it never decides |
restrict <target> in (<values>) |
the allowed values of a target |
check <condition> else block|error|warning|info "<message>" |
a finding when the condition is false; block stops the outcome (not ready, not ordered, not sent) |
try <target> by <resolver> [<arguments>] [as suggest] [when <condition>] [then set|suggest …] |
a lookup; the tries run in written order and the first that finds exactly one wins |
else ask ["<question>"], else skip, else create <entity> with <field> = <value>, … |
what happens when no try found exactly one |
do <action> <arguments> [when <condition>] [once | once per <n> days | every <n> days | always] [group by <value>] |
an action; once is the default |
let <name> = <value> |
a fact of the rule, usable by the statements after it (section 5) |
priority + <value> | - <value> | = <value> [when <condition>] |
adds to (or sets) the score of the subject |
group <name> by <value> [max <n>] [when <condition>] |
builds groups of subjects; max may name a setting |
cluster by <value> [max <n>] |
groups similar subjects |
stop |
the rules after this one do not run for the subject |
How the outcome is made
- Every matching rule applies, in written order.
stopends the rest for the subject. - All matching
doactions run (not just the first one). Each action has an idempotency key made of the rulebook, the rule, the action and the subject or group, sooncereally runs once. - Several
sets of one target: the most specific rule (deepest heading) wins; equal ones are a conflict. - Layers: system rulebooks (ready-made rules), then the organisation’s methodology, then its own rulebooks in their order.
- A
checkwithblockblocks the subject. Every blocking reason is reported, not only the first.
Collections: per line, per component
A statement prefixed with a collection name runs once per element of it:
lines: try product by ean
lines: try product by supplierCode
lines: else ask "Vyberte nebo založte produkt pro ${name}"
lines: check priceDifference <= priceTolerance else warning "Cena ${name} se liší od poslední nákupní ceny"
lines: when: quantity > 0
Inside a collection the element’s facts are read by name. The subject is read by the parent name the domain declares,
for example document.partner. <collection>: when: <condition> restricts the elements a rule applies to.
3. Conditions
when:, the when of a statement and check use the filter language (Where):
remaining < minimum
daysOverdue >= 3 and reminderLevel < 1
partner.labels.code in ('VIP', 'PARTNER')
labels.code = 'BEZ-UPOMINEK'
note is null
name like 'servis' // contains
code begins 'ZB'
isWorkday = false
total > 100000 and (costCenter.code = 'MKT' or branch.code = 'PRAHA')
-
Texts are in single quotes; numbers,
trueandfalseare bare. -
A relation or a list fact is followed with a dot. For a list,
labels.code = 'VIP'means “any of them”. -
A comparison with an unknown (
null) fact is false; facts never fail a rule. -
${name}inside a condition is replaced by a setting or parameter before the condition is read:partner.labels.code in ('${companyLabels}'). -
Names a condition may read:
- the facts of the domain;
- for domains that run on records (automation, dunning, approval), also the record’s fields and relations;
- the calendar:
today,now,country,isWorkday,isHoliday; - the rulebook’s
consts, the domain’s settings and the rule’s parameters; - the rule’s
lets.
Any other name is reported as
Unknown fact "x".
4. Values and messages
The value of set, the arguments of do and the value of let are expressions:
| Form | Example |
|---|---|
| fact, field, setting | quantity, partner.code, minimum |
| text, number, boolean | 'UPOMINKA-1', 11, true |
| arithmetic | quantity * 2, dueDate + 14, total - paid |
| functions | roundup(quantity, 11) (up to a multiple), max(quantity, minimumQuantity), min(a, b) |
template (a text with ${…}) |
"Objednávka ${code} je zaplacená", 'Upomínka ${number}' |
The messages of check and else ask are templates: "Objednávka u ${supplier.name} za ${orderTotal} je nad limitem".
5. Facts of the rule: let
| Form | Meaning |
|---|---|
let rest = total - paid |
an expression over the facts |
let lastPrice = lookup purchasePrices[product_id = '${product.id}'].price |
one field of the first record of an entity matching a filter (preloaded by the server) |
let score = script 'return facts.quantity * 2' |
sandboxed JavaScript (QuickJS): it sees only the facts, no network, no time; returns JSON |
let suspicious = ai boolean "Is the description of the invoice unusual?" using description, partner.name |
a question to the AI: boolean, number or text. Only in background runs, under the AI data policy; a fact the policy forbids is not sent and the answer is unknown |
6. Triggers: on:
Only domains with triggers (automations) take on:; the other domains are run by the application (on save, every day,
on the ordering page, …).
on: saved
on: created, deleted
on: changed isPaid to true
on: changed status from 'new' to 'confirmed'
on: event 'paid'
on: schedule daily
on: manual
- Several triggers are separated by commas. A rule without
on:takes its parent’s triggers; a rule with noon:above it runs on every trigger. - Events:
paid(a payment was paired),sent(a message about the record was sent),fromInbox(a document was created from the inbox). - In an automation the facts
previous.<field>(the value before the change) andchanged.<field>(true when the change touched the field) are available.
7. Ready-made rules, settings and parameters
System rulebooks hold ready-made rules: rules with an id, a description, default: on|off and the settings they
use. The organisation switches them on or off and fills in their settings; only that choice is stored (as activations),
never the text. Its own rules are written in its own rulebooks of the same domain and run after the system ones.
## Velké částky
> Nad nastavenou částku schvaluje vedoucí střediska, jeho nadřízený a pak jednatel.
id: large
default: off
when: total > largeAmount
do requireApproval by costCenter.manager then by costCenter.manager.superior then by role '${directorRole}'
A parameter of a rule: param <name>: <type> [= <default>] ["<label>"]. The types are:
number,money,days,text,template;code <entity>,codes <entity>,where <entity>;enum(a, b, c).
A parameter is used as ${name} or as a bare name.
param tolerance: money = 1 "Tolerance"
param labels: codes labels = 'VIP' "Štítky"
8. Examples by domain
Receiving: finding the product of a line
## Hledání produktu podle kódů
id: codes
lines: try product by ean
lines: try product by packageEan
lines: try product by supplierCode
lines: else ask "Vyberte nebo založte produkt pro ${name}"
Purchasing: quantity by packages, a suspicious order
## Množství podle dodavatele
id: packages
set quantity = roundup(max(quantity, minimumQuantity), packageQuantity)
## Podezřelá objednávka
id: suspicious
check orderTotal <= suspiciousOrder else block "Objednávka u ${supplier.name} za ${orderTotal} je nad limitem – zkontrolujte ji"
Approval: small amounts without approval, a changed bank account to the accountant
## Malé částky bez schvalování
id: small
when: total <= 5000
do approve
stop
## Změněný bankovní účet
id: account
when: bankAccountChanged = true
do requireApproval by role 'UCETNI'
stop
Automation: a paid order gets a task for the warehouse
## Zaplacená objednávka
id: paid
on: changed isPaid to true
do createTask 'Vyskladnit ${code}' for 'role:SKLAD'
9. Errors
The parser and the domain report each problem with its line:
| Error | Cause |
|---|---|
A single # heading is the title of the rulebook; nodes start at ## |
a second # heading |
Heading level skipped |
#### directly under ## |
`when:` belongs to a node, after its heading |
a rule line before the first ## |
Condition "…": … |
the condition is not a valid filter |
Expression "…": … |
the value is not a valid expression |
The id "x" is used twice |
two rules with one id: |
Unknown fact "x"; expected … |
a name the domain does not have (a typo, or a fact of another domain) |
check expects `check <where> else block|error|warning|info "<message>"` |
the else part or the quotes of the message are missing |
`try` expects `try <target> by <resolver> …` |
by is missing |