Write Your First Rule

Build, run, and validate a realistic fraud rule end to end, then extend it with an OR branch and inspect every result field.

This walkthrough takes a single rule from YAML to an evaluated decision. You will write a minimal rule file, evaluate a two-record batch, read the result fields, validate the routing, and then extend the rule with an or branch.

Goal

Build a rule named high_amount_emulator that blocks any transaction over 2000 made from an emulator device, with severity HIGH and a score weight of 40.

Prerequisites

  • A built blazerules Python module on your PYTHONPATH. See Quickstart or Installation.
  • A text editor for the YAML file.

Step 1 — Write a minimal rule file

Create my_rules.yaml. It declares the rule format version, a few optional field hints, and one rule:

schema_version: "2.1"

fields:
  card_token: {type: entity_key, nullable: false}
  amount: {type: float32, nullable: false}
  device_type:
    type: categorical
    values: [ios, android, web, emulator]

ruleset:
  name: First Rule
  version: "1.0.0"
  rules:
    - id: high_amount_emulator
      action: block
      severity: HIGH
      weight: 40
      conditions:
        and:
          - field: amount
            op: gt
            value: 2000
          - field: device_type
            op: eq
            value: emulator

The fields: block is optional. It declares card_token as an entity key and pins device_type to a closed set of categorical values. Without these hints, BlazeRules infers referenced fields from the first batch.

📘

Rules can load before a schema exists

You do not need to define a schema up front. When you call load_rules(...) the rules are parsed, and the first evaluated batch samples the rule-referenced fields, infers their types, binds the schema, then compiles and activates the rules. The fields: hints above simply remove ambiguity for entity keys and categoricals.

Step 2 — Prepare a small batch

Create two newline-delimited JSON records — one that should match the rule, one that should not:

{"card_token":"card_1","amount":2500.0,"device_type":"emulator"}
{"card_token":"card_2","amount":50.0,"device_type":"ios"}

The first record (amount 2500 on an emulator) satisfies both conditions. The second (amount 50 on ios) satisfies neither.

Step 3 — Evaluate the batch

import blazerules

config = blazerules.EngineConfig()
config.output_detail = blazerules.OutputDetail.DECISIONS

engine = blazerules.RuleEngine(config)
engine.load_rules("my_rules.yaml")

payload = b"""
{"card_token":"card_1","amount":2500.0,"device_type":"emulator"}
{"card_token":"card_2","amount":50.0,"device_type":"ios"}
"""

result = engine.evaluate_ndjson(payload)

print("records:        ", result.n_records)
print("matched:        ", result.n_matched)
print("decisions:      ", result.decisions)
print("decision codes: ", result.decision_codes)
print("scores:         ", result.scores)
print("winning rules:  ", result.winning_rule_ids)
print("match counts:   ", result.match_counts)

Expected output

  • n_records is 2.
  • n_matched is 1 — only the first record matched a rule.
  • decisions[0] is the block verdict and decisions[1] is the default approve — no rule fired on the second record, so it falls through to the ruleset default.
  • winning_rule_ids[0] is high_amount_emulator; for record 1 there is no winning rule.
  • match_counts reports high_amount_emulator matched one record.

The first record's score includes the rule's weight (40). The second record's score remains 0.

Step 4 — Validate the routing

Rather than looping over every record in Python, ask the result for grouped indices. This is the idiomatic way to route a batch:

blocked   = result.indices_for_decision("BLOCK")
approved  = result.indices_for_decision("APPROVE")
not_appr  = result.indices_for_not_decision("APPROVE")
groups    = result.grouped_decision_indices()

print("blocked indices: ", blocked)    # -> [0]
print("approved indices:", approved)   # -> [1]

indices_for_decision("BLOCK") returns the row positions that resolved to a block; indices_for_not_decision("APPROVE") returns everything that did not approve. grouped_decision_indices() returns every decision bucket at once — convenient for fanning a batch out to different downstream sinks.

Step 5 — Extend with an OR branch

Suppose you also want to catch high-value transactions from outside your core markets. Add an or branch so the rule fires when the amount is high and the transaction is either from an emulator or from a country outside US/GB:

conditions:
  and:
    - field: amount
      op: gt
      value: 2000
    - or:
        - field: device_type
          op: eq
          value: emulator
        - field: country_code
          op: not_in
          values: [US, GB]

Logical forms nest freely: and, or, and not can wrap any condition or each other. Add country_code to your records (and optionally to the fields: hints as a categorical) and re-run Step 3 to see the broader rule in action.

Where to go next