Skip to main content

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.

Variables are the mechanism that keeps your rules flexible and maintainable over time. Instead of hardcoding values directly into rule logic, variables allow you to reference externally-managed data — sanctions lists, risk thresholds, account categories — that can be updated independently of the rules themselves.

The Problem: Hardcoded Data in Rules

Consider this rule:
rule SanctionedCountryCheck {
    description "Blocks transactions to sanctioned countries."

    when metadata.destination_country in ("IR", "KP", "SY", "CU", "VE")

    then block
         score  1.0
         reason "Destination country is on sanctions list"
}
This works, but it has a critical flaw: the sanctions list is embedded in the rule. When the list changes (and it will — OFAC updates its list regularly), you have to:
  1. Edit the .ws file.
  2. Commit the change.
  3. Get it reviewed and merged.
  4. Wait for FinWatch to pick up the change.
For a list that might change weekly, this creates an operational burden. Worse, the same list might be referenced by multiple rules, requiring coordinated updates across several files. Variables solve this by separating the data from the logic.

Variable Syntax

Variables in Watch Script are prefixed with $:
$sanctioned_countries
$high_risk_bins
$blocked_accounts
$max_daily_limit

Naming Conventions

  • Always use $snake_case.
  • The name should clearly describe the content.
  • Good: $sanctioned_countries, $high_risk_mccs, $blocked_card_bins
  • Bad: $list1, $data, $x, $SC

Where Variables Are Used

Variables appear in two contexts within the Watch Script DSL:
  1. As values in comparisons — typically with the in operator:
    when metadata.destination_country in $sanctioned_countries
    when metadata.mcc in $high_risk_mccs
    
  2. As dynamic field references with $current — inside function arguments:
    count(when source == $current.source, "PT24H")
    sum(when destination == $current.destination, "PT24H")
    
These two uses serve fundamentally different purposes. External variables ($sanctioned_countries) reference static or semi-static data managed outside the rule. The $current variable references the transaction currently being evaluated.

$current — The Current Transaction Context

The $current variable is a special, built-in variable that provides access to the fields of the transaction currently being evaluated. It is the bridge between “this transaction” and “historical transactions” — enabling you to ask questions like “how many other transactions share characteristics with this one?”

Syntax

$current.<field_name>
Where <field_name> is any top-level field on the transaction: source, destination, amount, currency, description, status, etc.

How It Works Internally

When the interpreter encounters a $current.* reference, it calls the resolvePlaceholder() function:
  1. Checks if the value is a string starting with "$current.".
  2. Strips the "$current." prefix to get the field path.
  3. Calls dig() to look up the field in the current transaction’s data.
  4. Returns the resolved value.
If the field does not exist in the current transaction, the placeholder resolves to nil and the condition evaluates to false.

Common Usage Patterns

Inside Aggregate Functions

The most common use of $current is inside aggregate function filters, where it creates self-referencing queries:
// "Count transactions where the destination matches THIS transaction's destination"
count(when destination == $current.destination, "PT24H")

// "Sum amounts where the source matches THIS transaction's source"
sum(when source == $current.source, "PT24H")

// "Average amount from THIS transaction's source over the last 30 days"
avg(when source == $current.source, "P30D")
Without $current, you’d have no way to make an aggregate filter relative to the current transaction. You’d have to hardcode a specific account ID, which would make the rule useless for anyone else.

Inside previous_transaction() Match

$current references are also used inside the match object of previous_transaction():
previous_transaction(
    within: "PT1H",
    match: {
        status: "failed",
        source: "$current.source"    // Find failures from the SAME source
    }
)
Note that inside match, the $current reference is a string value (with quotes): "$current.source". This is because the match object uses string values that are resolved at runtime by the interpreter.

In Simple Comparisons

You can also use $current in simple conditions to compare fields:
// Check if source and destination are the same (self-transfer)
when source == $current.destination

Example: Self-Transfer Detection

rule SelfTransferCheck {
    description "Detects when the source and destination of a transaction are the same."

    when source == $current.destination

    then review
         score  0.5
         reason "Self-transfer detected — source and destination are identical"
}
What this detects: Transactions where money is being sent to the same account it’s coming from. This is a common technique for money laundering — the fraudster “washes” money by sending it to themselves through different channels.

External Variable Lists

External variables like $sanctioned_countries reference data that lives outside the rule file. This data is managed separately and injected into the rule engine at evaluation time.

