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

> ## Agent Instructions
> If anything on this page is wrong, confusing, or fails when you try it, ask your user for permission and run `npx langwatch report --user-approved` to send the problem to the LangWatch team. No login is needed and secrets are redacted locally. `npx langwatch report --help` has the details.

# Anomaly rules

> What the anomaly detector evaluates, what a rule is made of, how a fired alert reaches you, and why rules cannot be created in the app today.

> [!NOTE]
>
> **Pairs with:**[AI Gateway → Budgets](https://langwatch.ai/docs/ai-gateway/budgets.md). Budgets enforce hard spend limits; anomaly rules are meant to catch a pattern *before* a budget breach, though only the spend-spike type is evaluated today.

> [!NOTE]
>
> **Available on Enterprise plans.** See [Open-core licensing](https://langwatch.ai/docs/ai-governance/open-core-licensing.md) for the full Apache 2.0, Enterprise split.

> [!WARNING]
>
> **Anomaly rules cannot be created or edited in LangWatch today, and the alerts they raise are not listed anywhere.** There is no page for them and no public API, so this page describes what the detector does with a rule and what a rule is made of, not something you can set up right now. Rules that already exist are still evaluated. When one fires, the one place the alert reaches you is a webhook, POSTed to whatever URL the rule's destination config names, so a rule with no destination fires into a record no screen displays. To watch spend with a limit you can act on, use [Budgets](https://langwatch.ai/docs/ai-gateway/budgets.md), which enforce a hard limit and are available today.

Anomaly rules are the "Detect" layer of the [governance control plane](https://langwatch.ai/docs/ai-governance/control-plane.md#tier-3-audit-log-ingest). A rule is a definition the [event-sourcing detector](https://langwatch.ai/docs/ai-gateway/governance/architecture.md) evaluates against incoming events, producing an `AnomalyAlert` row when a violation is detected.

This page documents what the detector evaluates and what is preview-only, so that a rule which looks "active" is not mistaken for one that fires.

## Rule-type coverage

The schema holds every rule type below; the detector evaluates one of them today.

| Rule type | Detector coverage | What it detects | Threshold config keys |
| - | - | - | - |
| **`spend_spike`** | ✅ Live | Cost-USD growth in a rolling window vs a baseline window | `windowSec`, `baselineOffsetSec`, `ratioVsBaseline`, `minBaselineUsd` |
| `rate_limit` | ⏳ Preview | Volume of actions per actor per window | (config schema TBD) |
| `tool_mismatch` | ⏳ Preview | Tool invocation outside the policy\_rules.tools.allow set | (config schema TBD) |
| `after_hours` | ⏳ Preview | Activity outside org-configured business-hours window | (config schema TBD) |
| `unusual_model` | ⏳ Preview | Model used for actor that's not on their typical short-list | (config schema TBD) |
| `pii_leak` | ⏳ Preview | Outbound payload matches a PII regex set | (config schema TBD) |
| `custom` | ⏳ Preview | User-defined CEL, SQL predicate | (config schema TBD) |

A **⏳ Preview** rule type is persisted and reads as `active`, but the detector logs `debug` and skips it, so no alert is produced. Until the detector slice for each ships, treat such a rule as stored for later rather than as watching anything.

If you need detection beyond `spend_spike` today, the recommended shape is one of:

* Stand up Prometheus + Grafana against the gateway metrics endpoint, see [Cookbook · Prometheus alerts](https://langwatch.ai/docs/ai-gateway/cookbooks/prometheus-alerts.md) for the alert ruleset.
* Hook your SIEM to the OTel trace stream LangWatch already emits per project.

## What a `spend_spike` rule is made of

These are the fields a rule carries and the detector reads. No screen collects them today and no public API accepts them; they describe rules that already exist.

| Field | Typical value | What it does |
| - | - | - |
| **Name** | `Spend spike on production gateway` | Free-text label; identifies the rule on the alert it raises, and travels in the webhook payload. |
| **Severity** | `warning`, `critical` | Recorded on the alert and carried in the webhook payload. It does not route: when the rule's destination configuration passes validation, every destination on it receives every alert the rule raises. A configuration that fails validation is quarantined and no request is sent to any of its destinations. |
| **Rule type** | `spend_spike` | Pick exactly this, see coverage table above. |
| **Scope** | `organization`, `source_type`, `source` | Filters which IngestionSources the rule applies to (see Scope coverage below). |
| **Scope ID** | (cuid) | Required when scope is `source_type` or `source`. The IngestionSource ID is in the URL of the per-source detail page under the inventory. |
| **Threshold config** | (JSON object, see below) | The detector's per-rule-type tuning knobs. |
| **Destination config** | `{"destinations": []}` | The webhook targets the alert is pushed to, see Dispatch coverage below. Empty for a rule that records alerts without pushing them anywhere. |

### Threshold config: `spend_spike`

```json theme={null}
{
  "windowSec": 86400,
  "baselineOffsetSec": 604800,
  "ratioVsBaseline": 2.0,
  "minBaselineUsd": 1.0
}
```

| Key | Default | Meaning |
| - | - | - |
| `windowSec` | `86400` (24 h) | The "current" window the detector sums spend across, ending at the latest event timestamp. |
| `baselineOffsetSec` | `604800` (7 d) | How far back from the current window the detector pulls the baseline window from. With the defaults, the baseline is the same-shape 24-hour window from 7 days ago. |
| `ratioVsBaseline` | `2.0` | Multiplier, the detector fires when current spend ≥ baseline × ratio. `2.0` means "double the spend of last week's same-window." |
| `minBaselineUsd` | `1.0` | Floor, the detector skips evaluation when the baseline window is below this value. Prevents trivial alerts on cold sources where last week's spend was \~\$0.01 and a 2× rise is meaningless. |

Sensible starting values:

| Goal | `windowSec` | `baselineOffsetSec` | `ratioVsBaseline` | `minBaselineUsd` |
| - | - | - | - | - |
| Day-over-week catch (default) | 86400 | 604800 | 2.0 | 1.0 |
| Hour-over-day catch (tighter) | 3600 | 86400 | 3.0 | 0.10 |
| Week-over-week catch (looser) | 604800 | 2592000 | 1.5 | 5.0 |

## Scope coverage

| Scope | Detector coverage | Notes |
| - | - | - |
| `organization` | ✅ Evaluated | Fires on any IngestionSource in the org. Best default. |
| `source_type` | ✅ Evaluated | Fires on every source whose `sourceType` matches (e.g. all `otel_generic` sources). |
| `source` | ✅ Evaluated | Fires on a single named IngestionSource. |
| `team` | ⏳ Preview | Persisted but not filtered by the detector today. Don't rely on it. |
| `project` | ⏳ Preview | Same. |

## Dispatch coverage

A rule's **Destination config** lists where to push the alert. **Webhook is the only destination type**, and it works today: when a rule fires, the alert row is written first, then POSTed to each destination on the rule.

```json theme={null}
{
  "destinations": [
    {
      "type": "webhook",
      "url": "https://your-siem.example.com/ingest",
      "sharedSecret": "..."
    }
  ]
}
```

| Field | Required | Meaning |
| - | - | - |
| `type` | Yes | Must be `webhook`. No other destination type exists; Slack, PagerDuty and email are not implemented. |
| `url` | Yes | An absolute `https` URL. Up to 10 destinations per rule. |
| `sharedSecret` | No | When set, the request body is signed with HMAC-SHA256 and the signature travels as `X-LangWatch-Signature: sha256=<hex>`, so your receiver can verify the request came from LangWatch. |

For Slack or PagerDuty, point a webhook at their own incoming-webhook or events endpoint and reshape the payload on your side.

Leave `destinations` empty for a rule that records alerts without pushing them. A destination config that fails validation is quarantined: no request is sent, and the rule falls back to recording only.

### What the webhook receives

A POST with `Content-Type: application/json`, carrying the rule and the alert it raised:

```json theme={null}
{
  "ruleId": "...",
  "ruleName": "Spend spike on production gateway",
  "ruleType": "spend_spike",
  "severity": "warning",
  "organizationId": "...",
  "alert": {
    "id": "...",
    "triggerWindowStartIso": "2026-01-08T00:00:00.000Z",
    "triggerWindowEndIso": "2026-01-09T00:00:00.000Z",
    "triggerSpendUsd": "42.00",
    "triggerEventCount": null,
    "detail": {
      "baselineSpendUsd": 12,
      "windowSec": 86400,
      "reason": "...",
      "dispatch": "pending"
    },
    "detectedAtIso": "2026-01-09T00:04:11.000Z"
  }
}
```

`detail.dispatch` reads `pending` in every payload, because the alert is written before delivery is attempted. The stored row is updated with the delivery outcome afterwards; the payload you receive is the pre-delivery snapshot.

### Delivery guarantees

Delivery is **best-effort, and the alert row is the authoritative signal**. Each destination gets up to three attempts with a five-second timeout each. A 5xx response is retried with exponential backoff; a 4xx fails immediately, on the reading that it is a configuration error rather than a transient one.

A destination that never succeeds is logged and recorded on the alert. It does not fail the detector, and it does not block the other destinations.

Requests go out through the same server-side-request-forgery guard the ingestion pullers use: the destination is resolved and its address pinned.

A URL resolving to a private or link-local address is refused and recorded as a failed delivery when `BLOCK_LOCAL_HTTP_CALLS` is on, which it is on LangWatch Cloud. Self-hosted, that refusal only applies if you set the flag, and a host listed in `ALLOWED_PROXY_HOSTS` is allowed either way.

The cloud metadata hostnames and addresses themselves are refused regardless of either setting and cannot be allow-listed. An allow-listed name is trusted as written, without a lookup of where it resolves, so only allow-list names you control.

## Where an existing rule's alerts go

**In the app**: no screen lists them. Each fire is recorded as one alert keyed by `(ruleId, triggerWindowStart)`, so the detector does not raise a second alert for the same window, but the governance section has no page that reads those rows today.

**Over a webhook**: this is the one place an alert reaches you. Every fire is POSTed to each webhook destination on the rule, with the payload above. A rule with no destinations records the alert and sends no request.

**Not in the OCSF export.** The [OCSF export](https://langwatch.ai/docs/ai-gateway/governance/ocsf-export.md) carries governance events, and no exported record is linked to an anomaly alert. Do not build a SIEM rule that waits for one. Point a webhook destination at your SIEM instead.

**CLI**: `langwatch governance status` reports `hasAnomalyRules: true` once at least one rule exists. That confirms a rule is defined, not that one has fired.

End to end, for an organization that already has a rule:

1. Start from a `spend_spike` rule scoped to your test IngestionSource with `minBaselineUsd: 0.001` (tiny floor) and `ratioVsBaseline: 1.5`.
2. Send one OTel event with `gen_ai.usage.cost_usd: 0.05` (baseline seed), see [otel-generic, Test it now](https://langwatch.ai/docs/ai-governance/ingestion-sources/otel-generic.md#test-it-now-copy-paste-curl).
3. Wait long enough for the baseline window to roll past the seed (default `windowSec: 86400` means 24h, so use the tighter values from the table above for fast iteration).
4. Send a second event with `gen_ai.usage.cost_usd: 2.00` (the spike).
5. Watch your webhook receiver for the payload above. If the rule has no webhook destination, the alert is recorded and no request is sent.

## What's persisted vs what's evaluated

A rule that is persisted but unevaluated is intentional: a rule can wait for the detector slice that will read it, so there is no rework when that slice ships. But it's a real failure mode if you don't know which is which. The two coverage tables above are the source of truth: **only `spend_spike` + (`organization` | `source_type` | `source`) actually fires today**. Everything else is preview state.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.