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

# Rules engine

> Write CEL expressions in the Tracer rules engine to shape ALLOW, DENY, and REVIEW decisions in real time, and manage the DRAFT to ACTIVE lifecycle.

export const GMetadata = ({children}) => <Tooltip headline="Metadata" tip="Additional key-value information attached to entities like accounts or transactions — such as external IDs, reference numbers, or department codes." cta="See glossary" href="/en/glossary">
    {children}
  </Tooltip>;

export const GCEL = ({children}) => <Tooltip headline="CEL (Common Expression Language)" tip="A lightweight expression language for writing business rules — for example, 'if transaction amount > 10000 then REVIEW'. Tracer uses CEL for validation rules." cta="See glossary" href="/en/glossary">
    {children}
  </Tooltip>;

The rules engine is what risk and fraud teams use to change how transactions are approved or blocked, without touching application code. Each rule is a small expression that runs on every transaction Tracer validates — "block this MCC for this segment", "send anything over R\$ 50k to manual review", "deny if the account is suspended".

**What changes in your operation:** rule changes ship through an API endpoint, not through a release. An analyst can publish a new rule in the morning and see it evaluating real transactions within seconds. Every match is recorded, so a denied customer call six months later can be traced back to the exact rule that fired.

**Trade-off to be honest about:** you have to think in CEL (Common Expression Language) instead of Go, Python, or Java. The learning curve is short — most rules are one line — but the team writing them is no longer your application developers. The upside is no deploys, full audit, and the people closest to the policy own the policy.

<Tip>
  **Who is this guide for?** Risk and fraud analysts who will write rules, developers integrating the validation call, and compliance officers reading the audit trail. The CEL examples get technical further down, but the lifecycle and decision logic are useful for anyone evaluating the product.
</Tip>

The **Tracer rules engine** evaluates validation logic written in <GCEL>CEL (Common Expression Language)</GCEL> — a type-safe expression language from Google. Expressions are compiled at rule creation and run during every transaction validation; you change behavior by updating rules through the API, without redeploying code.

## Why use the rules engine

***

* **Flexibility**: Create and modify rules without code deploys
* **Performance**: Compiled expressions evaluate in under 1ms each
* **Type safety**: Expression syntax validated at rule creation
* **Every active rule runs**: All matching rules are evaluated so the audit trail records every trigger, not just the winning category
* **Scope-based**: Apply rules to specific segments, accounts, or transaction types

By the end of this guide, you will:

* Understand rule engine concepts and evaluation flow
* Create and test expression-based rules
* Manage the rule lifecycle (DRAFT, ACTIVE, INACTIVE, DELETED)
* Apply best practices for rule management

***

## What is the rules engine

***

The rules engine is the Tracer component responsible for evaluating expressions during transaction validation. It enables fraud analysts and risk managers to configure business logic that executes in real time—without requiring code deployments or engineering support.

### How it works

<Frame caption="Figure 1. Rules engine evaluation flow">
  <img src="https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/how-rules-works.svg?fit=max&auto=format&n=SEOef3JqTInYAAau&q=85&s=04a0fa4b9b4255b519e391b16ce1f1ef" alt="How the rules engine evaluates configured expressions against the transaction context during validation and returns a decision" width="1161" height="284" data-path="images/en/d2/how-rules-works.svg" />
</Frame>

In this flow:

* **Load rules** fetches all active rules from cache (or database on cache miss)
* **Evaluate expressions** runs all CEL expressions against the transaction context
* **Collect matches** gathers all rules that matched and determines the decision

### Evaluation pattern

All active rules are evaluated against every transaction. There is no priority ordering or short-circuit evaluation. This ensures:

* Complete audit trail (all matching rules recorded)
* No information loss (analysts can see all triggers)
* Simple logic (no priority conflicts)

**Decision precedence** (highest to lowest):

