Skip to main content
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

FieldTypeRequiredDescription
keystringYesUnique guardrail key
namestringYesGuardrail name
descriptionstringNoDescription of the constraint
severitystringNo"hard" (blocks decision, default) or "soft" (warns only)
expressionAstobjectNoExpression AST defining the guardrail condition
statusstringNo"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

FieldTypeRequiredDescription
keystringYesGuardrail key to update
namestringNoUpdated name
descriptionstringNoUpdated description
typestringNoGuardrail type
configobjectNoType-specific configuration
conditionsanyNoCondition definitions
priorityintegerNoEvaluation priority (0+)
severitystringNo"hard" or "soft"
expressionAstobjectNoUpdated expression AST
statusstringNo"draft", "active", "paused", or "archived"
scopestringNoGuardrail scope
rowVersionintegerNoOptimistic 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

ParameterTypeRequiredDescription
keystringYesGuardrail key to delete

Response 200

{ "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 pathResolves 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

SeverityWhen the rule fires
hardThe candidate is blocked (removed from the result set). Audit-logged with action mandatory_override, severity: "hard".
softThe 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.