Rules engine API
This is the type-by-type reference for the rule engine that the kis CLI
and rules.svc both run. Every signature here is the one in
the shipped source. For what a rule may call, see
Built-in functions; for how a rule set is written, see
The rule language.
Both surfaces drive the same engine with the same defaults, so the limits and behaviours below apply
identically to a CLI run and to a POST /rule request.
Where each type shows up
Section titled “Where each type shows up”| Type | Reachable from a rule as | Shows up in |
|---|---|---|
Output | out | The result; rules.svc returns Output.All() as the response body |
Findings | findings | Result.Findings — see the caution under Findings |
Audit | audit | Result.Audit |
Input / Memory | in, in.Mem(name) | Scratch state for the duration of one execution |
Result | — | Returned by Execute |
Config, Option, Plugin, Engine | — | Set up before any rule runs |
RuleTracker, DataContextAccessor, transform registry | — | See their sections |
Fixed limits
Section titled “Fixed limits”Neither surface exposes a knob for any of these. The values are the same on both.
| Limit | Value | Effect |
|---|---|---|
| Cycle cap | 5000 | Execution aborts with an error past this many firing cycles; no partial Result |
| Rule set version | 1.0.0 | The only version Execute looks up |
| Rules fired per cycle | 1 | The runnable rule with the highest salience |
| Abort on failed evaluation | always on | One rule’s evaluation error discards the whole run |
Output append separator | ", " | Changeable per execution with out.SetSeparator |
Engine
Section titled “Engine”The interface every backend implements. The grule backend is the implementation both surfaces
construct.
type Engine interface { AddRules(ctx context.Context, rulesetName, version string, content string) error Load(ctx context.Context, rulesetName, version, path string, recursive bool) error RegisterPlugin(ctx context.Context, p Plugin) error Execute(ctx context.Context, facts map[string]any, ruleset string, pluginData map[string]any) (*Result, error) Rulesets() []string Close() error}| Method | Returns | Behaviour |
|---|---|---|
AddRules | error | Compiles GRL text into (rulesetName, version). An empty version becomes the default. Parse and compile errors surface here, never from Execute |
Load | error | A file path loads that one file whatever its extension. A directory path loads every .grl file in it — recursive also walks subdirectories. Every file lands in the same rule set |
RegisterPlugin | error | Calls the plugin’s Init immediately and merges its facts into an engine-wide map bound into every later execution. Fails with plugin <name> init: <err> |
Execute | (*Result, error) | Runs ruleset against facts. Returns (nil, err) on any failure — there is no partial result |
Rulesets | []string | Distinct rule set names, in Go map order. Includes rule sets stored under an unreachable version |
Close | error | No-op returning nil. There is no unload: rule sets only accumulate |
Rule names must be unique inside a rule set. Loading a directory pools every file into one namespace,
so two files that both define rule ValidateTotal fail the load with
got N error(s) in grl the script.
Loading is not concurrency safe. Execute is: each call clones a fresh knowledge base and allocates
its own data context, Output, Findings and Audit.
Construction
Section titled “Construction”The concrete backend is a struct with unexported fields that satisfies Engine:
type Engine struct { /* unexported */ }It implements every method in the interface above — AddRules, Load, RegisterPlugin, Execute,
Rulesets and Close — with the behaviour described in that table. The backend is selected by the
literal name "grule". Both the CLI and rules.svc build it the same way, with the shipped
defaults and the document and utility namespaces registered, so every value below is the effective
one on every run.
Config
Section titled “Config”type Config struct { MaxCycles int DefaultVersion string}| Field | Type | Value | Effect |
|---|---|---|---|
MaxCycles | int | 5000 | Cycles allowed before the execution aborts. A cycle counts only when at least one rule is runnable |
DefaultVersion | string | "1.0.0" | Substituted for an empty version on load, and the only version Execute resolves |
Both values are fixed. There is no CLI flag, bootstrap key or request field on either surface that
changes the cycle cap or the rule-set version — a runaway rule set is fixed in the rules, normally by
adding the missing Retract.
Plugin
Section titled “Plugin”A plugin publishes named facts into executions. Three methods, two of them lifecycle.
Both surfaces register the same set — the document namespace and the utility namespaces — before
they serve anything, so there is nothing to enable and no configuration for it. What varies is
per run: the document namespace binds bbox only for a run that carried document input, which is
kis rules --bbox / kis ocr on the CLI and the reserved bbox object on the POST /rule body.
See Documents in rules.
type Plugin interface { Name() string Init(ctx context.Context) (map[string]any, error) Prepare(ctx context.Context, data any) (map[string]any, error)}| Method | Called | Facts live for | On error |
|---|---|---|---|
Name | Whenever the engine needs the plugin’s key | — | — |
Init | Once, during RegisterPlugin | Every execution from then on | plugin <name> init: <err>, registration fails |
Prepare | Once per Execute, and only when pluginData[Name()] is non-nil | That execution only | plugin <name> prepare: <err>, the execution aborts before any rule runs |
Prepare is skipped, not called with nil, when the caller supplies no data under the plugin’s name.
A fact that a plugin publishes only from Prepare is therefore absent in that run, and a rule naming
it fails with got non existent key <name> — which aborts everything.
Result
Section titled “Result”type Result struct { Ruleset string RulesMatched int Duration time.Duration Output *Output Findings *Findings Audit *Audit}| Field | Type | What it holds |
|---|---|---|
Ruleset | string | The rule set name passed to Execute, echoed back |
RulesMatched | int | Count of firings, not distinct rules. A rule that fires in three cycles counts three |
Duration | time.Duration | Wall clock across the whole Execute, including fact binding and every plugin’s Prepare |
Output | *Output | Everything the rules wrote to out |
Findings | *Findings | Severity-graded validation list |
Audit | *Audit | One entry per firing plus anything rules logged |
Result is non-nil only on success. Any evaluation error, action error, cycle-cap trip or context
cancellation returns (nil, error) and discards every value already collected.
rules.svc returns Output.All() as the response body and drops RulesMatched, Duration,
Findings and Audit.
Output
Section titled “Output”The key/value result collector, bound as out.
type Output struct { /* unexported */ }
func NewOutput() *Output| Method | Behaviour |
|---|---|
Set(key string, val any) | Stores the value and appends an OutputChange stamped with the firing rule |
Append(key string, val string) | Concatenates onto an existing string value using the separator; otherwise behaves as Set |
SetSeparator(sep string) | Separator used by later Append calls. Default ", " |
SetCurrentRule(rule string) | Sets the rule name stamped onto subsequent changes. The engine calls this before each firing |
Get(key string) any | Raw value, nil if absent |
Has(key string) bool | Key presence |
GetStr(key string) string | "" if absent or not a string |
GetInt(key string) int64 | 0 if absent or not int, int64 or float64 |
GetFloat(key string) float64 | 0 if absent or not float64, int or int64 |
GetBool(key string) bool | false if absent or not a bool |
GetMap(key string) map[string]any | nil if absent or not a map[string]any |
All() map[string]any | The live map — this is the result payload |
History() []OutputChange | Every change, in order |
HistoryFor(key string) []OutputChange | Changes for one key. The built-in provenance mechanism |
JSON() ([]byte, error) | JSON of the data map |
UnwrapColumn(input map[string]interface{}, section, columnName, renameName string, maxN int64) | Flattens one column of input[section]["rows"] into maxN keys named "<renameName> <n>", 1-based |
UnwrapColumnWithTemplate(input map[string]interface{}, section, columnName, renameName string, maxN int64) | Same, but renameName is a format string taking the index |
Both unwrap methods emit {"confidence": "", "value": ..., "label": ...} per key, pad missing rows
with empty values, and return silently without setting anything if input[section], its rows entry
or the row shape does not match.
type OutputChange struct { Key string Value any RuleID string Timestamp time.Time}OutputChange field | Type | What it carries |
|---|---|---|
Key | string | The key that was written |
Value | any | The value as written, not the value now |
RuleID | string | The rule current when the write happened; "" for a write outside a firing |
Timestamp | time.Time | time.Now() at the write |
Every Set appends one entry, and Append records the concatenated result, so History() is a
complete write log rather than a set of distinct keys.
Input and Memory
Section titled “Input and Memory”Input is bound as in. It reads facts and owns the per-execution scratch memory.
type Input struct { /* unexported */ }
type DataContextAccessor interface { Get(key string) (any, error)}
func NewInput(ctx DataContextAccessor) *InputDataContextAccessor is the single-method seam Input reads facts through; the backend supplies the
implementation.
Fact readers
Section titled “Fact readers”| Method | Returns |
|---|---|
Has(key string) bool | true when the key exists and its value is non-nil |
Get(key string) any | The raw value from the data context |
GetStr(key string) string | "" when absent or not a string |
GetInt(key string) int64 | 0 when absent or not numeric |
GetFloat(key string) float64 | 0 when absent or not numeric |
GetBool(key string) bool | false when absent or not a bool |
GetMap(key string) map[string]any | nil when absent or not a map[string]any |
Scratch memory
Section titled “Scratch memory”Input holds named memory slots. The default slot exists from the start; Mem(name) creates any
other slot on first use.
| Method | Behaviour |
|---|---|
Mem(key string) *Memory | The named slot, created lazily |
MemSet(key string, v any) | Sets in the default slot |
MemGet(key string) any | Reads the default slot |
MemGetStr(key string) string | Reads the default slot as a concrete string, "" on a type mismatch |
MemGetRows(key string) []map[string]any | Reads the default slot as rows, nil on a type mismatch |
MemDelete(key string) | Deletes from the default slot |
MemClear() | Empties the default slot |
GetMemAsMap() map[string]any | An independent deep copy of the default slot |
NewMapWithValues(kv ...any) map[string]any | Builds a fresh map from alternating key/value pairs |
Transform(key string, fn string) | Applies a transform and writes the result back to the default slot |
GetMemAsMap copies nested map[string]any, []map[string]any, []any and []string recursively,
unwraps internal ordered-map nodes to plain maps, and preserves scalar types exactly — notably
int64 versus float64, which the rule dispatcher treats as distinct. A value emitted through
out.Set(k, in.GetMemAsMap()) is therefore immune to later memory writes.
NewMapWithValues returns a brand-new map on every call, so it shares no backing with memory.
Non-string keys are stringified; an odd trailing argument is dropped.
Transform prefers a value already in the default slot and falls back to the data context, so chain
by calling it again on the same key.
Memory is the slot type returned by Mem. It wraps an ordered map and is created only through
Mem — there is no exported constructor:
type Memory struct { /* unexported */ }| Method | Behaviour |
|---|---|
Get(key string) any | Value, nil if absent |
Set(key string, v any) | Stores a value |
Delete(key string) | Removes a key |
Clear() | Replaces the slot with an empty one |
ToMap() map[string]any | The slot as a map. Nested containers are shared, not copied — use GetMemAsMap for an independent snapshot |
GetRows(key string) []map[string]any | Value as rows, nil on a type mismatch |
Findings
Section titled “Findings”The severity-graded validation list, bound as findings.
type Findings struct { /* unexported */ }
type Finding struct { RuleID string Severity Severity Category string Message string Fields map[string]any}
func NewFindings() *FindingsFinding field | Type | What it carries |
|---|---|---|
RuleID | string | The rule that raised it. Never populated by the engine — the caller sets it |
Severity | Severity | Grades the finding; drives Passed() and HasSeverity() |
Category | string | Free-form grouping label |
Message | string | The human-readable text |
Fields | map[string]any | Structured detail attached to the finding |
| Method | Behaviour |
|---|---|
Add(finding Finding) | Appends a finding |
All() []Finding | Every finding, in order |
Count() int | Number of findings |
Passed() bool | false only when at least one ERROR or CRITICAL finding exists |
HasSeverity(s Severity) bool | Whether any finding carries that severity |
Severity is a named int with four constants declared in iota order:
type Severity int
const ( SeverityInfo Severity = iota SeverityWarning SeverityError SeverityCritical)
func (s Severity) String() string| Constant | Value | String() |
|---|---|---|
SeverityInfo | 0 | INFO |
SeverityWarning | 1 | WARNING |
SeverityError | 2 | ERROR |
SeverityCritical | 3 | CRITICAL |
Any value outside that range stringifies as UNKNOWN.
The append-only decision trail, bound as audit.
type Audit struct { /* unexported */ }
type AuditEntry struct { RuleID string Message string Data map[string]any Timestamp time.Time}
func NewAudit() *AuditAuditEntry field | Type | What it carries |
|---|---|---|
RuleID | string | The firing rule’s name, or the first argument to audit.Log |
Message | string | executed for an engine-written entry, otherwise the logged text |
Data | map[string]any | Set only by LogWithData; nil for a plain Log |
Timestamp | time.Time | time.Now() at the moment the entry was appended |
| Method | Behaviour |
|---|---|
Log(ruleID, message string) | Appends an entry stamped time.Now() |
LogWithData(ruleID, message string, data map[string]any) | Same, carrying a data map |
All() []AuditEntry | Every entry, in order |
Count() int | Number of entries |
ForRule(ruleID string) []AuditEntry | Entries for one rule, nil when there are none |
JSON() ([]byte, error) | JSON of the entry slice. Not callable from a rule |
The engine writes one entry per firing with the message executed, so Audit.All() is a complete
firing trace even when no rule logs anything, and Count() is RulesMatched plus whatever rules
logged explicitly.
RuleTracker
Section titled “RuleTracker”type RuleTracker struct { /* unexported */ }
func NewRuleTracker() *RuleTracker
func (r *RuleTracker) Done(name string)func (r *RuleTracker) IsDone(name string) boolDone marks a name, IsDone reports whether it was marked.
Transform registry
Section titled “Transform registry”The registry behind in.Transform(key, fn). An fn string is either a registered name or a call of
the form Name("arg", "arg").
fn | Result |
|---|---|
trim | Leading and trailing whitespace removed |
upper | Uppercased |
lower | Lowercased |
normalize_spaces | Runs of whitespace collapsed to single spaces |
trim_currency | $ and , removed, then trimmed |
ExtractRegex("pattern", "defaultVal") | The whole match when the pattern has no capture groups, otherwise every non-empty group concatenated with no separator. defaultVal on no match, "" when the argument is omitted |
var Registry map[string]func(string) string
type ParserFunc func(args []any) (func(string) string, error)
func Register(name string, fn func(string) string)func RegisterParser(name string, fn ParserFunc)func Resolve(fn string) func(string) stringfunc ParseQuotedArg(s string) (string, string, error)| Symbol | Behaviour |
|---|---|
Registry | The name → function map holding the five zero-argument transforms above |
ParserFunc | Parses a call’s argument list and returns the transform to apply. The value being transformed is implicit, never an argument |
Register | Adds a zero-argument transform. Panics on an empty name or a name already registered |
RegisterParser | Adds a parameterised transform under a call name. Panics on an empty name |
Resolve | Returns the transform for an fn string, or nil when the name is unknown or the arguments are invalid |
ParseQuotedArg | Reads the first double-quoted string from s, honouring \n, \t and \<char> escapes. Returns the value, the remainder and an error |
Argument tokens parse as Go literals: quoted strings, true/false, int64, then float64.
Continue with
Section titled “Continue with”- Built-in functions — everything a rule may call
- The rule language — GRL syntax, facts and salience
- Rule patterns — working shapes for chaining and staging
- Documents — the document model the
bboxbinding exposes - The rules service — the same engine over HTTP
- Error reference — the load and execution error strings