1. **DENY** — any matching `DENY` rule wins outright.
2. **Limit exceeded** — if no DENY rule matched but any applicable limit is exceeded, the decision is DENY (rule precedence applies first; limits come in only when no DENY rule matched).
3. **REVIEW** — if no DENY rule matched and no limit was exceeded, any matching `REVIEW` rule wins.
4. **ALLOW** — if only `ALLOW` rules matched, the decision is ALLOW.
5. **Default** — if no rule matched at all, Tracer returns the configured `DEFAULT_DECISION_WHEN_NO_MATCH` (`ALLOW` unless explicitly set to `DENY` for fail-closed deployments).

`matchedRuleIds` in the response contains every rule that matched, regardless of the winning category, so audit consumers can see all triggers.

<Info>
  **Why DENY beats REVIEW beats ALLOW.** Precedence is fixed and not configurable, on purpose: it removes the "which DENY rule wins?" ambiguity at runtime and makes audit trivial — the response always identifies the strictest action that fired. The cost is that you can't write "ALLOW rules that override DENYs"; if you need that pattern, the right answer is to make the DENY rule more specific instead.
</Info>

<Note>
  Tracer returns decisions; it does not block transactions directly. Your system receives the decision and is responsible for taking the appropriate action (e.g., blocking, allowing, or queuing for review).
</Note>

***

## Core concepts

***

Before creating rules, understand the foundational elements.

### Rules

A rule is a unit of business logic composed of:

* **Expression** - A type-safe expression that evaluates to true or false
* **Action** - What decision to return when the expression is true
* **Scopes** - Which transactions the rule applies to
* **Status** - The rule's lifecycle state

### Expressions

Expressions are written in **CEL (Common Expression Language)**, a type-safe language that evaluates transaction context and returns a boolean value (true or false). CEL provides compile-time validation, so syntax errors are caught when you create the rule—not when transactions are being processed.

Example expressions:

```
amount > 10000
```

```
segment.segmentId == "high-risk-segment-uuid" && amount > 5000
```

```
merchant["category"] == "7995"
```

(`merchant.category` is the 4-digit ISO 18245 MCC code — `"7995"` is the MCC for betting/casino. Both `merchant.category` and `merchant["category"]` are accepted; the production examples use bracket notation by convention. If you need to match on a string label like `"gambling"`, store it in `metadata` and match on that instead.)

Expressions have access to the full validation request context. The most commonly used variables are listed below. For the complete catalog of every nested field type and edge case (UUID format, enum values, `omitempty` behavior), see the [ValidationRequest schema](/en/openapi/v3-current/tracer.yaml) in the API reference.

| Variable            | Type        | What you typically match on                                                     |
| ------------------- | ----------- | ------------------------------------------------------------------------------- |
| `amount`            | number      | Decimal value converted to `float64` for CEL evaluation (max `±2^53`).          |
| `transactionType`   | string      | One of `CARD`, `WIRE`, `PIX`, `CRYPTO`.                                         |
| `subType`           | string      | Free-form (e.g., `"international"`, `"debit"`). Empty string when not provided. |
| `currency`          | string      | ISO 4217 code (e.g., `"BRL"`).                                                  |
| `account.status`    | string      | One of `active`, `suspended`, `closed`.                                         |
| `merchant.category` | string      | ISO 18245 MCC code, 4 digits.                                                   |
| `merchant.country`  | string      | ISO 3166-1 alpha-2 (e.g., `"BR"`).                                              |
| `segment.segmentId` | string UUID | Segment scope of the transaction.                                               |
| `metadata.<key>`    | any         | Custom fields your integration passes in the request payload.                   |

<Note>
  `segmentId` and `portfolioId` live on the top-level `segment` and `portfolio` variables, **not** on `account`. To match by segment, use `segment.segmentId == "..."`, not `account.segmentId == "..."`. The `segment`, `portfolio`, and `merchant` maps are only present when the request includes them — guard with `has(segment)` if your rule must run even when the request doesn't carry one.
</Note>

<Note>
  Expression evaluation is bounded by `CEL_COST_LIMIT` (default `10000`). Expressions exceeding this cost are rejected at activation time with error code `0342` (cost limit exceeded); expression type errors surface as `0341`.
</Note>

### Expression examples by use case

Here are practical examples organized by business scenario:

