Rule Cascade
Playbooks

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:
transfers.jsonl
{"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

batch.py
"""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 denied

Act 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.

Source: site/content/docs/playbooks/batch-processing.mdx

On this page