> ## 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.

# Previous Transaction Lookups

Some fraud patterns can't be detected by looking at a single transaction or even aggregating numbers over time. They require understanding the **sequence** of events — what happened before this transaction, and does the combination tell a story of fraud? The `previous_transaction()` function gives you this power.

***

## What is Sequential Pattern Detection?

Sequential pattern detection is the ability to ask: "Has something specific happened recently that makes this current transaction suspicious?"

Consider these real-world fraud patterns:

* **Retry after failure:** A transaction fails (insufficient funds, declined card), and within minutes, a new transaction comes in from the same source with a slightly different amount. This is a common pattern in account takeover — the fraudster is probing limits.
* **Credential stuffing follow-up:** A login attempt from a new device is followed by a large transfer. The device change indicates possible account compromise.
* **Rapid reversal:** A refund or reversal is immediately followed by a new purchase for a larger amount. This is common in refund fraud.

None of these patterns involve a single suspicious transaction. Each individual transaction might look perfectly normal. It's the **sequence** that reveals the fraud.

***

## The `previous_transaction()` Function

### Syntax

```go theme={null}
previous_transaction(
    within: "<time_window>",
    match: {
        <field>: <value>,
        <field>: "$current.<field>"
    }
)
```

### Parameters

| Parameter | Type                   | Description                                                                                         |
| --------- | ---------------------- | --------------------------------------------------------------------------------------------------- |
| `within`  | Named argument, string | ISO 8601 duration defining how far back to look (e.g., `"PT1H"` for 1 hour).                        |
| `match`   | Named argument, object | A set of key-value pairs that previous transactions must satisfy. All pairs must match (AND logic). |

### Return Value

**Boolean** — `true` if at least one historical transaction exists within the time window that matches ALL of the specified criteria. `false` otherwise.

### How `match` Works

The `match` object defines the criteria for finding a previous transaction. Each key is a field name in the `transactions` table, and each value is either:

* **A literal value:** `"failed"`, `"blocked"`, `100` — matches transactions where that field equals this exact value.
* **A `$current` reference:** `"$current.source"` — resolved at runtime to the current transaction's field value. This lets you find previous transactions from the *same* user, account, or device.

All match conditions are combined with AND logic — a previous transaction must satisfy **every** condition in the `match` object to count.

***

## Example: Block After Previous Failure

This is the most common use case for `previous_transaction()`. If a recent transaction from the same source failed, and now a new high-value transaction is coming in, it could indicate a fraudster probing account limits.

```go lines theme={null}
rule BlockWhenPreviousTransactionFailed {
    description "Block when previous transaction failed for same source"

    when previous_transaction(
        within: "PT1H",
        match: {
            status: "failed",
            source: "$current.source"
        }
    )
    and amount > 700000

    then block
         score  1.0
}
```

### Breaking It Down

1. **`within: "PT1H"`** — Look back 1 hour from now.
2. **`match: { status: "failed" }`** — Find transactions where the `status` field equals `"failed"`.
3. **`match: { source: "$current.source" }`** — And where the `source` field matches the current transaction's source. If the current transaction has `source: "acct_alice"`, this resolves to `source: "acct_alice"`.
4. **`and amount > 700000`** — Additionally, the current transaction must be over 700,000 (a high-value threshold).
5. **`then block score 1.0`** — If both conditions are true, hard-block the transaction with maximum confidence.

### What the Engine Does Internally

The interpreter builds a SQL query:

```sql theme={null}
SELECT COUNT(*) FROM transactions
WHERE status = 'failed'
  AND source = 'acct_alice'
  AND timestamp >= '2026-04-18T13:30:00'  -- now minus 1 hour
LIMIT 1
```

If the count is greater than 0, the condition evaluates to `true`.

### Testing This Rule

**Step 1:** Inject a failed transaction:

```bash theme={null}
curl -X POST http://localhost:8081/inject \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "txn_fail_001",
    "amount": 500000,
    "currency": "USD",
    "source": "acct_alice",
    "destination": "acct_bob",
    "reference": "ref_fail_001",
    "status": "failed"
  }'
```

**Step 2:** Within the next hour, inject a high-value transaction from the same source:

```bash theme={null}
curl -X POST http://localhost:8081/inject \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "txn_retry_001",
    "amount": 800000,
    "currency": "USD",
    "source": "acct_alice",
    "destination": "acct_charlie",
    "reference": "ref_retry_001",
    "status": "pending"
  }'
```

**Expected result:** The rule fires. There is a failed transaction from `acct_alice` within the last hour, and the current transaction exceeds 700,000. Verdict: `block`, score: `1.0`.

**Step 3:** Inject a high-value transaction from a *different* source:

```bash theme={null}
curl -X POST http://localhost:8081/inject \
  -H "Content-Type: application/json" \
  -d '{
    "transaction_id": "txn_clean_001",
    "amount": 900000,
    "currency": "USD",
    "source": "acct_dave",
    "destination": "acct_eve",
    "reference": "ref_clean_001",
    "status": "pending"
  }'
```

**Expected result:** The rule does **not** fire. There are no failed transactions from `acct_dave` in the last hour.

***

## Example: Detecting Account Takeover Patterns

Account takeover (ATO) often follows a predictable sequence: a suspicious event (device change, password reset) followed by a large transfer. You can model this with `previous_transaction()` if your system logs these events as transactions or includes relevant metadata.