#### Amount-based rules

```cel theme={null}
// Block transactions above a threshold
amount > 10000

// Block high-value international transfers
transactionType == "WIRE" && subType == "international" && amount > 50000

// Review large cryptocurrency transactions
transactionType == "CRYPTO" && amount > 5000
```

#### Merchant-based rules

```cel theme={null}
// Block gambling merchants
merchant.category == "7995"

// Block high-risk merchant categories
merchant.category in ["7995", "5967", "5966"]

// Review transactions from new merchant countries
merchant.country != "BR" && amount > 1000
```

#### Account-based rules

```cel theme={null}
// Block suspended accounts
account.status == "suspended"

// Review transactions from newly created accounts
metadata.accountAgeDays < 30 && amount > 500

// Block closed accounts
account.status == "closed"
```

#### Combined conditions

```cel theme={null}
// High-value transaction from high-risk segment
segment.segmentId == "high-risk-segment-uuid" && amount > 5000

// International Pix above threshold
transactionType == "PIX" && subType == "international" && amount > 10000

// Large card transaction to foreign merchant
transactionType == "CARD" && merchant.country != "BR" && amount > 3000
```

#### Using metadata

```cel theme={null}
// Block transactions from untrusted devices
metadata.deviceTrust == "untrusted"

// Review first-time purchases above threshold
metadata.isFirstPurchase == true && amount > 1000

// Block transactions outside business hours (using metadata)
metadata.isBusinessHours == false && amount > 5000

// VIP customers bypass certain restrictions
metadata.customerTier == "vip" && amount < 50000
```

<Note>
  Metadata fields are provided by your integration. Design your payload to include the context your rules need.
</Note>

### Actions

Actions determine the decision when an expression evaluates to true:

| Action   | Description            |
| -------- | ---------------------- |
| `ALLOW`  | Allow the transaction  |
| `DENY`   | Deny the transaction   |
| `REVIEW` | Route to manual review |

### Scopes

Scopes define which transactions a rule applies to. A rule with no `scopes` is **global** and evaluates against every transaction. A rule with one or more scope objects evaluates only when the transaction matches at least one of them (OR semantics across scope objects).

Within a single scope object, the supported fields are:

* `segmentId` - Match transactions from a specific segment
* `portfolioId` - Match transactions from a specific portfolio
* `accountId` - Match transactions from a specific account
* `merchantId` - Match transactions to a specific merchant
* `transactionType` - Match specific transaction types (CARD, WIRE, Pix, CRYPTO)
* `subType` - Match specific subtypes (debit, credit, instant, etc.)

**Matching semantics:**

* **Within one scope object:** fields combine with AND. A field that is not specified is treated as a wildcard (matches any value). At least one field must be set — empty scope objects (`{}`) are rejected with error code `0358`.
* **Across multiple scope objects on the same rule:** they combine with OR. The rule matches if **any** scope object matches the transaction.

For example, a rule with two scopes — one targeting `transactionType: CARD` and another targeting `transactionType: Pix` — runs for both card and Pix transactions. A single scope with both `segmentId` AND `accountId` requires the transaction to match the segment AND the account.

***

## Rule lifecycle

***

Rules progress through a defined lifecycle to ensure safe deployment.

<Frame caption="Figure 2. Rules lifecycle and status transitions">
  <img src="https://mintcdn.com/lerian-49cb71fc/SEOef3JqTInYAAau/images/en/d2/rules-limits-lifecycle-tracer.svg?fit=max&auto=format&n=SEOef3JqTInYAAau&q=85&s=0e9996cc0609ad9f3b8d0ee10feb3a56" alt="Lifecycle of rules and limits in Tracer, showing the status transitions a definition moves through from creation to active enforcement" width="531" height="1050" data-path="images/en/d2/rules-limits-lifecycle-tracer.svg" />
</Frame>

### States

