Skip to content
Talk to our solutions team

Your first rule set

One .grl file, one JSON input, one command. Every rule on this page compiles and runs against the pinned engine, and the console output is what the commands print for it.

You needNote
The kis CLIThe entry point for every local command below.
The rules pluginkis rules is a proxy command, not a compiled-in subcommand — see below.
Nothing elseNo database, no service, no network. The local pass is entirely offline.

kis rules, kis ocr, kis bbox and kis hocr2bbox are dispatched to a separately installed plugin executable. kis forwards your arguments to it untouched and contains none of the rules logic itself. Check it is present before anything else:

Terminal window
kis rules --help

If the plugin is missing, the command fails immediately and nothing else on this page works:

Error: 'rules' is not installed.

A second line names the install command for the plugin package. The plugin is looked for in the directory holding the kis binary, then ~/.kisai/bin, then every entry of $KIS_PLUGIN_PATH, then $PATH; the first executable match wins. Full flag reference: the CLI.

quickstart/
rules/
triage.grl # the rule set
input/
claim-1041.json # one input document; its top-level keys become the facts

-r rules/ compiles every .grl file in the directory into one rule set — it also takes a single .grl file, and -R walks subdirectories. -i must be a directory; it processes every *.json file in it, one execution per file. Files not matching -p (default *.json) are skipped silently, and matching nothing at all is an error.

Every top-level key of the input file is bound as its own fact, under its own name. There is no wrapper object and no schema.

{
"claim_id": "CLM-1041",
"status": "submitted",
"amount": 12500.0,
"memo": "Closing Balance: $7,325.00"
}

That gives four facts: claim_id, status, amount and memo. A rule reads them as bare identifiers.

rule TriageHighValue "Claims above the review threshold go to a manager" salience 100 {
when
amount > 10000.0
then
out.Set("review_tier", "manager");
audit.Log("TriageHighValue", "amount above the 10000 threshold");
Retract("TriageHighValue");
}
rule TriageStandard "Everything else is auto-adjudicated" salience 90 {
when
amount <= 10000.0
then
out.Set("review_tier", "auto");
audit.Log("TriageStandard", "amount within the 10000 threshold");
Retract("TriageStandard");
}
rule StampClaim "Copy the identifier onto the result" salience 80 {
when
strings.ToUpper(status) == "SUBMITTED"
then
out.Set("claim_id", claim_id);
Retract("StampClaim");
}

Five things in that file are load-bearing:

ElementWhy it matters
out.Set(key, value)The only thing that leaves the engine. The result is exactly what the rules Set.
Retract("SelfName")Removes the rule from the pool for the rest of this execution. Without it the rule re-fires every cycle.
Distinct salienceThe only ordering control. Higher fires first.
10000.0 vs 10000Comparison operators coerce, so either works here. Function arguments do not: passing 10000 where a float parameter is declared fails with reflect: Call using int64 as type float64.
Trailing ; on every then statementIncluding the last one. A missing semicolon is a parse error.

The full grammar is on the rule language; the callable surface is on functions.

Terminal window
kis rules -r rules/ -i input/
Result for /path/to/quickstart/input/claim-1041.json:
{
"claim_id": "CLM-1041",
"review_tier": "manager"
}
Processed 1/1 file(s) successfully

Two of the three rules fired. TriageHighValue matched because amount is 12500, StampClaim matched on status, and TriageStandard never matched because its condition is the negation of the first rule’s.

Salience decided which of the two runnable rules went first; it did not stop the other one running. Every retracted rule leaves the pool, and every remaining rule is re-evaluated on the next cycle — so a lower-salience rule still fires later unless its own when is false. Make competing rules mutually exclusive in their conditions; do not use salience to suppress one.

The final line is the exit contract: any failed input prints ERROR: <path>: <err>, processing continues, and the command exits 1 with a count of what succeeded.