```go lines theme={null}
rule AccountTakeoverPattern {
    description "Detects large transfers following a recent password change event."

    when previous_transaction(
        within: "PT2H",
        match: {
            description: "password_change",
            source: "$current.source"
        }
    )
    and amount > 5000

    then block
         score  0.9
         reason "Large transfer detected within 2 hours of a password change event"
}
```

**What this detects:** If the same source account had a "password\_change" event logged in the last 2 hours, and now a transfer over \$5,000 is being attempted, block it. In many ATO scenarios, the first thing a fraudster does after gaining access is change the password to lock out the legitimate owner, then immediately initiate transfers.

> **Note:** This requires your system to log password change events as transactions (or as records in the transactions table). If your system stores these events differently, you'll need to adapt the approach.

***

## Example: Detecting Rapid Retry Patterns

Fraudsters often retry transactions with slightly modified parameters after an initial failure — adjusting the amount, changing the destination, or switching currencies.

```go lines theme={null}
rule RapidRetryAfterDecline {
    description "Detects rapid retry after a declined transaction from the same source."

    when previous_transaction(
        within: "PT15M",
        match: {
            status: "declined",
            source: "$current.source"
        }
    )
    and amount > 1000

    then review
         score  0.6
         reason "Transaction attempted within 15 minutes of a declined transaction from the same source"
}
```

**What this detects:** A 15-minute window is deliberately tight. Legitimate users might retry after a decline, but they usually take some time (checking their balance, calling their bank). A retry within 15 minutes, especially for a significant amount, is more likely to be automated or fraudulent.

***

## Combining `previous_transaction()` with Aggregates

The most sophisticated rules combine `previous_transaction()` with aggregate functions to create layered detection. This lets you check both the sequence of events and the volume of activity.

```go lines theme={null}
rule FailedThenHighVolume {
    description "High-volume transfers after a recent failure from the same source."

    when previous_transaction(
        within: "PT1H",
        match: {
            status: "failed",
            source: "$current.source"
        }
    )
    and count(when source == $current.source, "PT1H") > 3
    and amount > 1000

    then block
         score  0.95
         reason "Multiple transactions after a failed attempt — possible account compromise"
}
```

**What this detects:** Three layers of suspicion:

1. A failed transaction from this source in the last hour (sequential pattern).
2. More than 3 transactions from this source in the last hour (velocity).
3. The current transaction is over \$1,000 (significance threshold).

Together, these signals paint a picture of a compromised account: the attacker's first attempt failed, but they're continuing to try with multiple rapid transactions.

***

## Combining with Time Functions

```go lines theme={null}
rule NightFailureRetry {
    description "Late-night retry after failure is extremely suspicious."

    when previous_transaction(
        within: "PT30M",
        match: {
            status: "failed",
            source: "$current.source"
        }
    )
    and hour_of_day(timestamp) >= 23
    and amount > 5000

    then block
         score  1.0
         reason "Late-night high-value retry after failed transaction"
}
```

**What this detects:** A failed transaction followed by a high-value retry at 11 PM or later. This combines three independent risk signals: sequential pattern (retry after failure), temporal anomaly (late night), and significance (high value).

***

## Performance Notes

`previous_transaction()` is the most expensive operation in the FinWatch DSL because it executes a SQL query against DuckDB for each evaluation.

### How the Query Works

For each `previous_transaction()` call, the interpreter:

1. Resolves all `$current.*` placeholders in the `match` object.
2. Builds a `WHERE` clause with one condition per match key-value pair.
3. Adds a timestamp filter for the `within` window.
4. Executes `SELECT COUNT(*) FROM transactions WHERE ... LIMIT 1`.
5. Returns `true` if count > 0.

### Optimization Tips

1. **Use the smallest `within` window possible.** `"PT15M"` scans far less data than `"P1D"`.
2. **Gate with cheap conditions.** Always put simple checks (`amount > X`) before `previous_transaction()` in your `and` chain. This avoids running expensive SQL queries on low-value transactions:
   ```go theme={null}
   // GOOD: amount check runs first, skips previous_transaction for small amounts
   when amount > 5000
    and previous_transaction(within: "PT1H", match: { status: "failed", source: "$current.source" })
   // BAD: previous_transaction runs on every transaction, even $1 ones
   when previous_transaction(within: "PT1H", match: { status: "failed", source: "$current.source" })
    and amount > 5000
   ```
3. **Be specific in your `match` criteria.** The more fields you specify in `match`, the more selective the query is, and the faster it returns.
4. **Avoid very large time windows.** A `within: "P30D"` window on a high-traffic system can be slow. If you need long lookback periods, consider using aggregate functions instead (`count()` with a large window), as those are optimized with the batch aggregate context.

***

## Next Steps

* [**Variables and Dynamic Data**](variables-and-dynamic-data.md) — Learn how `$current` and `$variables` work across all function types.
* [**Aggregate Functions Guide**](aggregate-functions-guide.md) — Compare `previous_transaction()` with aggregate approaches.
* [**The Rule Cookbook**](rule-cookbook.md) — See production-ready rules that use sequential pattern detection.
* [**Conditions Deep Dive**](conditions-deep-dive.md) — Master the full condition system.
* [**DSL Reference**](../DSL_REFERENCE.md) — Complete function reference.