How External Variables Work

When the parser encounters a variable like $sanctioned_countries, it stores it as a Variable AST node with the name sanctioned_countries. During compilation to the JSON rule format, the variable is preserved as a reference. At evaluation time, the interpreter resolves the variable against a variable registry — a key-value store of variable names to their values.

Defining Variable Lists

Variables are typically defined in a configuration file or through an API. The exact mechanism depends on your FinWatch deployment, but the pattern is always the same:
{
  "sanctioned_countries": ["IR", "KP", "SY", "CU", "VE", "MM"],
  "high_risk_mccs": ["7995", "6012", "4829", "6211"],
  "blocked_card_bins": ["411111", "400000", "510510"]
}

Using Variables in Rules

Once defined, variables can be used in any rule:
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"
}
rule HighRiskMCCCheck {
    description "Flags transactions with high-risk merchant category codes."

    when metadata.mcc in $high_risk_mccs

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

Updating Variables Without Redeploying Rules

The key benefit of variables is operational agility. When the OFAC sanctions list is updated:
  1. Update the $sanctioned_countries variable in your configuration.
  2. FinWatch picks up the new values.
  3. All rules referencing $sanctioned_countries immediately use the updated list.
  4. No rule files are modified. No Git commits. No PR reviews for the rule itself.
This separation of concerns means your compliance team can manage the data while your engineering team manages the logic.

Practical Patterns

Pattern 1: Blacklists

Block or flag transactions involving known-bad entities:
// Block transactions to blacklisted accounts
when destination in $blocked_accounts

// Flag transactions from known fraud BINs
when metadata.card_bin in $fraud_bins

// Block transactions from sanctioned entities
when metadata.sender_id in $sanctioned_entities

Pattern 2: Whitelists

Allow transactions that match trusted criteria (use with allow verdict):
rule TrustedPartnerBypass {
    description "Allows transactions to pre-verified trusted partners."

    when destination in $trusted_partners

    then allow
         score  0.0
         reason "Destination is a pre-verified trusted partner"
}

Pattern 3: Dynamic Thresholds

Use variables for thresholds that vary by region or account type:
// If your variable system supports scalar values:
when amount > $high_value_threshold

// Or use metadata to apply different thresholds per account tier:
when metadata.account_tier == "basic"
 and amount > 5000

Pattern 4: Regional Configuration

Different regions may have different risk profiles:
rule HighRiskRegionCheck {
    description "Extra scrutiny for transactions from high-risk regions."

    when metadata.source_region in $high_risk_regions
     and amount > 1000

    then review
         score  0.5
         reason "Transaction from a high-risk region"
}
The $high_risk_regions variable can be maintained by your regional compliance teams without touching the rule logic.

Variable Resolution Order

When the interpreter evaluates a condition, it resolves values in this order:
  1. Literal values — Numbers (10000), strings ("USD"), booleans (true, false), and inline arrays (("a", "b", "c")) are used as-is.
  2. $current.* references — Resolved against the current transaction’s fields using resolvePlaceholder().
  3. External variables — Resolved against the variable registry.
  4. Missing values — If a $current.* field doesn’t exist, the condition returns false. If an external variable isn’t defined, the behavior depends on the operator (for in, an empty list means no match).

Best Practices

Keep Rules Logic-Only

A rule should contain logic, not data. If you find yourself listing more than 2-3 values inline, extract them into a variable:
// BAD: Data embedded in the rule
when metadata.country in ("IR", "KP", "SY", "CU", "VE", "MM", "BY", "ZW", "SD")

// GOOD: Data externalized
when metadata.country in $sanctioned_countries

Name Variables Descriptively

The variable name should tell a reader exactly what it contains without needing to look up the definition:
BadGood
$list$sanctioned_countries
$codes$high_risk_mcc_codes
$bad$blocked_card_bins
$t$max_daily_transfer_limit

Document Variable Sources

For each variable, document:
  • Where the data comes from (OFAC, internal fraud database, compliance team).
  • How often it’s updated (daily, weekly, quarterly).
  • Who is responsible for maintaining it.
  • What format the values are in (ISO country codes, 6-digit BINs, account IDs).

Use $current Consistently

When writing aggregate filters or previous_transaction() match criteria, always use $current.* to reference the current transaction. Never hardcode account IDs or other transaction-specific values in a rule.

Next Steps