Terminal window
kis rules -r rules/ -i input/ --history --audit --history-audit ./audit/
--- History for /path/to/quickstart/input/claim-1041.json ---
1. [TriageHighValue] review_tier = "manager"
2. [StampClaim] claim_id = "CLM-1041"
--- Audit for /path/to/quickstart/input/claim-1041.json ---
1. [TriageHighValue] executed
2. [TriageHighValue] amount above the 10000 threshold
3. [StampClaim] executed
Result for /path/to/quickstart/input/claim-1041.json:
{
"claim_id": "CLM-1041",
"review_tier": "manager"
}
Processed 1/1 file(s) successfully

Every out.Set is attributed to the rule that wrote it — the engine stamps the firing rule name onto each history entry. The executed audit lines are added automatically for every rule that fires; the other lines are your own audit.Log calls.

The same two collections are also written to a <basename>-hist-audit.json file:

{
"history": [
{ "rule_id": "TriageHighValue", "key": "review_tier", "value": "manager" },
{ "rule_id": "StampClaim", "key": "claim_id", "value": "CLM-1041" }
],
"audit": [
{ "rule_id": "TriageHighValue", "message": "executed" },
{ "rule_id": "TriageHighValue", "message": "amount above the 10000 threshold" },
{ "rule_id": "StampClaim", "message": "executed" }
]
}

Printing is the default. -o alone writes nothing — --update is what turns on writing.

Terminal window
kis rules -r rules/ -i input/ -o results/ -k triage --update --history-audit ./audit/
{
"triage": {
"claim_id": "CLM-1041",
"review_tier": "manager"
}
}
CombinationEffect
neither -o nor --updatePrint only.
--update, no -oMerge the results back into each source file.
--update with a different -oWrite only the rule results, mirrored under -o. Source untouched.
-k <key>Nest the results under that key instead of merging at the root.

In the order a new rule set tends to produce them.

The sidecar becomes an input. Run the first command twice in a row without --history-audit:

ERROR: /path/to/input/claim-1041-hist-audit.json: executing rules on /path/to/input/claim-1041-hist-audit.json: executing ruleset rules: evaluating expression in rule 'TriageStandard' the when raised an error. got left hand expression error. got non existent key amount
Processed 1/2 file(s) successfully
1 file(s) failed to process

kis ocr filters *-hist-audit.json out of its inputs. kis rules does not.

A missing fact aborts the whole execution, not the rule. That is the same error, read a different way: the sidecar carries none of the four facts, so the first rule the engine reaches cannot be evaluated, and evaluation failure is unconditional abort — every rule is evaluated every cycle, so one rule referencing an unsupplied fact discards all output for that input. Guard optional facts with in.Has("status") && strings.ToUpper(status) == "SUBMITTED"&& short-circuits, so the guarded term is never evaluated when the fact is absent, and in.Has is the one input accessor that works.

A rule with no Retract runs until the cycle cap. Delete the Retract line from StampClaim:

ERROR: ...: executing ruleset rules: the GruleEngine successfully selected rule candidate for execution after 5000 cycles, this could possibly caused by rule entry(s) that keep added into execution pool but when executed it does not change any data in context. Please evaluate your rule entries "When" and "Then" scope. You can adjust the maximum cycle using GruleEngine.MaxCycle variable

Not a hang — a hard failure at cycle 5001, and no partial result. The cap the message tells you to raise is fixed at 5000 on both surfaces. Neither kis rules nor rules.svc exposes a flag or a config key for it — fix the rule set instead, usually by adding the missing Retract.

in.GetStr and friends return the zero value. Reading a fact through the typed input accessors silently yields nothing:

rule Probe salience 10 {
when
in.Has("status")
then
out.Set("direct", status);
out.Set("via_getstr", in.GetStr("status"));
Retract("Probe");
}
{
"direct": "submitted",
"via_getstr": ""
}

