---
title: Risk engine
description: Thirteen named signals, weighted into an explainable score that policy can test — with no model you cannot interrogate.
label: Risk
---

Risk runs as stage 8 of the pipeline, before approval, because a policy rule can
test the risk score and the signals that produced it. The output is a level, a
score from 0 to 100, and the list of signals that fired with their individual
contributions.

:::security Explainable by construction
There is no opaque model here. Every score decomposes into named signals, each
with a weight you set and a human-readable explanation of what it observed.

That is a requirement, not a preference. A refusal an operator cannot explain to
the person it affected is a refusal that gets overridden, and then switched off.
"Blocked, risk score 87" is useless. "Blocked: amount is 4.2× this agent's
trailing median, and the destination has never been contacted before" is
something you can act on.
:::

## The signals

| Signal | Fires when |
| --- | --- |
| `UNUSUAL_AMOUNT` | The amount is far outside this agent's own history |
| `UNUSUAL_DESTINATION` | The destination is rare or newly seen for this agent |
| `VELOCITY_ANOMALY` | Action rate is well above the agent's baseline |
| `TIME_ANOMALY` | Activity at an hour this agent does not normally act |
| `NEW_TOOL` | First use of this tool by this agent |
| `NEW_RESOURCE` | First touch of this resource |
| `NEW_RECIPIENT` | First time acting against this recipient |
| `IDENTITY_ANOMALY` | Something inconsistent about the identity presentation |
| `CREDENTIAL_ANOMALY` | Unusual credential use — stale, over-leased, unexpected scope |
| `DELEGATION_ANOMALY` | An unusual chain: unexpected depth, or a path not seen before |
| `POLICY_VIOLATION` | Recent refusals by this agent |
| `TRUST_EVIDENCE` | Required trust evidence is missing, expiring or expired |
| `BEHAVIORAL_DEVIATION` | The overall pattern departs from the agent's established behaviour |

Several of these are *baseline* signals — they compare against what this agent
has done before, not against a global average. A finance agent that moves large
amounts all day has a high baseline and does not trip `UNUSUAL_AMOUNT`; a
read-only agent that suddenly moves one does.

That also means a brand-new agent trips the "new" signals on almost everything
it does. That is correct behaviour and worth planning for: run new agents in
`OBSERVE` or `SIMULATE` long enough to establish a baseline before promoting.

## What a risk assessment returns

```json
{
  "level": "HIGH",
  "score": 72,
  "signals": [
    {
      "type": "UNUSUAL_AMOUNT",
      "weight": 30,
      "explanation": "Amount 120000 is 4.2× this agent's trailing median of 28500."
    },
    {
      "type": "UNUSUAL_DESTINATION",
      "weight": 22,
      "explanation": "payments-eu.partner.example.com has not been contacted by this agent before."
    },
    {
      "type": "TIME_ANOMALY",
      "weight": 20,
      "explanation": "03:14 UTC is outside this agent's usual 08:00–19:00 activity."
    }
  ]
}
```

On a decision, `explanation.risk.top_signals` carries the three
highest-weighted explanations, so the reason a request was escalated is in the
response the agent already received.

## Using risk in policy

This is the point of computing it.

```json
{
  "id": "rule-high-risk-approval",
  "name": "High risk needs a human",
  "priority": 600,
  "enabled": true,
  "when": {
    "kind": "condition",
    "id": "c-risk-score",
    "field": "risk.score",
    "operator": "gte",
    "value": 70
  },
  "then": {
    "decision": "REQUIRE_APPROVAL",
    "reason": "Risk score is at or above the review threshold.",
    "approval": { "routingMode": "SINGLE", "requiredApprovals": 1, "ttlSeconds": 900 }
  }
}
```

Testing a specific signal is often better than testing the score, because it
says what you actually mean:

```json
{
  "id": "rule-new-destination-large-amount",
  "name": "Block large amounts to unseen destinations",
  "priority": 820,
  "enabled": true,
  "when": {
    "kind": "group",
    "id": "g-new-dest-large",
    "operator": "and",
    "children": [
      {
        "kind": "condition",
        "id": "c-signal-new-dest",
        "field": "risk.signals",
        "operator": "contains",
        "value": "UNUSUAL_DESTINATION"
      },
      { "kind": "condition", "id": "c-large", "field": "amount", "operator": "gt", "value": 50000 }
    ]
  },
  "then": {
    "decision": "BLOCK",
    "reason": "Large amounts to a destination this agent has never used are refused."
  }
}
```

Three fields are available: `risk.level`, `risk.score` and `risk.signals`. The
velocity and history counters the signals are built from are exposed separately
too — `velocity.actions_last_minute`, `history.tool_used_before` and the rest —
so you can write a rule on the raw observation rather than on the derived
signal when that is clearer.

## Tuning

```http
PUT /v1/risk/configuration
```

Signal weights and level thresholds are yours. Tuning is an audited change
requiring `risk:configure`.

Approach it from the evidence rather than from intuition:

1. Run in `OBSERVE` or `SIMULATE` and let real traffic accumulate.
2. Open the Risk Center and look at the score distribution and the signal breakdown.
3. A signal firing on nearly everything is not discriminating — lower its weight or fix the underlying baseline.
4. A signal that never fires is either weighted too low or genuinely not relevant to your traffic.
5. Set thresholds where the distribution has a natural gap.

:::warning Raising a threshold is a security change
Moving the `HIGH` threshold from 70 to 85 silently removes approval
requirements from every rule that tests it. It is audited for exactly that
reason. Simulate before and after, and look at what stops being escalated.
:::

## Risk events

Every assessment that crosses a threshold writes a risk event with its signal
evidence attached.

```http
GET /v1/risk/events
GET /v1/risk/overview
```

The overview is what the Risk Center renders: counts by level, the signal
breakdown, the highest-risk tools, the destinations in play, policy violations
and credential anomalies. Events can be acknowledged with `risk:acknowledge`, so
"we have looked at this" is recorded rather than remembered.

## Related

:::cards

:::card Approvals | /concepts/approvals
What happens when risk escalates to a human.
:::

:::card Risk Center | /operate/risk-center
Reading the overview and tuning from it.
:::

:::card Trust evidence | /concepts/trust
The input behind the `TRUST_EVIDENCE` signal.
:::

:::card Risk API | /reference/api/risk
Overview, events and configuration.
:::

:::
