Condition Composition

Combine predicates into readable condition trees without losing same-element or null semantics.

Every rule has one conditions root. That root may be a scalar predicate, a boolean branch, or a derived predicate such as a window, model score, vector distance, SQL expression, or array_any scan.

Boolean branches

and and or contain lists of child conditions. not contains one child condition. Branches may be nested to a maximum depth of 128.

conditions:
  and:
    - {field: order.amount, op: gte, value: 500}
    - {field: customer.account_age_days, op: lt, value: 7}

Mixed nesting

Use explicit branches when precedence must be obvious. This rule reviews a large order only when either the account is new or the device is risky:

- id: large_order_risk
  action: review
  weight: 35
  conditions:
    and:
      - {field: order.amount, op: gte, value: 750}
      - or:
          - {field: customer.account_age_days, op: lt, value: 14}
          - {field: device.fingerprint, op: in_lookup, lookup: blocked_devices}

Keep each branch responsible for one business statement. If a branch becomes difficult to name in one sentence, split it into multiple rules and let decision precedence and scoring combine their effects.

Cross-field conditions

Cross-field operators compare two columns from the same record. They do not require an arithmetic expression:

conditions:
  and:
    - {field: order.amount, op: gt_field, other_field: customer.avg_order_value_30d}
    - {field: billing.country, op: neq_field, other_field: shipping.country}

Available cross-field operators are gt_field, lt_field, gte_field, lte_field, eq_field, and neq_field. Numeric comparisons require compatible numeric columns. Equality and inequality also support encoded categorical/entity columns.

Nested object fields

Dotted names address nested object fields consistently across JSON, Arrow structs, Avro, Protobuf, and PyArrow batches:

conditions:
  and:
    - {field: merchant.risk.score, op: gte, value: 70}
    - {field: network.ip_address, op: ip_not_in_subnet, value: "10.0.0.0/8"}

Only fields referenced by the compiled ruleset are projected. A missing or null parent makes a normal comparison false; use is_null or is_not_null when null state is itself the condition.

Arrays of objects with array_any

array_any evaluates a condition tree against each object in an array. All children under where are scoped to the same array element:

conditions:
  array_any:
    path: cart.items
    where:
      and:
        - {field: price, op: gt, value: 500}
        - {field: category, op: in, values: [electronics, luxury]}
        - {field: seller.risk_score, op: gte, value: 60}

The rule does not match when one item satisfies price > 500 and a different item satisfies the category condition. A null or empty list produces false. Nested array_any inside another array_any is not supported.

Combining windows and model scores

Derived predicates are ordinary condition children and can be combined with scalar predicates:

conditions:
  and:
    - window:
        entity_field: customer.id
        function: count
        duration_seconds: 600
        op: gte
        value: 5
    - model_score:
        model: checkout_risk
        features: [order.amount, customer.account_age_days, device.risk_score]
        op: gte
        value: 0.82
    - {field: merchant.id, op: not_in_lookup, lookup: trusted_merchants}

model_score.features must contain numeric scalar fields. Window state is keyed by entity_field; use partition-affine processing when the same entity may arrive through parallel stream workers.

Null-safe patterns

Normal comparisons against null or missing values return false. Express alternate behavior explicitly:

conditions:
  or:
    - {field: customer.phone, op: is_null}
    - {field: customer.phone, op: is_empty}
    - {field: customer.phone, op: starts_with, value: "+"}

For a condition that requires both presence and comparison, use and:

conditions:
  and:
    - {field: customer.email, op: is_not_null}
    - {field: customer.email, op: ends_with, value: ".invalid"}

Related pages


Did this page help you?