> ## Documentation Index
> Fetch the complete documentation index at: https://docs.finwatch.finance/llms.txt
> Use this file to discover all available pages before exploring further.

# Conditions Deep Dive

This guide provides an exhaustive exploration of every mechanism available for constructing `when` clauses in Watch Script rules. Each section includes real-world fraud detection examples, internal behavior details, and common pitfalls.

For the quick-reference version of operators and syntax, see the [DSL Reference](../DSL_REFERENCE.md).

***

## Simple Field Comparisons

The most fundamental building block of a condition is a comparison between a field and a value using one of the six comparison operators.

### Comparison Operators

| Operator | Meaning                  | Example                |
| -------- | ------------------------ | ---------------------- |
| `==`     | Equal to                 | `currency == "USD"`    |
| `!=`     | Not equal to             | `status != "verified"` |
| `>`      | Greater than             | `amount > 10000`       |
| `>=`     | Greater than or equal to | `amount >= 5000`       |
| `<`      | Less than                | `amount < 100`         |
| `<=`     | Less than or equal to    | `amount <= 50000`      |

### Numeric Comparisons

When both sides of a comparison can be interpreted as numbers (specifically `float64`), FinWatch performs a numeric comparison. This is the most common case for the `amount` field:

```go theme={null}
// Exact amount check
when amount == 9999.99

// Range check (combine with 'and')
when amount >= 5000
 and amount <= 50000

// Micro-transaction detection (card testing)
when amount < 1.00
```

### String Comparisons

When either side cannot be parsed as a number, FinWatch falls back to string comparison. Only `==` and `!=` produce meaningful results for strings. Using `>`, `<`, `>=`, or `<=` on non-numeric strings will return `false`.

```go theme={null}
// Exact string match
when currency == "USD"

// Exclude a specific value
when status != "verified"

// Check a metadata string field
when metadata.payment_method == "wire_transfer"
```

> **Warning:** String comparison is case-sensitive. `"USD"` does not equal `"usd"`. If you need case-insensitive matching, use the `regex` operator with `(?i)`.

### Boolean Comparisons

The DSL supports `true` and `false` as literal values:

```go theme={null}
when metadata.is_first_transaction == true
when metadata.kyc_verified == false
```

***

## Accessing Nested Data

Transaction data in FinWatch is a JSON object. Fields can be nested inside the `metadata` (or `meta_data`) object. You access nested fields using **dot notation**.

### Top-Level Fields

These fields are directly available on every transaction:

```go theme={null}
amount              // float64 — the transaction amount
currency            // string  — e.g., "USD", "EUR", "NGN"
source              // string  — source account identifier
destination         // string  — destination account identifier
description         // string  — free-text description
status              // string  — e.g., "pending", "applied", "failed"
timestamp           // string  — RFC3339 timestamp
```

### Metadata Fields (1-Level Nesting)

Your application can send arbitrary metadata with each transaction. Access it with a single dot:

```go theme={null}
when metadata.destination_country == "IR"
when metadata.mcc == "7995"
when metadata.ip_address == "192.168.1.1"
when metadata.days_since_last_transaction > 90
```

### Deeply Nested Fields (2+ Levels)

If your metadata contains nested objects, chain the dots:

```go theme={null}
when metadata.device.fingerprint == "abc123"
when metadata.sender.kyc_level == "basic"
when metadata.location.country_code != "US"
```

### How It Works Internally

The interpreter uses a `dig()` function that splits the field path by `.` and traverses the JSON tree one level at a time:

```go theme={null}
// dig("metadata.device.fingerprint") resolves to:
// transaction["metadata"]["device"]["fingerprint"]
```

If **any** part of the path does not exist, `dig()` returns `(nil, false)` and the condition evaluates to `false`. This is a deliberate design choice — it prevents rules from crashing on incomplete data.

### The Silent Failure Pitfall

This behavior means a typo in a field name will cause a rule to **never trigger** — and there will be no error message to alert you:

```go theme={null}
// BUG: "ammount" is a typo. This condition will ALWAYS be false.
when ammount > 10000

// CORRECT:
when amount > 10000
```

> **Tip:** When a rule isn't triggering as expected, the first thing to check is field name spelling. Log the actual transaction JSON and verify the exact field paths.

***

## Combining Conditions with `and`

