Skip to main content
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:
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 $:

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:
  2. As dynamic field references with $current — inside function arguments:
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

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:
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():
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:

Example: Self-Transfer Detection

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:

Using Variables in Rules

Once defined, variables can be used in any rule:

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:

Pattern 2: Whitelists

Allow transactions that match trusted criteria (use with allow verdict):

Pattern 3: Dynamic Thresholds

Use variables for thresholds that vary by region or account type:

Pattern 4: Regional Configuration

Different regions may have different risk profiles:
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:

Name Variables Descriptively

The variable name should tell a reader exactly what it contains without needing to look up the definition:

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