Guardrail rules are tenant-global business constraints that the Recommend
engine enforces on every decision. Unlike qualification rules (which are
attached to individual offers) and contact policies (which throttle frequency),
guardrails apply across the whole candidate set. See
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 |
status | string | No | "active" (default) or "paused" |
Example
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
{
"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",
"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
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.
{ "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.
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.
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:
{
"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 for the full debug trace shape.