| State      | Description                                                                                                                                                                              |
| ---------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `DRAFT`    | Not evaluated; expression can be modified freely                                                                                                                                         |
| `ACTIVE`   | Evaluated during validations; expression is immutable                                                                                                                                    |
| `INACTIVE` | Not evaluated; preserved for audit trail; can be reactivated. The expression is still immutable in this state — to edit it, move the rule back to DRAFT via `POST /v1/rules/{id}/draft`. |
| `DELETED`  | Soft-deleted; not returned by listings and cannot be recovered through the API, but the row is preserved in the database for audit trail.                                                |

### Transitions

| Transition   | From            | To       | Description                                               |
| ------------ | --------------- | -------- | --------------------------------------------------------- |
| `activate`   | DRAFT, INACTIVE | ACTIVE   | Start evaluation (validates expression)                   |
| `deactivate` | ACTIVE          | INACTIVE | Stop evaluation                                           |
| `draft`      | INACTIVE        | DRAFT    | Re-edit a previously deactivated rule before reactivating |
| `delete`     | DRAFT, INACTIVE | DELETED  | Permanent removal (cannot delete ACTIVE rules)            |

<Note>
  Active rules must be deactivated before deletion. This prevents accidental removal of rules that are currently being evaluated.
</Note>

***

## Create a rule

***

Create rules using `POST /v1/rules`. Rules are created in `DRAFT` status by default.

A rule requires:

* **name**: A unique, descriptive name
* **expression**: A CEL expression that evaluates to true or false
* **action**: The decision to return when the expression matches (ALLOW, DENY, or REVIEW)
* **scopes** (optional): Limit which transactions the rule applies to

For complete payload structure and field details, see the [API reference](/en/reference/tracer/create-rule).

***

## Activate and deactivate rules

***

After creating a rule, activate it to start evaluation. Deactivate rules to stop evaluation without deleting them.

| Operation  | Endpoint                         | Description                           |
| ---------- | -------------------------------- | ------------------------------------- |
| Activate   | `POST /v1/rules/{id}/activate`   | Start evaluating this rule            |
| Deactivate | `POST /v1/rules/{id}/deactivate` | Stop evaluating (preserves for audit) |

<Note>
  Deactivating a rule preserves it for audit purposes. Use delete only when you want to permanently remove a rule.
</Note>

***

## List and query rules

***

Query rules for management and auditing using `GET /v1/rules`.

### Query parameters

| Parameter          | Type    | Description                                                                        |
| ------------------ | ------- | ---------------------------------------------------------------------------------- |
| `name`             | string  | Filter by name (case-insensitive partial match)                                    |
| `status`           | string  | Filter by status (DRAFT, ACTIVE, INACTIVE). `DELETED` is not a valid filter value. |
| `action`           | string  | Filter by action (ALLOW, DENY, REVIEW)                                             |
| `account_id`       | UUID    | Filter by scope: account ID                                                        |
| `segment_id`       | UUID    | Filter by scope: segment ID                                                        |
| `portfolio_id`     | UUID    | Filter by scope: portfolio ID                                                      |
| `merchant_id`      | UUID    | Filter by scope: merchant ID                                                       |
| `transaction_type` | string  | Filter by scope: transaction type (CARD, WIRE, Pix, CRYPTO)                        |
| `sub_type`         | string  | Filter by scope: subtype (e.g., debit, credit)                                     |
| `limit`            | integer | Items per page (default: 10, max: 100)                                             |
| `cursor`           | string  | Pagination cursor from previous response                                           |
| `sort_by`          | string  | Sort field: `created_at`, `updated_at`, `name`, `status` (default: `created_at`)   |
| `sort_order`       | string  | Sort direction: `ASC`, `DESC` (default: `DESC`)                                    |

### Get a specific rule

Use `GET /v1/rules/{id}` to retrieve the full rule definition including expression and scopes.

***

## Update a rule

***

Update rules using `PATCH /v1/rules/{id}`. Rules can be updated in any status, with one important restriction:

<Warning>
  The `expression` field is immutable in **ACTIVE** and **INACTIVE** states — deactivating a rule is not enough. To edit an expression, move the rule from ACTIVE → INACTIVE (`POST /v1/rules/{id}/deactivate`), then from INACTIVE → DRAFT (`POST /v1/rules/{id}/draft`). Only DRAFT rules accept expression updates. Once edited, reactivate with `POST /v1/rules/{id}/activate`.
