Skip to content
Talk to our solutions team

Rules Troubleshooting

Every entry below is a symptom you can observe, what it means, a command that confirms it, and the fix. Failures in this block are quiet in several places. An empty result is far more common than an error, so read the “confirm” step before changing rules.

What you seeSection
got N error(s) in grl the scriptRule sets that will not load
<ns> has no method <M>, <call> takes N argument(s)Rule sets that will not load
<call> returns <type>, which a rule variable cannot holdRule sets that will not load
<call> is deprecated; use <replacement>Rule sets that will not load
An edited rule set still runs its old rulesRule sets that will not load
A rule fires once and never againRules that never fire
A rule gated on findings.* never firesRules that never fire
Output differs between identical runsRules that never fire
got non existent key <fact>Executions that abort
this node identified as "<fact>" is not referencing to an objectExecutions that abort
<fact>[string-><key>] is not an array nor mapExecutions that abort
reflect: call of reflect.Value.Type on zero ValueExecutions that abort
reflect: Call using int64 as type float64Executions that abort
this code part should not be reachedExecutions that abort
the GruleEngine successfully selected rule candidate ... after 5000 cyclesExecutions that abort
out.Append turned a number into textAccessors that return nothing
Error: 'rules' is not installed.Command line
no inputs resolvedCommand line
recovered : runtime error: invalid memory address or nil pointer dereferenceCommand line
got non existent key bboxCommand line
503 rules-tenant-not-loadedThe deployed service
404 rules-scope-not-foundThe deployed service
400 rules-facts-invalidThe deployed service
400 rules-json-request-parse-failedThe deployed service
403 rules-access-deniedThe deployed service
422 rules-execution-failedThe deployed service
504 rules-execution-timeoutThe deployed service
loading rules: /srv/rules/b.grl: got 1 error(s) in grl the script

Means: the rule text failed to parse. Two unrelated causes produce the identical message: a syntax error, or a rule name that is already registered in that rule set.

Confirm: the count is the only detail you get: no line, no token. Bisect by loading one file at a time:

Terminal window
kis rules -r ./rules/a.grl --validate

Fix: rule names must be unique across every .grl file loaded in one run, because a folder is pooled into a single rule set and rules are keyed by name. Rename the collision, or fix the syntax. A # comment (the marker is //) and a struct literal are the common syntax traps, see struct literals.

R:3: error: strings has no method Trim (did you mean Join?)
then out.Set("v", strings.Trim("x"));
R:4: error: in.Str takes 2 argument(s), called with 1
out.Set("w", in.Str("x"));

Means: every call a rule makes is checked when the rule set loads. A method that does not exist, or a wrong number of arguments, is an error naming the rule and the line, with a did-you-mean when one is close. in.Transform does not exist: a rule calling it fails with in has no method Transform.

Confirm: kis rules --validate prints the diagnostics above without running anything; add --strict to exit non-zero on an error. kis rules check <product> checks a product’s rules/ folder the way rules.svc loads it, naming the file:

rules "invoice/**" do not validate:
rules/invoice/b3.grl: rule R, line 3: strings has no method Trim (did you mean Join?)

rules.svc refuses to load a folder with such an error and keeps what it served. A plain kis rules run does not check: it runs, and the rule fails when it fires, with got this node identified as "strings" have no function named Trim.

Fix: correct the call. For string clean-up use the string’s own methods (s.Trim(), s.ToUpper(), s.Replace(old, new)) or the strings helpers; the transform names trim, upper, lower, normalize_spaces and trim_currency are what bbox.PostprocessRows takes for table rows. Argument types are not checked at load: see reflect: Call using int64 as type float64.

A:3: error: map.New returns map[string]interface {}, which a rule variable cannot hold; "m" will read back as a zero value and the next call using it aborts the page. Inline the call, or stage it with out.Set and read it back with out.GetMap

Means: a rule assigned a map or a slice to a local variable. Such a variable reads back as a zero value, and the next call using it aborts the execution.

Confirm: kis rules --validate or kis rules check reports it. A plain kis rules run aborts when the rule fires, with this code part should not be reached for a map, or reflect: Call using zero Value argument for a slice passed onward.

Fix: pass the call straight to the next one, or stage the value with out.Set and read it back with the typed reader: out.Map("m"), or out.Value("rows").Rows() for rows. Build a map with map.Of(k1, v1, k2, v2, …) passed directly to the call that takes it.