in.GetStr, in.GetInt, in.GetFloat, in.GetBool and in.GetMap all return their zero value on facts that are present, and in.Get returns an opaque reflection wrapper that serializes to {}. The lookup hands back that wrapper rather than the value, so every type assertion inside the accessor fails. Read facts by their bare name. in.Has and the in.Mem* scratch-memory family work normally.

More failure shapes: troubleshooting.

rules.svc runs the identical GRL. Two things change: the rules arrive as product metadata rather than from -r, and the rule set is selected by name.

Deliver the rule set under the product’s rules metadata. Each entry’s name becomes a rule-set name and rule carries the GRL source:

rules:
- name: claim-triage
rule: |
rule TriageHighValue "Claims above the review threshold go to a manager" salience 100 {
when
amount > 10000.0
then
out.Set("review_tier", "manager");
audit.Log("TriageHighValue", "amount above the 10000 threshold");
Retract("TriageHighValue");
}
rule TriageStandard "Everything else is auto-adjudicated" salience 90 {
when
amount <= 10000.0
then
out.Set("review_tier", "auto");
Retract("TriageStandard");
}
rule StampClaim "Copy the identifier onto the result" salience 80 {
when
strings.ToUpper(status) == "SUBMITTED"
then
out.Set("claim_id", claim_id);
Retract("StampClaim");
}

Only name and rule are read. A rule set that fails to compile is logged and skipped, not fatal — so a typo removes one rule set and leaves the service running.

Post the facts. The entire body becomes the fact map, so the facts are the same top-level keys the input file carried:

Terminal window
curl -sS -X POST https://rules.example.com/rule \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"claim-triage","claim_id":"CLM-1041","status":"submitted","amount":12500.0}'
{
"claim_id": "CLM-1041",
"review_tier": "manager"
}

The response body is the output collector. Add "audit": true to the request and the trail comes back beside it, under a reserved audit key holding the same entries --audit printed above:

Terminal window
curl -sS -X POST https://rules.example.com/rule \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"name":"claim-triage","claim_id":"CLM-1041","status":"submitted","amount":12500.0,"audit":true}'
{
"claim_id": "CLM-1041",
"review_tier": "manager",
"audit": [
{"rule_id": "TriageHighValue", "message": "executed"},
{"rule_id": "TriageHighValue", "message": "amount above the 10000 threshold"}
]
}
kis rulesPOST /rule
Rule-set selectionFixed. --ruleset is accepted and never read.The body’s name field, required.
FactsTop-level keys of the input file, plus -f and -vEvery top-level key of the request body
Nested factsBracket access, one levelThe same — bracket access, one level
Audit trailPrinted with --audit, written to the sidecarReturned under audit when the body sets "audit": true
FindingsNot surfaced — no rule can record oneNot surfaced
Document namespaceAvailable with -b / --with-bboxAvailable with a bbox object on the body
log namespaceAbsentAbsent

Document rules run here too. Add a reserved bbox object beside the facts, keyed by the name the rule passes to bbox.Get, and the service parses and analyses each document before the first rule is evaluated — the HTTP counterpart of -b page=invoice.bbox:

{
"name": "invoice-checks",
"total": 1200,
"bbox": {
"page": {"pages": [{"number": 1, "width": 612, "height": 792, "words": []}]}
}
}

The rule text is unchanged between the two surfaces — see documents in rules.

Execution failures come back as 400 with a kiserr envelope — including rule engine not initialized, which is a readiness condition rather than a client error and is what an empty metadata load looks like from the caller’s side. Codes and messages: errors. Boot, metadata polling and tenancy: running rules.svc.

You want toPage
Chain one rule’s decision into another’s conditionWriting rules
Look up an operator, a literal form or fact-access syntaxThe rule language
Find the function you need in strings, map, array, time, mathFunctions
Copy a rule shape that is known to workPatterns
Run rules over scanned pages instead of JSONOCR input
See every flag on every local commandThe CLI
Send documents and facts on one requestRunning rules.svc