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

# Guardrails

> Create and manage guardrail rules that enforce business constraints on decisions.

Guardrail rules are tenant-global business constraints that the Recommend
engine **enforces** on every decision. Unlike decisioning gates (which are
attached to individual offers) and contact policies (which throttle frequency),
guardrails apply across the whole candidate set. See
[Enforcement at decision time](#enforcement-at-decision-time) for the runtime
semantics.

## POST /api/v1/guardrails

Create a guardrail rule.

### Request Body

| Field           | Type   | Required | Description                                                    |
| --------------- | ------ | -------- | -------------------------------------------------------------- |
| `key`           | string | Yes      | Unique guardrail key                                           |
| `name`          | string | Yes      | Guardrail name                                                 |
| `description`   | string | No       | Description of the constraint                                  |
| `severity`      | string | No       | `"hard"` (blocks decision, default) or `"soft"` (warns only)   |
| `expressionAst` | object | No       | Expression AST defining the guardrail condition. Default `{}`. |
| `status`        | string | No       | `"draft"`, `"active"` (default), `"paused"`, or `"archived"`   |

### Example

```bash theme={null}
curl -X POST https://playground.kaireonai.com/api/v1/guardrails \
  -H "Content-Type: application/json" \
  -H "X-Tenant-Id: my-tenant" \
  -d '{
    "key": "max-daily-spend",
    "name": "Maximum Daily Spend Guard",
    "description": "Prevents decisions that would exceed daily budget allocation",
    "severity": "hard",
    "status": "active"
  }'
```

### Response

```json theme={null}
{
  "id": "gr_abc123",
  "key": "max-daily-spend",
  "name": "Maximum Daily Spend Guard",
  "description": "Prevents decisions that would exceed daily budget allocation",
  "severity": "hard",
  "expressionAst": {},
  "status": "active",
  "rowVersion": 1,
  "tenantId": "my-tenant",
  "createdAt": "2026-03-17T10:00:00Z",
  "updatedAt": "2026-03-17T10:00:00Z"
}
```

***

## GET /api/v1/guardrails

List all guardrails for the tenant.

### Response

Returns an array of guardrail objects.

***

## PUT /api/v1/guardrails

Update a guardrail by key. Supports optimistic concurrency via `rowVersion`.

### Request Body

| Field           | Type    | Required | Description                                                                                               |
| --------------- | ------- | -------- | --------------------------------------------------------------------------------------------------------- |
| `key`           | string  | Yes      | Guardrail key to update                                                                                   |
| `name`          | string  | No       | Updated name                                                                                              |
| `description`   | string  | No       | Updated description                                                                                       |
| `type`          | string  | No       | Guardrail type                                                                                            |
| `config`        | object  | No       | Type-specific configuration                                                                               |
| `conditions`    | any     | No       | Condition definitions                                                                                     |
| `priority`      | integer | No       | Evaluation priority (0+)                                                                                  |
| `severity`      | string  | No       | `"hard"` or `"soft"`                                                                                      |
| `expressionAst` | object  | No       | Updated expression AST                                                                                    |
| `status`        | string  | No       | `"draft"`, `"active"`, `"paused"`, or `"archived"`                                                        |
| `scope`         | string  | No       | Guardrail scope                                                                                           |
| `rowVersion`    | integer | No       | Optimistic concurrency check. If provided and does not match the current version, returns `409 Conflict`. |

***

## DELETE /api/v1/guardrails

Soft-delete a guardrail by key.

### Query Parameters

| Parameter | Type   | Required | Description             |
| --------- | ------ | -------- | ----------------------- |
| `key`     | string | Yes      | Guardrail key to delete |

### Response `200`

```json theme={null}
{ "success": true }
```

***

## Enforcement at decision time

Active guardrails run during `POST /api/v1/recommend`. The stage executes
**once per request, at rank-node entry** — so ranking and the result `limit`
operate on the survivors, and expressions can reference `offer.score`. Flows
without a rank node fall back to running the stage at the response node.
Guardrails are independent of contact policies: setting `skipContactPolicy`
on a flow does **not** skip guardrails.

### Expression semantics

The `expressionAst` describes **when the rule fires** — i.e. when the
candidate matches the constraint the guardrail is guarding against. A leaf
condition is `{ field, operator, value }`; conditions combine with `all` (AND),
`any` (OR), and `not`.

| Field path   | Resolves to                                                                |
| ------------ | -------------------------------------------------------------------------- |
| `offer.*`    | `id`, `name`, `category`, `priority`, `score`, plus the offer's `metadata` |
| `customer.*` | Request-time `attributes` merged with enriched customer data               |
| `channel.*`  | `type`, `id`, `name` of the candidate's channel                            |

Operators: `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `in`, `not_in`, `contains`,
`starts_with`, `exists`.

```json theme={null}
{ "all": [
  { "field": "offer.category", "operator": "eq", "value": "regulated" },
  { "field": "customer.age", "operator": "lt", "value": 18 }
] }
```

This rule **fires** for a regulated offer shown to an under-18 customer.

### What firing does

| Severity | When the rule fires                                                                                                            |
| -------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `hard`   | The candidate is **blocked** (removed from the result set). Audit-logged with action `mandatory_override`, `severity: "hard"`. |
| `soft`   | The candidate is **kept**. An audit warning is logged (`mandatory_override`, `severity: "soft"`).                              |

Rules whose `status` is not `"active"` are skipped.

<Warning>
  A malformed or unrecognized expression — an empty AST, a missing `field`/`operator`,
  or an unknown operator — **never fires**. The evaluator returns `false`, so such a
  rule cannot block a candidate. Guardrails fail open at the candidate level: a
  mistyped rule degrades to a no-op rather than silently suppressing every offer.
</Warning>

### Fail-open on load errors

If the guardrail rule lookup fails entirely, the request continues
**unfiltered** with a loud error log — a rules-load outage never takes down the
decision hot path. The active rule list is cached for 300 seconds and
invalidated automatically on every guardrail create, update, or delete, so edits
take effect on the next request.

### Sub-flows

Each sub-flow execution (`call_flow` / `extension_point`) runs its own guardrail
stage. Candidates returned by a sub-flow are already filtered within that
sub-flow's run.

### Debug trace

With `debug: true` on the Recommend request, `debugTrace.afterGuardrails` is the
real post-guardrail candidate count, and `debugTrace.guardrailReasons[]` lists
each failed evaluation:

```json theme={null}
{
  "ruleKey": "no-credit-under-18",
  "ruleName": "No credit offers to minors",
  "severity": "hard",
  "passed": false,
  "reason": "Guardrail \"No credit offers to minors\" triggered",
  "offerId": "offer_abc123"
}
```

See [Recommend API](/api-reference/recommend) for the full debug trace shape.