B:7: warning: out.Append is deprecated; use out.Set

Means: the call works and the rule set loads, but the name is on its way out.

DeprecatedUse
in.GetStr, in.GetInt, in.GetFloat, in.GetBool, in.GetMap, in.Getin.Str, in.Int, in.Double, in.Bool, in.Map, in.Value
in.MemSet, in.MemGet, in.MemGetStr, in.MemGetRows, in.MemDelete, in.MemClear, in.GetMemAsMapout.Set, and the out readers
in.NewMapWithValuesmap.Of
out.GetStr, out.GetInt, out.GetFloat, out.GetBool, out.GetMapout.Str, out.Int, out.Double, out.Bool, out.Map
out.Appendout.Set, one key per entry (out.Set("failed.total_mismatch", "ERROR"))
bbox.Getbbox.Doc

Confirm: kis rules --validate groups the warnings by kind.

An edited rule set still runs its old rules

Section titled “An edited rule set still runs its old rules”

Means: the load that the edit triggered did not build, so the tenant kept serving the rules it had. A load builds the tenant’s whole rules/ folder, every folder and static scope, or none of it.

Confirm: GET /rulesets lists what the tenant serves and, after a failed load, says why under error, naming the file:

Terminal window
curl -sS https://rules.example.com/rulesets -H 'Authorization: Bearer <token>' -H 'X-Customer: acme' -H 'X-Product: erp' -H 'X-Env: prod' -H 'X-Tenant: eu1'

Run kis rules check over the same folder to reproduce the error offline.

Fix: fix the file. The next change under rules/ loads again.

Means: a rule that fires is retracted for the rest of the execution, so each rule fires at most once per run, however long its when stays true. A Retract("SelfName") at the end of a then is harmless and unnecessary.

Confirm: --audit shows one executed entry per rule.

Fix: a rule that must fire again calls agenda.Repeat() in its then, and its then must falsify its own when, or it runs to the cycle cap:

rule Count salience 10 {
when out.Int("n", 0) < 3
then
out.Set("n", out.Int("n", 0) + 1);
agenda.Repeat();
}
facts
{}
out
{ "n": 3 }

Means: a rule’s when sees every earlier write to out, but raising a finding is not such a write. When the rule that raises a finding writes nothing to out, a later when !findings.Passed() can keep the answer it computed before the finding existed.

Confirm: --audit shows the raising rule and not the gated one.

Fix: write the verdict to out in the rule that raises the finding, out.Set("decision", "reject") beside findings.Critical(...), and gate later rules on out. Inside a then, findings.Count() and findings.Passed() read the findings raised so far correctly.

The same rule set produces different output on different runs

Section titled “The same rule set produces different output on different runs”

Means: rules of equal salience have no defined firing order. Running the same three default-salience rules twelve times produced five distinct firing orders.

Confirm: run the same input twice with --audit and compare the order of the audit entries.

Fix: give every rule whose position matters a distinct salience. Exactly one rule fires per cycle, highest salience first, so distinct salience is the only ordering guarantee. The lower rule fires last and its write stands:

rule High "high" salience 100 {
when in.Has("x")
then out.Set("tier", "high");
}
rule Low "low" salience 1 {
when in.Has("x")
then out.Set("tier", "low");
}
facts
{ "x": 1 }
out
{ "tier": "low" }

Means: the rule output map was empty. The run skips an empty output, so --update leaves the source file untouched, -o writes nothing, and nothing is printed except Processed N/M input(s) successfully. There is no “0 rules fired” diagnostic at the default log level.

Confirm: --audit prints the decision trail; an empty trail means no rule matched.

Fix: check the fact names your conditions reference. Input .json top-level keys become top-level fact identifiers, so a rule reads docid == "d1" directly, or in.Str("docid", "").

Means: bbox.PostprocessRows(rows, key, fn) returns the rows untouched when fn is a name it does not know, and a malformed ExtractRegex pattern behaves the same way. There is no error and no log line.

Confirm: emit the rows straight after the transform and compare them with the input.

Fix: check the name against trim, upper, lower, normalize_spaces, trim_currency and ExtractRegex("pattern", "default"); see tables.

Executions that abort and discard everything

Section titled “Executions that abort and discard everything”

One failing rule ends the whole execution. There is no per-rule isolation. kis rules prints ERROR: <path>: <err> for that input and no result. rules.svc answers 422 rules-execution-failed, and POST /rules/execute returns what the rules had written so far under the error’s meta.partial.

