Errors and limits
Errors in this block come from five places: compiling a rule set, executing it, the rules.svc
HTTP surface, the kis CLI, and document processing. This page lists all of them
with the exact text you see. For symptom-first diagnosis, start at
troubleshooting.
Two facts govern everything below:
- One bad rule discards the whole execution. Abort-on-failed-evaluation is on unconditionally and every non-retracted rule is re-evaluated every cycle, so a rule that references a fact you did not supply kills output that other rules already produced. There is no partial result.
- Every engine failure is an HTTP 400. A malformed request, an unknown rule set, a rule that crashed, and a service that has not loaded any rules yet are indistinguishable by status code.
Response shapes on rules.svc
Section titled “Response shapes on rules.svc”| Origin | Content type | Body |
|---|---|---|
| Handler and engine failures | application/json | {"error":{"code":"…","message":"…","context":…,"meta":…}} |
| Auth, tenant and config middleware | text/plain | Bare message — Unauthorized, invalid tenant |
| Timeout middleware | text/plain | context-middleware-timed-out |
| Panic recovery | text/plain | 500 - Internal error |
| Unmatched path | none | Empty body with the status only |
code is empty on every error except one. context is an internal field that serialises as {} or
null. Trace and span IDs are never emitted, so an error response carries no correlation handle —
the only one is the server-side log line.
HTTP status codes
Section titled “HTTP status codes”| Status | Raised by | Body |
|---|---|---|
| 200 | Successful execution | The rule set’s out map, unenveloped; {} when no rule wrote to out |
| 400 | Request parse, missing name, engine not initialised, unknown rule set, any rule failure, cycle cap | JSON envelope |
| 400 | Tenant middleware | invalid tenant |
| 400 | Config client unresolved | config client not initialised |
| 401 | Auth middleware | Unauthorized |
| 404 | Unregistered path | Empty |
| 405 | Wrong method on /rule | Method Not Allowed |
| 408 | X-Api-Timeout deadline exceeded | context-middleware-timed-out |
| 500 | Panic in the handler | 500 - Internal error |
| 500 | Result map could not be serialised | JSON envelope |
| 503 | Request context cancelled — client disconnect or shutdown, and only when X-Api-Timeout was sent | Empty |
Service errors
Section titled “Service errors”| Message | Code | Status | Cause |
|---|---|---|---|
json request parse failed due to {{err}} | rules-json-request-parse-failed | 400 | Body is not decodable JSON, is not a JSON object, or is empty |
name of rule to execute not given | (empty) | 400 | name is missing, null, or an empty string. A non-string name is coerced to its string form and fails as an unknown rule set instead |
rule engine not initialized | (empty) | 400 | No rule set has ever loaded in this process |
ruleset <name> not found: specified knowledge base name and version not exist | (empty) | 400 | Typo in name, or the rule document never loaded |
ruleset <name> not found: expression <grl text> is not on the clone table - E(EA(A(C(…)))) | (empty) | 400 | The rule set is poisoned — an edited rule body was re-delivered under an existing rule name |
executing ruleset <name>: <engine error> | (empty) | 400 | Any compile-time or run-time rule failure; see the tables below |
The parse-failure message ships an unrendered {{err}} placeholder. The decoder’s actual complaint
is in meta.error, never in message:
{"error":{"code":"rules-json-request-parse-failed","context":{},"message":"json request parse failed due to {{err}}","meta":{"error":"invalid character 'b' looking for beginning of object key string"}}}Every other failure looks like this — no code, no meta:
{"error":{"code":"","context":null,"message":"executing ruleset invoice: error while executing rule R1. got non existent key log","meta":null}}Rule compilation errors
Section titled “Rule compilation errors”Rules compile at load time, not at execution time. A syntax error surfaces when the rule set is
added — from the CLI on startup, from rules.svc during a metadata refresh where it reaches the log
only.
| Message | Cause |
|---|---|
got N error(s) in grl the script | Syntax error, an unsupported construct, or two rules sharing a name in one rule set. Prefixed with <path>: when the rules were loaded from disk, bare when they arrived as a string |
stat <path>: <err> | The rule path does not exist or is not readable |
reading <path>: <err> | A rule file could not be read |
The count is all you get. The parser collects one message per error — grl error on <line>:<col> <detail> for a syntax fault, rule entry <name> already exist for a duplicate name — but nothing
unwraps that list, so a missing semicolon and a duplicate rule name produce the same sentence. From
the CLI you at least get the file path; in the service you get adding rule failed <ruleset> <err>
in the log and nothing on the wire.
The constructs that most often produce this error are all things the language does not have:
| Written | Why it fails |
|---|---|
findings.Add(Finding{...}) | No struct literals in the grammar. There is no way to record a finding from a rule |
if cond { … } else { … }; | No control flow in then. Use two rules at different salience |
Two conditions on separate lines in when | when takes exactly one expression; join with && or || |
A then statement without a trailing ; | The semicolon is mandatory, including on the last statement |
| The same rule name in two files | Every file loaded into one rule set shares a flat name space |
Rule execution errors
Section titled “Rule execution errors”All of these abort the execution and return no result. Over HTTP they arrive wrapped as
executing ruleset <name>: … with status 400; on the CLI they fail that input file.
| Message | Cause |
|---|---|
error while executing rule <R>. got non existent key <fact> | A then referenced a name that was never bound |
got left hand expression error. got non existent key <fact> | A when referenced a name that was never bound |
evaluating expression in rule '<R>' the when raised an error. got <detail> | Condition evaluation failed; see the details below |
this node identified as "<fact>" is not referencing to an object | Dot access on a map fact. Use fact["Key"] |
<fact>[string-><key>] is not an array nor map | Two-level indexing into decoded JSON — the intermediate value is interface-typed |
<var> is not an array nor map | Indexed a local variable holding a map or slice. Index the call directly instead |
this node identified as "<obj>" have no function named <Fn> | Misspelled or non-existent method on a bound object |
this node identified as "DEFUNC" have no function named <Fn> | An unqualified call matched no built-in — a helper needs its namespace, as in strings.Contains |
this node identified as "<obj>" calling function <Fn> which … returns multiple values … | The method returns (value, error). Affects out.JSON(), audit.JSON() and the document loaders |
reflect: Call using int64 as type int | An integer literal was passed to a plain Go int parameter |
reflect: Call using int64 as type float64 | A float parameter needs a decimal point — math.Min(10.0, 20.0) |
reflect: Call using interface {} as type string | An any-typed value reached a string parameter |
reflect: call of reflect.Value.Type on zero Value | A comparison operand was nil — guard with out.Has(k) && out.Get(k) == v |
reflect: Call using zero Value argument | A local variable holding a map or slice was passed to a function |
the GruleEngine successfully selected rule candidate for execution after 5000 cycles, … | The cycle cap tripped; a rule keeps re-qualifying |
context canceled | The caller’s context was cancelled or its deadline expired |
The multiple-values message is emitted across three lines with blank lines between them. Match on
returns multiple values rather than on the whole string:
this node identified as "out" calling function JSON which
returns multiple values, multiple value
returns are not supportedRuntime panics are recovered and re-reported with the rule name — a panic in then as
rule engine execute panic on rule <R> ! recovered : <panic message>, a panic in when as
error while evaluating rule <R> ! recovered : <panic message>. Every reflect: row above arrives
inside one of those two wrappers.
CLI errors
Section titled “CLI errors”Everything below exits 1 and writes to stderr unless stated otherwise. A run terminated by a signal exits with 128 plus the signal number.
| Message | Cause |
|---|---|
Error: 'rules' is not installed. | kis rules, kis ocr, kis bbox and kis hocr2bbox proxy to a separately installed plugin binary. A second line names the package to install. The search order is the directory holding kis, ~/.kisai/bin, $KIS_PLUGIN_PATH, then $PATH |
required flag(s) "rules" not set | -r/--rules omitted. Required by kis rules, kis ocr page and kis ocr doc |
loading rules: loading rules from <dir>: <file>: got N error(s) in grl the script | A rule file failed to compile |
no files matched pattern <glob> | The -i directory and -p pattern selected nothing |
no prefixes resolved (no positional args matched and no files matched pattern <p> in <dir>) | kis ocr page/doc found no .json file with a matching .bbox sidecar |
invalid --skip-pattern "<p>": <err> | Malformed glob, validated before the run starts |
ERROR: <path>: <err> | One input failed. Printed inline as it happens — on stdout for kis rules, on stderr for kis ocr — and the batch continues |
N file(s) failed to process / N prefix(es) failed | The final error from a batch that had at least one failure |
Batch runs never stop early. Every input is attempted, each failure is printed as it happens, and
the exit code is 1 if anything failed — so with --update the files that succeeded are already
rewritten in place even though the command reports failure. See the CLI.
Document processing errors
Section titled “Document processing errors”Document work runs on both surfaces. The CLI reads documents from disk — kis ocr page, kis ocr doc, kis rules --bbox / --with-bbox — and POST /rule carries them in the request body under
bbox, keyed by the name a rule passes to bbox.Get, alongside the facts:
{ "name": "invoice-checks", "total": 1200, "bbox": {"page": {"pages": [{"words": []}]}}}bbox is reserved in the request body — its value is document input, not a fact — and the document
bindings a rule sees are the same ones the CLI provides. Reading, parsing or analysing a document
fails before any rule fires, on either surface, and the whole execution is discarded.
| Message | Cause |
|---|---|
plugin bbox prepare: loading document <name>: <err> | A supplied document could not be read, parsed or analysed. Over HTTP this arrives as a 400 error envelope |
duplicate schema name "<n>" (from <a> and <b>) | Two --schema/--schemas files share a basename — the rule-callable name is the filename without its extension |
unsupported top-level type "<t>" (expected object or array) | A schema’s top level is neither an object nor an array |
array-typed schema must have an "items" object | An array schema with no items |
at least one input file is required (use positional args or -i flag) | A kis bbox subcommand ran with no inputs |
no input files provided | Same, from the shared input resolver |
invalid snippet definition "<s>": expected "name|start phrase|end phrase" | A --snip value that did not split into three |-separated segments. An empty segment gets its own message naming it |
table has no headers | kis bbox table -H found a table with no header row |
missing headers: <a> (found: <b>) | Header validation failed against the detected table |
The rows naming a flag or an input file are CLI-side; document load and analysis failures reach both surfaces. Over HTTP the schemas travel as a name-keyed object on the request rather than as files, so the basename collision cannot arise there — see service operations.
Execution limits
Section titled “Execution limits”| Limit | Value | Configurable | On trip |
|---|---|---|---|
| Cycle cap | 5000 firing cycles | No — fixed at 5000 on both surfaces | Execution aborts, all output discarded, HTTP 400 |
| Request body size | None | — | — |
| Request rate | None | — | — |
| Server-side request deadline | None unless the caller sends X-Api-Timeout | Per request, by header | 408, and the request context is cancelled with it — the execution stops at the next rule boundary |
| HTTP read timeout | 15s | timeouts.read | Connection read fails |
| HTTP write timeout | 15s | timeouts.write | The response is aborted; the execution keeps running |
| HTTP read-header timeout | 5s | timeouts.readheader | Connection read fails |
| Document pages, words, analysis time | None | — | — |
Configuration keys are covered in service configuration.
The cycle cap is a correctness backstop, not a timeout. A cycle counts only when a rule actually fires, so a rule set that blocks inside a helper never trips it, while one that fires cheap rules 5000 times does. Cancellation is checked between rules and between cycles, so a rule blocked inside a single helper call runs to completion whatever the caller’s deadline says.
Failures that produce no error
Section titled “Failures that produce no error”These are the expensive ones, because nothing tells you.
| What you wrote | What happens |
|---|---|
in.GetStr / GetInt / GetFloat / GetBool / GetMap | Always return the zero value, even for facts that are present. in.Has() works; reference facts directly instead |
| Rules with equal salience | Firing order is non-deterministic across runs. Give every rule in an ordered set a distinct salience |
when out.GetStr("tier") == "gold" after another rule set it | The condition is memoised and never re-evaluated. The writing rule must call Changed("out") |
A request field named out, in or findings | The collector is bound last and wins. Your value is dropped without a word. Over HTTP the whole request body becomes the fact map, so this is easy to hit |
A request field named bbox or audit that meant something else | Both are reserved control keys, read before the fact map is built. bbox must be documents keyed by name and audit a boolean; neither is ever visible as a fact |
in.Transform(key, "typo") | Unknown transform names are a no-op — no error, no log, nothing written to memory |
A malformed regex in any strings.* helper | Degrades quietly: false, "", nil, or the input unchanged |
audit.Log(...) in a rule served over HTTP, on a request that did not ask for the trail | The trail comes back only when the body sets "audit": true. Without it the response is the out map alone and the entries go unread. The output history, rules-matched count and duration are computed and never serialised on either surface |
An out key called audit on a request that set "audit": true | The trail is serialised last and overwrites it. Rename the output key |
A kis ocr input whose .bbox sidecar is missing | Silently skipped, both when the input came from a directory glob and when it was named as a .json/.bbox path. The only signal is no prefixes resolved when every candidate was dropped |
| Two tenants shipping a rule set with the same name | One flat name space per process. The second delivery is refused and logs adding rule failed. Identical bodies mean both tenants run the first tenant’s rules; different bodies poison the rule set for both |
A request field named util, num, strings, time, math, map or array collides the other
way: your fact is bound after the helper namespace and replaces it, so the first rule that calls
into that namespace aborts the execution with this node identified as "<name>" call function <Fn> is not supported for map.