</Warning>

***

## Delete a rule

***

Delete rules that are no longer needed. Only DRAFT and INACTIVE rules can be deleted. ACTIVE rules must be deactivated first.

```http theme={null}
DELETE /v1/rules/{id}
X-API-Key: {api_key}
```

<Warning>
  Deletion is permanent. Deleted rules cannot be recovered and do not appear in any listings.
</Warning>

***

## Best practices

***

Follow these practices for effective, maintainable rules.

### Naming

* **Use descriptive names** - The name should clearly state what the rule does
* **Include context** - Mention the scenario or transaction type
* **Avoid abbreviations** - Prefer clarity over brevity

| Less clear   | More clear                                 |
| ------------ | ------------------------------------------ |
| `Rule 1`     | `Block night transactions above BRL 5,000` |
| `Block high` | `Deny high-value weekend transactions`     |
| `Pix rule`   | `Review Pix transfers to new recipients`   |

### Expression design

* **Keep expressions simple** - Complex logic is harder to maintain
* **Use scopes for filtering** - Don't repeat scope conditions in expressions
* **Test edge cases** - Consider boundary values and null fields

### Lifecycle management

* **Start in DRAFT** - Test before activating
* **Return to DRAFT before editing the expression** - The expression is immutable in ACTIVE and INACTIVE; move the rule to DRAFT via `POST /v1/rules/{id}/draft` to edit, then reactivate
* **Archive unused rules** - Keep audit trail intact
* **Delete only when certain** - Deletion is permanent

### Monitoring

* **Review matched rules** - Check which rules are triggering
* **Monitor DENY rates** - High deny rates may indicate overly aggressive rules
* **Audit regularly** - Ensure rules still align with business requirements

<Warning>
  **Common pitfalls when working with rules:**

  * **"I edited the expression but the change didn't take effect."** The expression is immutable in ACTIVE and INACTIVE states. Move the rule back to DRAFT via `POST /v1/rules/{id}/draft`, edit, then reactivate. INACTIVE alone is not enough.
  * **"My rule is ACTIVE but Tracer isn't evaluating it yet."** The rule cache refreshes every \~10 seconds (`RULE_SYNC_POLL_INTERVAL_SECONDS`). Newly activated rules take up to that window to start running. Plan integration tests around this delay.
  * **"I want to delete an ACTIVE rule."** You can't — `POST /v1/rules/{id}/deactivate` first, then `DELETE /v1/rules/{id}`. This forces a visible step where the rule stops affecting traffic before it disappears from listings.
  * **"My empty scope `{}` is being rejected with error code `0358`."** Every scope object must have at least one field set. To run a rule globally (against every transaction), omit the `scopes` array entirely — don't pass `{}`.
</Warning>

***

## Quick reference

***

Key endpoints, actions, and status information.

### Endpoints

| Operation       | Method | Endpoint                    |
| --------------- | ------ | --------------------------- |
| Create rule     | POST   | `/v1/rules`                 |
| List rules      | GET    | `/v1/rules`                 |
| Get rule        | GET    | `/v1/rules/{id}`            |
| Update rule     | PATCH  | `/v1/rules/{id}`            |
| Delete rule     | DELETE | `/v1/rules/{id}`            |
| Activate rule   | POST   | `/v1/rules/{id}/activate`   |
| Deactivate rule | POST   | `/v1/rules/{id}/deactivate` |
| Draft rule      | POST   | `/v1/rules/{id}/draft`      |

### Statuses

| Status     | Evaluated | Editable                                              | Can delete            |
| ---------- | --------- | ----------------------------------------------------- | --------------------- |
| `DRAFT`    | No        | Yes                                                   | Yes                   |
| `ACTIVE`   | Yes       | Partial (expression immutable)                        | No (deactivate first) |
| `INACTIVE` | No        | Partial (expression immutable; return to DRAFT first) | Yes                   |
| `DELETED`  | No        | No                                                    | N/A                   |