A missing fact aborts the run from either clause, and the two wrap the message differently. From when:

executing ruleset invoice: evaluating expression in rule 'CheckTotal' the when raised an error. got left hand expression error. got non existent key order

From then:

executing ruleset invoice: error while executing rule CheckTotal. got non existent key vendor

Means: a rule named a fact the caller did not supply. The rule does not have to be relevant to the request: a bare fact name in any rule’s when is evaluated whether or not that rule could fire, and the load-time check does not catch it.

Confirm: both forms name the rule and the fact. Compare that fact name against the keys of the request’s input or the input file.

Fix: read optional facts through in.* with a default, which never aborts:

rule CheckTotal salience 20 {
when in.Double("order.Total", 0.0) > 1000.0
then out.Set("review", true);
}
facts
{ "order": { "Total": 1500.0 } }
out
{ "review": true }

A guard in the same expression works too, in.Has("order") && order["Total"] > 1000.0, because && short-circuits and the bare name is never evaluated when the guard is false.

this node identified as "<fact>" is not referencing to an object

Section titled “this node identified as "<fact>" is not referencing to an object”
executing ruleset invoice: evaluating expression in rule 'D' the when raised an error. got left hand expression error. got this node identified as "order" is not referencing to an object

Means: dot access was used on a map fact. Only structs and pointers to structs support dot access, and a JSON object arrives as a map.

Confirm: the message names the fact. Any order.Total form against decoded JSON hits this.

Fix: read through a dotted path, in.Double("order.Total", 0.0), or use bracket access, order["Total"].

<fact>[string-><key>] is not an array nor map

Section titled “<fact>[string-><key>] is not an array nor map”

Means: two-level indexing into decoded JSON. The intermediate value is untyped, so it cannot be indexed again.

Confirm: doc["header"] alone resolves; doc["header"]["total"] fails.

Fix: read nested values through a dotted path:

rule ReadTotal salience 20 {
when in.Has("doc")
then out.Set("total", in.Double("doc.header.total", 0.0));
}
facts
{ "doc": { "header": { "total": 12.5 } } }
out
{ "total": 12.5 }

reflect: call of reflect.Value.Type on zero Value

Section titled “reflect: call of reflect.Value.Type on zero Value”
executing ruleset invoice: error while evaluating rule Second ! recovered : reflect: call of reflect.Value.Type on zero Value

Means: out.Get("k") == "v" compared a key no rule has written yet. out.Get answers nothing for an unset key, and the comparison aborts.

Fix: use the typed reader with a default: out.Str("k", "") == "v", out.Double("k", 0.0), out.Has("k").

Means: an integer literal was passed to a float64 parameter: math.Min(10, 20), in.Double("total", 0), NeedsReview(1). The load-time check counts arguments, not their types, so this surfaces only when the rule runs. reflect: Call using float64 as type int64 is the mirror image.

Fix: write a float literal with a decimal point, 10.0, 0.0, wherever the parameter is a float64, and a whole number wherever it is an int64.

Means: a local variable held a map, m = map.New(); m["k"] = 1;. kis rules --validate reports it at load; see a variable that cannot hold its value.

Fix: build the map with map.Of(k1, v1, k2, v2, …) passed straight to the call.

executing ruleset loop: 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

Means: a rule that calls agenda.Repeat() kept qualifying until the cycle cap tripped. Without agenda.Repeat() a rule fires at most once and cannot loop.

Confirm: the rule at fault calls agenda.Repeat() and its then does not falsify its own when. On rules.svc, meta.partial shows the writes the loop kept making.

Fix: make the repeating rule falsify its own condition, as the counter in a rule fires once and never again does, or drop agenda.Repeat(). Complete() ends the execution outright.

loading rules: /srv/rules/checks.grl: got 3 error(s) in grl the script

Means: a struct literal in a rule body. The rule language has no struct-literal syntax, so findings.Add(Finding{...}) produces a burst of parse errors. Only the count reaches you.

Confirm: delete the findings.Add(Finding{...}) line and load again. If the count drops to zero, that was it.

Fix: write the string form: findings.Add(ruleID, severity, category, message, fields), or the graded verbs.

in.Str returns the default for a fact that is present

