Your first rule set
One .grl file, one JSON input, one command. Every rule on this page compiles and runs against the
engine, and the console output is what the commands print for it.
Before you start
Section titled “Before you start”| You need | Note |
|---|---|
The kis CLI | The entry point for every local command below. |
| The rules plugin | kis rules is a proxy command, not a compiled-in subcommand: see below. |
| Nothing else | No database, no service, no network. The local pass is entirely offline. |
kis rules and kis bbox 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:
kis rules --helpIf 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.
Lay out the project
Section titled “Lay out the project”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. With no input named on the command line, -i is the
directory searched for inputs: 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.
Write the rule set
Section titled “Write the rule set”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");}
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");}
rule StampClaim "Copy the identifier onto the result" salience 80 { when strings.ToUpper(status) == "SUBMITTED" then out.Set("claim_id", claim_id);}Four things in that file are load-bearing:
| Element | Why it matters |
|---|---|
out.Set(key, value) | The result. What the rules Set is exactly what the caller gets back. |
Distinct salience | The only ordering control. Higher fires first. |
10000.0 vs 10000 | Comparison 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 statement | Including the last one. A missing semicolon is a parse error. |
A rule that fires leaves the pool for the rest of the execution, so each rule fires at most once per
run. A rule does not need to end with Retract to stop itself firing again.
The full grammar is on the rule language; the callable surface is on functions.
Supply the facts
Section titled “Supply the facts”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. input/claim-1041.json:
{ "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. Over those facts the rule set writes:
{ "claim_id": "CLM-1041", "review_tier": "manager"}Run it
Section titled “Run it”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 input(s) successfullyTwo 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 matching rules went first; it did not stop the other one running.
A rule that fires leaves the pool, and every remaining rule is evaluated again 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.
See what fired
Section titled “See what fired”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 input(s) successfullyEvery 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.
--history and --audit also record their section in a <basename>-hist-audit.json sidecar:
{ "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" } ]}Write the results
Section titled “Write the results”Printing is the default. -o writes the results to a directory; --update merges them back into
each source file instead.
kis rules -r rules/ -i input/ -o results/ -k triageresults/claim-1041.json:
{ "triage": { "claim_id": "CLM-1041", "review_tier": "manager" }}| Combination | Effect |
|---|---|
neither -o nor --update | Print only. |
-o <dir> | Write only the rule results, mirrored under -o. Source untouched. |
--update, no -o | Merge the results back into each source file. |
--update with -o | -o wins; a warning says --update was ignored. |
-k <key> | Nest the results under that key instead of merging at the root. |
Three failures you will hit on run two
Section titled “Three failures you will hit on run two”In the order a new rule set tends to produce them.
A missing fact aborts the whole execution, not the rule. Feed a rule set a JSON file that does not carry the facts it reads and the run fails like this:
ERROR: /path/to/input/summary.json: executing rules: executing ruleset rules: evaluating expression in rule 'TriageHighValue' the when raised an error. got left hand expression error. got non existent key amountProcessed 1/2 input(s) successfully1 input(s) failedThe file carries none of the four facts, so the first rule the engine reaches cannot be evaluated,
and a failed evaluation aborts the execution. Every rule is evaluated every cycle, so one rule
naming an unsupplied fact fails that input. Read a fact that may be absent through in, which takes
a default and cannot fail: strings.ToUpper(in.Str("status", "")) == "SUBMITTED". A dotted path
reads into nested objects, in.Double("claim.amount", 0.0). See
Reading facts.
A misspelt method fails when the rule runs. Change strings.ToUpper to strings.ToUper in
StampClaim and run again:
ERROR: /path/to/quickstart/input/claim-1041.json: executing rules: executing ruleset rules: evaluating expression in rule 'StampClaim' the when raised an error. got left hand expression error. got this node identified as "strings" have no function named ToUperProcessed 0/1 input(s) successfully1 input(s) failed--validate finds it without an input, names the rule and line, and suggests the nearest name:
kis rules -r rules/ --validateStampClaim:19: error: strings has no method ToUper (did you mean ToUpper?) strings.ToUper(status) == "SUBMITTED"
1 diagnostic(s): 1 error(s), 0 warning(s)Add --strict to make it exit non-zero. rules.svc runs the same check when it loads the rules and
refuses a set that fails it. See Validating a rule set.
An integer literal passed to a float parameter aborts the run. Cap the amount with
math.Min(amount, 10000) and the rule fails when it fires:
ERROR: /path/to/quickstart/input/claim-1041.json: executing rules: executing ruleset rules: error while executing rule TriageHighValue. got rule engine execute panic on rule TriageHighValue ! recovered : reflect: Call using int64 as type float64The load-time check counts arguments; it does not check their types. Write 10000.0.
More failure shapes: troubleshooting.
The same rule set over HTTP
Section titled “The same rule set over HTTP”rules.svc runs the identical GRL. Two things change: the rule set is a folder of the product rather
than a path given to -r, and a call names it as a scope.
Put rules/triage.grl unchanged in the product’s rules/claim-triage/ folder. A top folder of
rules/ is a scope, called by the folder’s name:
rules/ claim-triage/ triage.grlThe change reaches the service by itself. A load that fails keeps what was served before, and
GET /rulesets says why. kis rules check, run in the product’s root, loads the folder the way the
service does, so you can check it before it ships:
rules: 1 folder scope(s), 0 static scope(s): they load claim-triage folder, 1 rule file(s)Post the facts. The keys of input are the facts, the same top-level keys the input file carried:
curl -sS -X POST https://rules.example.com/rules/execute \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -H "X-Customer: acme" -H "X-Env: dev" -H "X-Product: claims" -H "X-Tenant: main" \ -d '{"scope":"claim-triage","input":{"claim_id":"CLM-1041","status":"submitted","amount":12500.0}}'{ "scope": "claim-triage", "stamp": "sha256:…", "principal": { "id": "u-claims-agent", "ptyp": "user" }, "out": { "claim_id": "CLM-1041", "review_tier": "manager" }, "findings": [], "actions": []}out is the output collector, the same object kis rules printed. findings holds what the rules
raised with findings.*, stamp identifies the exact rule text that decided, and principal is who
the rules ran as. Add "options": {"audit": true} and the trail comes back beside it, the same
entries --audit printed above:
{ "audit": [ { "rule": "TriageHighValue", "message": "executed", "cycle": 1 }, { "rule": "TriageHighValue", "message": "amount above the 10000 threshold", "cycle": 1 }, { "rule": "StampClaim", "message": "executed", "cycle": 2 } ]}What differs from the local run
Section titled “What differs from the local run”kis rules | POST /rules/execute | |
|---|---|---|
| Rule-set selection | The files -r names. --scope runs a product’s scope by name, as the service does. | scope: a top folder of rules/, or a static scope of rules/rules.yaml. Required. |
| Facts | Top-level keys of the input file, plus -f and -v | The keys of input, plus the named facts in facts |
| Nested facts | in.* with a dotted path | The same |
| Audit trail | Printed with --audit, written to the sidecar | Returned under audit when options.audit is true |
| Findings | Raised with findings.*; not printed | Returned under findings on every call |
| Documents | -b / --with-bbox | None. Rules that read a document run through kis rules only |
log namespace | Bound | Bound |
A rule that fails mid-run answers 422 rules-execution-failed, naming the rule, with what the rules
had written before the failure under meta.partial. A scope the product does not have answers
404 rules-scope-not-found, and a tenant whose rules have not loaded yet answers
503 rules-tenant-not-loaded with Retry-After. Codes and messages: errors.
Routes, headers and tenancy: the rules service.
Where to go next
Section titled “Where to go next”| You want to | Page |
|---|---|
| Chain one rule’s decision into another’s condition | Rule patterns |
| Look up an operator, a literal form or fact-access syntax | The rule language |
Find the function you need in strings, map, array, time, math | Functions |
| Copy a rule shape that is known to work | Patterns |
| Run rules over scanned pages instead of JSON | OCR input |
| See every flag on every local command | The CLI |
| Declare a scope’s facts and limits | Scopes |