Batch processing
Evaluate thousands of records against one bundle, reproducibly - with a native library, or through one engine process from any language.
A batch job is an API without a user interface: it evaluates each record on the server channel
before it changes anything. Because evaluation is a pure function of the bundle and the request,
a batch run is reproducible: the same bundle, records and ctx.now give the same decisions.
Before you start
- The compiled bundle of the ruleset (ship a rule change).
- The records as JSON Lines, one entity per line. The examples use this file:
{"id": "t-1", "type": "domestic", "amount": 120.5, "currency": "USD", "memo": "rent", "beneficiary": {"name": "Jo Lee", "country": "US"}}
{"id": "t-2", "type": "international", "amount": 500, "currency": "USD", "memo": "gift", "beneficiary": {"name": "X", "country": "KP", "swiftCode": "ABCDKPPY"}}
{"id": "t-3", "type": "domestic", "amount": -5, "currency": "USD", "memo": "oops", "beneficiary": {"name": "Sam", "country": "US"}}Steps
Fix the inputs of the run
Decide once per run: the bundle (record its checksum), the actor the job acts as, and one
ctx.now. Never read the clock per record: a rule that reads ctx.now then gives the same answer
when the run is repeated.
Load the bundle once and evaluate each record
"""Evaluate every record of a JSON Lines file against one bundle; write one decision per line."""
import json
import sys
from pathlib import Path
from rule_cascade import RuleSet
bundle_path, records_path = sys.argv[1], sys.argv[2]
rules = RuleSet.from_bundle(json.loads(Path(bundle_path).read_text(encoding="utf-8")))
now = "2026-10-03T00:00:00Z" # one clock for the whole run: results are reproducible
denied = 0
with open(records_path, encoding="utf-8") as records:
for number, line in enumerate(records, start=1):
try:
record = json.loads(line)
result = rules.evaluate({
"entity": "Transfer",
"operation": "create",
"data": record,
"actor": {"id": "batch-import", "roles": ["system"]},
"ctx": {"now": now},
})
except ValueError as malformed: # not JSON, or not a valid request
print(json.dumps({"line": number, "error": str(malformed)}))
denied += 1
continue
if result["decision"] == "deny":
denied += 1
print(json.dumps({
"line": number,
"id": record.get("id"),
"decision": result["decision"],
"findings": [f["code"] for f in result["findings"]],
"checksum": result["checksum"],
}))
print(f"{denied} of {number} records denied", file=sys.stderr)
sys.exit(1 if denied else 0)PYTHONPATH=packages/python/src python batch.py \
conformance/bundles/acme.payments.transfer.bundle.json transfers.jsonl{"line": 1, "id": "t-1", "decision": "allow", "findings": [], "checksum": "sha256:c192dd53b5b1d307d52ccbc27fc1674114e8714d53b699b24088a648ae242c7e"}
{"line": 2, "id": "t-2", "decision": "deny", "findings": ["ORG-TRF-001"], "checksum": "sha256:c192dd53b5b1d307d52ccbc27fc1674114e8714d53b699b24088a648ae242c7e"}
{"line": 3, "id": "t-3", "decision": "deny", "findings": ["PAY-TRF-001"], "checksum": "sha256:c192dd53b5b1d307d52ccbc27fc1674114e8714d53b699b24088a648ae242c7e"}
2 of 3 records deniedAct on the decisions
Persist only the allowed records, apply their computed values (value effects), and write the
returned commands to an outbox in the same transaction, exactly as an API does
(backend API). Report denied records with their finding codes.
Scale out
Split the input and run several workers. Each worker loads the bundle once; with the engine, run a pool of engine processes with one request in flight per process. Workers need no coordination: evaluation shares nothing.
Record the run
Log the bundle checksum, the ctx.now used, the counts of allow and deny, and the finding
codes. That is enough to repeat the run exactly.
Done when the job evaluates every record before writing it, every output line names the bundle checksum, a malformed record is reported instead of crashing the run, and running the job twice on the same inputs gives byte-identical output.
Run rules from any other language
Ruby, PHP, Rust, C#, PowerShell, shell or anything else - through the rule server over HTTP, or the command or WebAssembly module over the engine protocol.
AI agent tools
Give an LLM agent list_rules, evaluate_rules and explain_rule so it checks before it acts - without letting it decide, name its own roles, or accept its own risks.