The `and` operator requires **both** conditions to be `true` for the combined expression to be `true`.

### Syntax

```go theme={null}
when <condition_1>
 and <condition_2>
 and <condition_3>
```

You can chain as many `and` conditions as you need. All must be `true` for the rule to fire.

### Example: Multi-Factor Suspicious Transaction

```go lines theme={null}
rule HighValueForeignWireTransfer {
    description "Flags high-value wire transfers to foreign countries."

    when amount > 10000
     and currency != "USD"
     and metadata.payment_method == "wire_transfer"
     and metadata.destination_country != "US"

    then review
         score  0.7
         reason "High-value foreign wire transfer detected"
}
```

This rule requires **all four** conditions to be `true`:

1. Amount exceeds \$10,000
2. Currency is not USD
3. Payment method is a wire transfer
4. Destination country is not US

If any single condition is `false`, the entire rule does not fire.

### Top-Level AND Semantics

At the top level of a rule's `when` clause, conditions connected by `and` are evaluated with **AND semantics** — meaning all must pass. The interpreter short-circuits: if any condition fails, it stops evaluating and returns `false` immediately.

This short-circuit behavior is important for performance. See the [Short-Circuit Evaluation](#short-circuit-evaluation) section below.

***

## Combining Conditions with `or`

The `or` operator requires **at least one** condition to be `true` for the combined expression to be `true`.

### Syntax

```go theme={null}
when <condition_1>
  or <condition_2>
```

### Example: Late Night Transaction Detection

```go lines theme={null}
rule LateNightTransactions {
    description "Detects transactions made late at night."

    when hour_of_day(timestamp) >= 23
      or hour_of_day(timestamp) <= 4

    then review
         score  0.4
         reason "Transaction occurred during late night hours"
}
```

This rule fires if the transaction happened at 11 PM or later **OR** at 4 AM or earlier.

### The Precedence Pitfall

This is one of the most critical details in the entire DSL:

> **`and` and `or` have equal precedence and are evaluated left-to-right.**

There is **no** implicit grouping that makes `and` bind tighter than `or` (unlike most programming languages where `&&` has higher precedence than `||`).

Consider this rule:

```go theme={null}
when A or B and C
```

In most programming languages, this would be interpreted as `A or (B and C)`. **In Watch Script, it is interpreted as `(A or B) and C`** — because it's evaluated strictly left-to-right.

This can lead to unexpected behavior. For example:

```go theme={null}
// INTENDED: Flag if (late night) OR (early morning AND high value)
// ACTUAL:  Flag if (late night OR early morning) AND high value
when hour_of_day(timestamp) >= 23
  or hour_of_day(timestamp) <= 4
 and amount > 5000
```

The actual behavior is `(hour >= 23 OR hour <= 4) AND amount > 5000` — which means a \$100 late-night transaction would **not** trigger the rule, even though you might have intended it to.

### How to Handle Complex Logic

**Option 1: Restructure your conditions.** Place the `and` conditions before the `or`:

```go theme={null}
when amount > 5000
 and hour_of_day(timestamp) >= 23
```

**Option 2: Split into multiple rules.** If you need true `OR` logic between independent patterns, create separate rules:

```go lines theme={null}
// Rule 1: Late night + high value
rule LateNightHighValue {
    when hour_of_day(timestamp) >= 23
     and amount > 5000
    then review score 0.5 reason "Late night high-value transaction"
}

// Rule 2: Early morning + high value
rule EarlyMorningHighValue {
    when hour_of_day(timestamp) <= 4
     and amount > 5000
    then review score 0.5 reason "Early morning high-value transaction"
}
```

This is the recommended approach for complex logic. Each rule is simpler, easier to test, and produces a clear, specific reason.

***

## Set Membership with `in`

The `in` operator checks whether a field's value exists within a given list. This is essential for blacklists, whitelists, and category checks.

### Syntax with Inline Arrays

```go theme={null}
when metadata.mcc in ("7995", "6012", "4829")
```

Inline arrays use parentheses with comma-separated values. Values can be strings or numbers.

### Syntax with Variables

```go theme={null}
when metadata.destination_country in $sanctioned_countries
```

Variables (prefixed with `$`) reference externally-managed lists. This is the recommended approach for lists that change over time. See the [Variables and Dynamic Data](variables-and-dynamic-data.md) guide for details.

### How It Works Internally

The interpreter converts the field value to a string, then iterates through the list and compares each element as a string:

```go theme={null}
g := fmt.Sprint(got)   // Convert field value to string
for _, v := range arr {
    if g == fmt.Sprint(v) {  // Compare as strings
        found = true
        break
    }
}
```

This means `in` is a **string membership check**. The number `7995` and the string `"7995"` will match each other because both are converted to the string `"7995"` before comparison.

### Example: Sanctioned Country Check

```go lines theme={null}
rule SanctionedCountryCheck {
    description "Blocks transactions to sanctioned countries."

    when metadata.destination_country in $sanctioned_countries

    then block
         score  1.0
         reason "Destination country is on global sanctions list"
}
```

### Example: High-Risk Merchant Category

```go lines theme={null}
rule SuspiciousMCCCheck {
    description "Flags transactions with high-risk merchant category codes."

    when metadata.mcc in ("7995", "6012", "4829", "6211")

    then review
         score  0.4
         reason "Transaction uses a high-risk merchant category code"
}
```

The MCC codes above correspond to:

* `7995`: Gambling
* `6012`: Financial institutions
* `4829`: Wire transfers
* `6211`: Security brokers

***

## Pattern Matching with `regex` and `not_regex`

Regular expression operators let you match patterns in string fields. This is powerful for detecting suspicious keywords in free-text fields like transaction descriptions.

### Syntax

```go theme={null}
when <field> regex "<pattern>"
when <field> not_regex "<pattern>"
```

* `regex` returns `true` if the pattern matches **anywhere** in the field value.
* `not_regex` returns `true` if the pattern does **not** match.

### The Pattern Language

FinWatch uses Go's `regexp` package, which implements the [RE2 syntax](https://github.com/google/re2/wiki/Syntax). This is a safe subset of regular expressions that guarantees linear-time matching (no catastrophic backtracking).

### Case-Insensitive Matching

Use `(?i)` at the beginning of your pattern for case-insensitive matching:

```go theme={null}
// Matches "bitcoin", "Bitcoin", "BITCOIN", "bItCoIn", etc.
when description regex "(?i)bitcoin"
```

### Example: Suspicious Description Patterns

```go lines theme={null}
rule SuspiciousDescriptionCheck {
    description "Detects suspicious keywords or patterns in transaction descriptions."

    when description regex "(?i)(btc|bitcoin|crypto|wallet|transfer|gift.?card|western.?union)"
     and amount > 1000

    then review
         score  0.2
         reason "Suspicious description pattern."
}
```

This pattern matches:

* `btc`, `bitcoin`, `crypto`, `wallet`, `transfer` — cryptocurrency and money transfer keywords
* `gift.?card` — "giftcard", "gift card", "gift-card" (`.?` matches zero or one character)
* `western.?union` — "westernunion", "western union", "western-union"

### Example: Email Domain Filtering

```go lines theme={null}
rule SuspiciousEmailDomain {
    description "Flags transactions from temporary email domains."

    when metadata.email regex "(?i)@(tempmail|guerrillamail|throwaway|mailinator)\\.com$"

    then review
         score  0.3
         reason "Transaction initiated from a temporary email domain"
}
```

### Example: Excluding Known-Good Patterns

```go lines theme={null}
rule NonStandardReference {
    description "Flags transactions with non-standard reference formats."

    when reference not_regex "^[A-Z]{3}-[0-9]{6,10}$"
     and amount > 5000

    then alert
         score  0.2
         reason "Transaction reference does not match expected format"
}
```

### Performance Warning

Regular expressions are evaluated at runtime for every transaction. While RE2 guarantees linear-time matching, complex patterns on long strings can still be slow. Guidelines:

* **Keep patterns simple.** Prefer alternation (`a|b|c`) over complex nested groups.
* **Anchor when possible.** Use `^` and `$` to anchor patterns to the start/end of the string. This allows the engine to fail fast.
* **Avoid `.+` on large fields.** Patterns like `.+something` must scan the entire string.
* **Combine with cheap conditions.** Use `and amount > X` to filter out low-value transactions before the regex runs.

***

## Type Coercion

Understanding how FinWatch compares values of different types is essential for writing correct rules.

### The Comparison Algorithm

When evaluating a comparison like `amount > 10000`, the interpreter follows this algorithm:

1. **Try numeric comparison first.** If both the field value and the comparison value can be parsed as `float64`, perform a numeric comparison.
2. **Fall back to string comparison.** If either value cannot be parsed as a number, convert both to strings using `fmt.Sprint()` and compare lexicographically. For string comparisons, only `==` and `!=` are supported; `>`, `<`, `>=`, `<=` return `false`.

### Practical Implications

**Numbers work as expected:**

```go theme={null}
// Both sides are numeric. Correct behavior.
when amount > 10000    // 15000 > 10000 → true
when amount == 100.50  // 100.5 == 100.5 → true
```

**String numbers can be tricky:**

If the `amount` field arrives as a string `"15000"` (which can happen depending on your JSON serialization), FinWatch will still parse it as a number and compare correctly — because `toFloat()` handles string-to-float conversion.

However, if you compare a string field like `currency` with `>`, the result may surprise you:

```go theme={null}
// String comparison: "EUR" > "USD"?
// Lexicographically: "E" < "U", so this is FALSE.
// This is almost certainly not what you intended.
when currency > "EUR"  // Probably a bug
```

> **Tip:** Only use `==` and `!=` for string fields. Use `>`, `<`, `>=`, `<=` exclusively for numeric fields.

### JSON Number Types

When transaction data arrives as JSON, numbers without quotes are parsed as `float64` or `json.Number`. Numbers with quotes are parsed as strings. FinWatch's `toFloat()` function handles both:

```json theme={null}
{ "amount": 1000 }      // → float64, works perfectly
{ "amount": "1000" }     // → string, but toFloat() parses it to 1000.0
{ "amount": "one thousand" } // → string, toFloat() fails, falls back to string comparison
```

***

## Short-Circuit Evaluation

FinWatch's interpreter uses short-circuit evaluation for logical operators. This has important implications for both correctness and performance.

### How It Works

* **`and`:** If the left condition is `false`, the right condition is **not evaluated**. The result is `false`.
* **`or`:** If the left condition is `true`, the right condition is **not evaluated**. The result is `true`.

### Why It Matters for Performance

Aggregate functions (`count()`, `sum()`, etc.) are the most expensive operations in FinWatch because they execute SQL queries against DuckDB. Simple field comparisons are nearly free — they just look up a value in a map.

By placing cheap conditions **before** expensive conditions in an `and` chain, you can avoid running expensive queries on transactions that would fail the cheap check anyway:

```go theme={null}
// GOOD: Cheap check first. If amount <= 100, the count() never runs.
when amount > 100
 and count(when destination == $current.destination, "PT24H") > 10

// BAD: Expensive count() runs on EVERY transaction, even $1 ones.
when count(when destination == $current.destination, "PT24H") > 10
 and amount > 100
```

In a high-throughput environment processing thousands of transactions per second, this optimization can reduce DuckDB query load by an order of magnitude.

### The Rule of Thumb

Order your `and` conditions from **cheapest to most expensive**:

1. **Simple field comparisons** (`amount > X`, `currency == "USD"`) — near zero cost.
2. **Metadata lookups** (`metadata.country == "US"`) — very cheap.
3. **Regex matching** (`description regex "..."`) — moderate cost.
4. **Time functions** (`hour_of_day(timestamp) >= 23`) — cheap but involves parsing.
5. **Aggregate functions** (`count(...)`, `sum(...)`) — expensive (SQL query).
6. **Previous transaction lookups** (`previous_transaction(...)`) — most expensive (SQL query with joins).

***

## Next Steps

You now have a thorough understanding of every condition mechanism in the Watch Script DSL. Continue your learning with:

* [**Aggregate Functions Guide**](aggregate-functions-guide.md) — Deep dive into `count()`, `sum()`, `avg()`, `max()`, `min()` for detecting behavioral patterns over time.
* [**Time-Based Rules**](time-functions-guide.md) — Use `hour_of_day()`, `day_of_week()`, and other time functions.
* [**Previous Transaction Lookups**](previous-transaction-guide.md) — Detect sequential fraud patterns with `previous_transaction()`.
* [**Variables and Dynamic Data**](variables-and-dynamic-data.md) — Use `$variables` for externally-managed lists and `$current` for self-referencing filters.
* [**DSL Reference**](../DSL_REFERENCE.md) — The complete language specification.
