Skip to content
Talk to our solutions team

Findings

A finding is one severity-graded validation issue produced during a rule execution: which check raised it, how bad it is, what it says, and any structured detail. Findings are collected in one list per execution, and that list answers a single question: did this execution pass?

Every execution gets its own findings and audit collectors, bound under those names by every host, so two concurrent executions never share a list. Findings are not part of out: they come back beside it.

The engine never inspects a finding: it allocates the collector, exposes it, and hands it back. Severity is the only thing that carries meaning, and only through Passed.

PartSet byIn the rules.svc response
Rule idFirst argument to every raiserule. Free text: not checked against the loaded rule names
SeverityAdd’s second argument, or the verb you usedseverity: INFO, WARNING, ERROR or CRITICAL
CategoryAdd’s third argument, or the verb’s secondcategory, omitted when empty. Free-text grouping, such as validation or completeness
MessageThe human-readable statementmessage
FieldsAdd’s comma-separated fifth argument, or .Field(name)fields, a list, omitted when empty
Details.Detail(key, value)details, an object of text values, omitted when empty
TimestampThe engine, when the finding is raisedNot returned

rules.svc returns them in every response. POST /rules/execute answers with a findings list beside out, empty when nothing was raised, in the order the rules raised them:

{
"out": { "totalsChecked": true, "status": "held", "errors": 1 },
"findings": [
{
"rule": "CheckTotals",
"severity": "ERROR",
"category": "totals",
"message": "line items do not sum to the invoice total",
"fields": ["invoice.total"],
"details": { "expected": "12000.00", "actual": "11750.00" }
}
]
}

The rest of the response is on the rules service page.

kis rules does not print findings. It prints out, and with --audit and --history the audit trail and the write history. To see a verdict in a local run, carry it in out as well (below).

Four grades, named by string everywhere a rule touches them:

SeverityFails Passed()
INFOno
WARNINGno
ERRORyes
CRITICALyes

Severity names are resolved case-insensitively, and the short forms rules actually write are accepted:

AcceptedResolves to
info, information, informationalINFO
warn, warningWARNING
error, errERROR
critical, fatalCRITICAL

An unrecognised name passed to Add resolves to INFO, so a misspelling silently downgrades an error to information. Prefer the graded verbs, where the severity is in the function name and cannot be misspelled.

Every parameter is a string, so there is nothing a rule cannot express.

CallBehaviour
findings.Add(ruleID, severity, category, message, fields)Raises a finding from five strings. fields is a comma-separated list of field keys; empty means none
findings.Info / Warn / Error / Critical (ruleID, category, message)Raise at a fixed grade. Severity is in the verb, so it cannot be misspelled
.Detail(key, value)Attaches one key/value to the finding just raised. Chainable
.Field(name)Records another field key on the finding just raised. Chainable
findings.Count()How many were raised
findings.CountOf(severity)How many carry that severity name. 0 for an unrecognised name
findings.HasSeverity(severity)Whether any carries that severity name
findings.Has(category)Whether any carries that category
findings.Passed()true when nothing at ERROR or above was raised
findings.All()The findings in the order raised. A rule can only count them, with .Len()
findings.JSON()Not callable. It returns two values; the call loads, then aborts the execution with calling function JSON which returns multiple values

Passed has only two failing grades. INFO and WARNING findings never fail an execution however many accumulate, and an empty collector passes.

The five-string form:

rule ValidateLoanAmount "loan amount must parse as a number" salience 80 {
when
!strings.MatchRegex("^[0-9.]+$", in.Str("LELoanAmount", ""))
then
findings.Add("validate_le_loan_amount_numeric", "Error", "invalid_format",
"LELoanAmount does not parse as a valid number", "LELoanAmount");
}

The four graded verbs are shorter and cannot carry a misspelled severity:

findings.Error("check_totals", "totals", "line items do not sum to the invoice total");
findings.Warn("check_dates", "completeness", "no issue date found");
findings.Info("classify", "routing", "document classified as a closing disclosure");
findings.Critical("check_signature", "integrity", "signature block is missing");

Attach detail by chaining onto the raise, and gate later rules on what has been raised. This pair produces the response shown above:

rule CheckTotals "line items must sum to the invoice total" salience 20 {
when
math.Abs(in.Double("invoice.total", 0.0) - in.Double("invoice.linesTotal", 0.0)) > 0.01
then
findings.Error("CheckTotals", "totals", "line items do not sum to the invoice total")
.Detail("expected", to.Fixed(in.Double("invoice.total", 0.0), 2))
.Detail("actual", to.Fixed(in.Double("invoice.linesTotal", 0.0), 2))
.Field("invoice.total");
out.Set("totalsChecked", true);
}
rule HoldForReview "anything at error or above holds the invoice" salience 10 {
when
!findings.Passed()
then
out.Set("status", "held");
out.Set("errors", findings.CountOf("error"));
}
facts
{ "invoice": { "total": 12000.0, "linesTotal": 11750.0 } }
out
{ "totalsChecked": true, "status": "held", "errors": 1 }

Carrying the verdict in the output as well

Section titled “Carrying the verdict in the output as well”

Findings are the validation channel, but a caller that reads only out, and every kis rules run, still needs the verdict there. Setting both costs one line and makes the result readable either way:

rule ValidateEmail "flag an unparseable customer email" salience 80 {
when
!strings.MatchRegex("^[^@ ]+@[^@ ]+\\.[a-zA-Z]{2,}$", in.Str("customer_email", ""))
then
findings.Error("ValidateEmail", "validation", "customer email failed the format check")
.Field("customer_email");
out.Set("validation_status", "FAILED");
}
facts
{ "customer_email": "ana@example" }
out
{ "validation_status": "FAILED" }

A later rule can branch on either channel: findings.HasSeverity("error") or out.Str("validation_status", "") == "FAILED".

audit is the timestamped decision trail, allocated per execution alongside findings. It is a rule’s write path for diagnostics.

CallBehaviour
audit.Log(ruleID, message)Appends an entry stamped with the current time
audit.LogWithData(ruleID, message, data)Same, carrying a structured payload. Build it with map.Of(...), passed directly
audit.Count()Number of entries
audit.All()Every entry in append order. A rule can only count them, with .Len()
audit.ForRule(ruleID)Entries whose rule id matches exactly. Count with .Len()
audit.JSON()Not callable. Two return values abort the execution
PartMeaning
Rule idThe first argument to Log / LogWithData. Free text. The engine’s own entries carry the firing rule’s name
MessageThe second argument. The engine writes executed
DataThe third argument to LogWithData. Absent for entries written by Log
CycleThe engine cycle the entry was made in, from 1
TimestampWhen the call was made. Set for you

Every rule that fires also appends an automatic entry with the message executed before its then block runs, so a rule’s own audit.Log lands immediately after its executed marker:

Terminal window
kis rules -r classify.grl --with-bbox -i pages --history --audit page1
--- Audit for pages/page1.json ---
1. [DetectInvoice] executed
2. [DetectInvoice] matched the phrase 'Invoice Number'

The automatic entry is written by the engine, not by the rule, so Count() is the number of firings plus the number of explicit Log / LogWithData calls. A rule fires once per execution, or once per cycle it repeats in when it calls agenda.Repeat().

SurfaceHow to get themWhat comes back
kis rules--audit prints them; --audit or --history-audit <dir> also records them in the <prefix>-hist-audit.json sidecarRule, message, and the data of a LogWithData entry
rules.svc"options": {"audit": true} on POST /rules/executeAn audit list beside out, each entry’s rule, message and cycle. The data payload is not returned