Section titled “in.Str returns the default for a fact that is present”
rule Probe salience 10 {
when in.Has("doctype")
then
out.Set("direct", doctype);
out.Set("in_has", in.Has("doctype"));
out.Set("in_str", in.Str("doctype", ""));
out.Set("wrong_path", in.Str("doc_type", "none"));
}
facts
{ "doctype": "invoice", "pages": 3 }
out
{ "direct": "invoice", "in_has": true, "in_str": "invoice", "wrong_path": "none" }

Means: you are reading a fact by a path that does not resolve. The typed readers return their default rather than failing, so an unexpected default usually means a wrong path, not a broken reader.

Confirm: check the path with in.Has(path). Nested values are dotted paths into the fact, in.Str("invoice.currency", "").

A value staged with in.MemSet is missing from the result

Section titled “A value staged with in.MemSet is missing from the result”

Means: only out is returned. in.MemSet and the other in.Mem* calls are deprecated and never reach the result.

Fix: stage with out.Set and read back with the out readers. A later rule’s when and a later statement in the same then both see the write.

rule SetCount salience 20 {
when in.Has("x")
then out.Set("k", 5);
}
rule AppendNote salience 10 {
when in.Has("x")
then out.Append("k", "x");
}
facts
{ "x": 1 }
out
{ "k": "5, x" }

Means: out.Append stringifies whatever the key holds and joins with ", ". The separator is fixed; there is no out.SetSeparator.

Fix: out.Append is deprecated. Give each entry its own key, out.Set("notes.first", "x"), which writes a nested object, and keep numbers in keys of their own.

Error: 'rules' is not installed.
Install with: kvm install kis-rules

Means: kis rules and kis bbox are proxy commands; the kis binary carries only the stubs, and the implementation is a separately installed plugin executable.

Confirm: the lookup order is the directory holding the kis binary, then ~/.kisai/bin, then every entry of $KIS_PLUGIN_PATH, then $PATH. First executable match wins.

Terminal window
echo $KIS_PLUGIN_PATH
ls ~/.kisai/bin

Fix: install the plugin, or point $KIS_PLUGIN_PATH at it. See the CLI reference.

no inputs resolved (no positional argument matched, and no file matched pattern ...)

Section titled “no inputs resolved (no positional argument matched, and no file matched pattern ...)”

Means: kis rules found nothing to process. This is a hard error, not a no-op. Input discovery globs -p (default *.json) in -i, not recursively; -R/--recursive affects rule-file discovery only. Under --require-bbox, every candidate .json lacked its .bbox.

Confirm: read the warning: skipped N input(s) with no .bbox sidecar: ... line printed just above it, which names what was dropped. Files matching *-hist-audit.json are always skipped.

