📘 Public beta · Endpoints are stable; OpenAPI specs and SDKs ship monthly. See changelog →
Products
AML Platform
Rules & decision policy

Rules & decision policy

The rules engine is the heart of AML monitoring. Rules are declarative conditions that, when satisfied by a screening or transaction, contribute a score and (optionally) take an action.

Anatomy of a rule

{
  "id": "rul_01HXY...",
  "code": "CASH_STRUCT_D7",
  "name": "Cash deposit structuring (7-day window)",
  "description": "More than 3 cash credits in 7 days, each ≥ 80% of cash threshold but < 100%.",
  "lane": "transaction",
  "category": "structuring",
  "type": "velocity",
  "severity": "high",
  "action": "alert",
  "scoreContribution": 45,
  "conditions": {
    "channel": "in:['cash', 'atm_deposit']",
    "direction": "==:credit",
    "amountIdr": "between:[400000000, 499999999]",
    "occurrenceWindow": "rolling:P7D",
    "occurrenceThreshold": ">=:3"
  },
  "status": "active",
  "tags": ["structuring", "fatf-r-10"],
  "version": 3,
  "createdAt": "...",
  "updatedAt": "...",
  "approvedBy": "usr_compliance_lead_...",
  "approvedAt": "..."
}

Field meanings

FieldNotes
codeUnique per org. Use for cross-referencing in your compliance docs.
laneonboarding · transaction · ongoing.
categoryFree-text bucket — structuring, pep, geography, velocity, kyc, custom.
typethreshold · velocity · pattern · lookup.
severitylow · medium · high · critical.
actionalert (score adds to risk) · escalate (force escalate) · block (force escalate + halt).
scoreContributionInteger added to alert's riskScore when this rule fires.
conditionsDSL describing match — see below.
statusdraft · pending_approval · active · paused.

Condition DSL

A condition object maps fields to predicates. Operators:

  • ==:value · !=:value
  • >:n · >=:n · <:n · <=:n · between:[a,b]
  • in:[a,b,c] · not_in:[a,b,c]
  • match:regex (anchored)
  • rolling:ISO-8601-duration (for velocity windows, paired with occurrenceThreshold)

Conditions are AND-joined within a single rule. For OR logic, define multiple rules with the same category.

List + create rules

GET/api/rules
Auth · API keyScope · rule.read

Filters: lane, status, category. Paginated (default 50, max 200).

POST/api/rules
Auth · API keyScope · rule.write

Body is the rule object above (omit ID, timestamps, version, approvedBy, approvedAt). New rules start in draft status and require approval before they go active.

Coverage (before activation)

Rules reference canonical fields like transaction.amountIdr and transaction.isCrossBorder. For a rule to fire correctly in production, every field it references must either be engine-computed (e.g. transaction.isRoundNumber, velocity counters — derived automatically) or covered by at least one active channel in the rule's lane (see Channels).

The coverage endpoint tells you which fields are covered and which aren't:

GET/api/rules/{id}/coverage
Auth · API keyScope · rule.read
{
  "ok": true,
  "data": {
    "ruleId":   "rul_01HXY...",
    "code":     "TX-030",
    "name":     "PER-11 cash threshold",
    "lane":     "transaction",
    "fields": [
      {
        "path":            "transaction.amountIdr",
        "channelAware":    true,
        "canonical":       "transaction.amount_idr",
        "covered":         true,
        "coveredByChannels": [
          { "id": "ch_01HXY...", "name": "Core banking — transfers" }
        ]
      },
      {
        "path":            "transaction.isCrossBorder",
        "channelAware":    true,
        "canonical":       "transaction.is_cross_border",
        "covered":         false,
        "coveredByChannels": [],
        "reason":          "no active channel in lane=transaction maps this canonical field"
      },
      {
        "path":            "transaction.isRoundNumber",
        "channelAware":    false,
        "covered":         true,
        "reason":          "engine-computed — always covered"
      }
    ],
    "allCovered":   false,
    "unmappedRequired": ["transaction.isCrossBorder"]
  }
}

The dashboard's inline rule editor (Rules → ⋯ → Edit) renders the same coverage live as a green "all mapped" pill or amber "X fields unmapped" list with deep-links to the channels that need editing.

Activate without coverage = silent underfire

A rule can be activated even when allCovered is false — the engine doesn't refuse to run. But a missing-mapped field evaluates to false for every event, so the condition never trips. CI tip: gate rule activation on allCovered === true for every rule you flip from pending_approval to active.

Approval workflow

A second outcome from pending_approval is rejected — the rule returns to draft with the rejector's note attached.

POST/api/rules/{id}/approve
Auth · API keyScope · rule.write

The approver must differ from the drafter (configurable four-eyes; AML defaults to required for rules in block action class).

{ "decision": "approve", "notes": "Aligns with PPATK Reg. PER-11 cash threshold." }

Update + status

PATCH/api/rules/{id}
Auth · API keyScope · rule.write

Edits put the rule back into draft and bump the version. Active version keeps running until approval reactivates the new draft. No silent edits.

PATCH/api/rules/{id}/status
Auth · API keyScope · rule.write
{ "status": "paused" }

Use paused for emergency rule-off without deletion.

Temporary bypass

POST/api/rules/{id}/bypass
Auth · API keyScope · rule.write
{
  "until": "2026-05-25T23:59:59Z",
  "reason": "Investigating false-positive surge on counterparty acquisitions during M&A close."
}

Bypasses fire webhook.rule.bypassed events. Max bypass duration is 30 days; auto-expires.

Decision policy

Per-org thresholds that turn raw scores into actions.

GET/api/decision-policy
Auth · API keyScope · read
PATCH/api/decision-policy
Auth · API keyScope · write
{
  "ok": true,
  "data": {
    "onboardingConfirmedHitScore": 80,
    "onboardingReviewScore": 50,
    "txReviewScore": 40,
    "txEscalateScore": 70,
    "txSarScore": 90,
    "cashThresholdIdr": "500000000",
    "structuringTolerancePct": 80,
    "crossBorderPremiumOnly": true,
    "fourEyesOnRuleApprove": true,
    "fourEyesOnSarSubmit": true,
    "fourEyesOnCaseClose": false
  }
}

Tuning thresholds is regulatory-sensitive

Indonesian regulators expect your AML program to document the rationale for any threshold change. Quantum Elixir audit-logs every PATCH /api/decision-policy call with actor + before/after values; export the policy-changes report quarterly from the dashboard for examiner review.

Backtesting

Before promoting a rule from draft to pending_approval, test it against historical data:

POST /api/rules/{id}/backtest

{
  "lookbackDays": 90,
  "scope": "all | sample",
  "sampleSize": 10000
}

Returns:

{
  "ok": true,
  "data": {
    "totalEvaluated": 387412,
    "wouldHaveFired": 1872,
    "byLane": { "onboarding": 134, "transaction": 1738 },
    "estimatedFalsePositiveRate": 0.94,
    "topMatchingCustomers": [...]
  }
}

Use the false-positive estimate to right-size scoreContribution before activating the rule in production traffic.

Audit & versions

Every rule keeps a version history queryable via GET /api/rules/{id}/versions. Use this to demonstrate to examiners exactly what was active during any past time window.