Terminal window
ls pages/*.json pages/*.bbox

Fix: produce the missing sidecars with kis bbox create, or correct -i, -p or the path.

recovered : runtime error: invalid memory address or nil pointer dereference

Section titled “recovered : runtime error: invalid memory address or nil pointer dereference”
executing rules: executing ruleset rules: error while executing rule B. got rule engine execute panic on rule B ! recovered : runtime error: invalid memory address or nil pointer dereference

Means: a rule called a document method on a document name that is not loaded. There is no “no such document” message; bbox.Get returns nothing and the method call on it panics. In a when the wording is error while evaluating rule B ! recovered : ….

Confirm: compare the name in the rule against the injected name:

CommandDocument nameOverride
kis rules -b <file>bboxwrite -b <name>=<file>
kis rules --with-bboxpage--bbox-name
kis rules --with-bbox --mode docdoc--bbox-name

Fix: align the two, and guard with bbox.Has("<name>"). bbox.Doc() with no argument resolves the only loaded document, or the one named page.

Means: the run carried no document. The bounding-box plugin is registered only when something could load a document: -b, --with-bbox, --with-image, --image, --schema, --schemas, or --validate; and a --with-bbox input with no .bbox beside it runs without one. Either way the bbox name is never bound and the first rule that touches it aborts the run.

Fix: add --with-bbox (sidecar beside each input) or -b [name=]<file>, and check the sidecar exists. --require-bbox drops an input that has none instead of running it.

*-hist-audit.json files keep appearing, and counts keep growing

Section titled “*-hist-audit.json files keep appearing, and counts keep growing”

Means: --history, --audit or --history-audit <dir> writes a sidecar for every processed input in which a rule fired. The writer reads the existing file and appends, so repeated runs accumulate duplicates.

Confirm: run the same command twice and count the entries.

Fix: redirect them out of the data directory with --history-audit <dir>, and delete stale ones between runs. *-hist-audit.json is always excluded from input matching, so a sidecar is never re-processed as an input.

A numeric comparison against a --vars value never matches

Section titled “A numeric comparison against a --vars value never matches”

Means: -v/--vars values are always injected as strings. -v threshold=10 gives a rule the string "10". --vars also splits its own value on commas, so a value containing one fails with invalid var format.

Fix: put numeric or comma-carrying facts in a --facts JSON file instead, where the JSON type is preserved.

A batch run exited 1 but some files were written

Section titled “A batch run exited 1 but some files were written”

Means: batch runs are best-effort. Every input is attempted, each failure prints ERROR: <path>: <err>, and the command exits 1 after reporting Processed N/M. With -o or --update the successful results are already written.

Confirm: the ERROR: lines go to stderr and the run finishes with Processed N/M input(s) successfully on stdout. Redirect both streams.

Every error answers with a status and a code, in the body ({"error":{"code","message","meta"}}) and in the X-Kis-Error-Code header. Match on the code, not the message. The full table is on the error reference.

Means: the service’s configuration names the calling tenant, but no load of its rules/ folder has succeeded yet: the first load is still running, or it failed. The response carries Retry-After: 5.

Confirm: GET /rulesets answers "loaded": false, and, when the load failed, says why under error, naming the file. One tenant’s broken folder does not take the service out of rotation for the others.

Fix: retry after the interval. If error names a file, fix it; the next change under rules/ loads again.

Means: the request names no tenant (the request names no tenant), or names one the service’s configuration does not (tenant <key> is not served by rules).

Fix: send the tenancy headers, and add the tenant to the service’s configuration; see configuration.

the product has no scope invoice: no static scope of rules.yaml and no folder rules/invoice/

Means: the calling tenant’s product has no static scope of that name in rules/rules.yaml and no folder of that name under rules/. POST /rule, the earlier call, words it ruleset invoice not found: the product has no rules/invoice/ folder of rules.

Confirm: GET /rulesets lists the folders and static scopes the tenant serves.

Fix: correct the scope, or add the folder.

Means: the facts were refused before any rule ran. The message says which case:

MessageCause
out: the engine binds these names itself (act, address, agenda, …); send the facts under other namesA fact named like a helper or a collector. Bound as a fact it would replace the helper, or lose its value to the collector
the facts do not meet scope <name>'s contract: …A static scope declares facts and the request does not meet them
<names>: scope <name> reads these itself as knowledge; send the facts under other namesThe static scope fills those names from its own sources
at most 20 factsToo many named facts entries

Fix: rename the facts, or send what the scope’s contract asks for; see static scopes.

the body is not an execute request: json: unknown field "audit"

Means: the body did not decode as the call’s request. POST /rules/execute refuses a key it does not read, so a misplaced option is an error rather than silently ignored: audit and history belong under options.

Fix: send {"scope", "input", "facts", "options": {"audit", "history"}}. A body over rules.body.maxbytes (1 MiB by default) answers 413 rules-body-too-large instead.

Means: the tenant’s access rule for the action refused the caller, or could not be evaluated. By default executing a scope asks can("rules.scopes", "read") of the caller; a product refines the rules in rules/extend/access.yaml.

Fix: grant the privilege, or check the product’s access rules; see access.

Means: a rule failed while the rules ran: a missing fact, dot access on a map, an argument of the wrong type, the cycle cap. The message is the engine’s, the same text kis rules prints, and executions that abort walks each one. On POST /rules/execute, meta.partial holds what the rules had written before the failure.

Confirm: run the same input through kis rules against a local copy of the folder: kis rules -r <product> --scope <name> loads it as rules.svc does and holds each input to the scope’s facts contract.

Means: an execution outlived its bound: rules.exec.timeout (10s by default), or a static scope’s limits.timeout, which rules.exec.maxtimeout (30s by default) caps. kis rules bounds an execution at five minutes.

Fix: find the rule that runs long, usually one that repeats with agenda.Repeat(), or raise the scope’s limits.timeout within the ceiling; see configuration.

Means: the request did not ask for one. The trail is opt-in.

Fix: set "options": {"audit": true} on POST /rules/execute; add "history": true for every write with the rule that made it. POST /rule, the earlier call, answers out alone.

Means: the path is not one of the service’s routes. Unknown paths answer 404 with no body at all, not the JSON envelope. The decision call is POST /rules/execute.