Skip to Content
PoliciesPolicy Reference

Policy Reference

A Keel policy is a named document containing one or more rules. When a project has active policy rows, Keel evaluates them as part of the permit decision before allowing or denying a request.

For how policies fit into the broader permit evaluation, see Decision Model.

PolicyDocument shape

{ "name": "free-tier-guardrails", "rules": [ { "if": {"field": "context.account_tier", "op": "eq", "value": "free"}, "action": "deny_if_cost_exceeds", "params": { "window": "daily", "cap_micros": 100000 } } ] }
  • name — a label for the policy document
  • rules — an ordered array of rule objects, evaluated in array order

Condition nodes

Every rule has an "if" node that determines when the rule fires. Four shapes are supported:

ShapeMeaning
{"all": [...]}AND — all child conditions must be true
{"any": [...]}OR — at least one child condition must be true
{"not": {...}}Negation of a single condition
{"field": "...", "op": "...", "value": ...}Leaf comparison

Nodes can be nested arbitrarily deep.

  • {"all": []} is always true — use it when a rule should apply unconditionally
  • {"any": []} is always false

For the full operator and field vocabulary, see Policy Conditions.

Evaluation order

  • rules in a document are evaluated in array order
  • the first terminal match wins
  • allow rules are non-terminal — see allow (non-terminal)
  • constrain_* rules are non-terminal and continue accumulating constraints after a match

Actions

Keel supports nine action literals. Custom policy authoring is gated by plan tier — the runtime engine evaluates every action on every project, but what callers may author in custom policies depends on the project’s authoring level.

PlanAuthoring levelActions allowed in custom policies
StarterTemplates onlyNone directly. Apply preset templates via POST /v1/projects/{project_id}/apply-policy-template/{template_id}.
Growth (entitlement: policy_basic_authoring)Basicdeny, deny_if_model_not_in, deny_if_cost_exceeds, deny_if_rate_exceeds, deny_if_spike_detected, constrain_max_output_tokens
Business and Enterprise (entitlement: policy_full_authoring)FullAll nine actions, including allow, require_human_review, throttle_if_rate_exceeds, and deny_if_projected_monthly_ratio_exceeds

Approval and attestation gates carry an additional plan boundary independent of the authoring level. See Approver Groups for the typed approval_requirement schema and tier matrix.

allow (non-terminal)

Non-terminal. A matching allow rule does not end evaluation. Later rules in the same document, and rules in any later policy, are still evaluated for deny, require_human_review, throttle_if_rate_exceeds, and constrain_* actions.

The first matching allow rule’s policy attribution — policy_id, policy_name, policy_version, and rule_index — is preserved through a pending-allow accumulator on the engine result. When the engine falls through to allow after no later rule has denied, reviewed, or throttled the request, the permit’s policy attribution still points at the rule that contributed approval. Audit trails remain rule-level and audit-visible.

To get a terminal outcome from an allow rule, attach an approval_requirement (or the legacy require_attestation shape). Those keep the rule terminal at challenge rather than allow. See Approver Groups for the typed schema.

Allow + later deny example

{ "name": "internal-allow-with-pii-deny", "rules": [ { "if": {"field": "context.account_tier", "op": "eq", "value": "internal"}, "action": "allow" }, { "if": {"field": "context.contains_pii", "op": "eq", "value": true}, "action": "deny" } ] }

For an internal-tier request that also contains PII, both rules match. The first rule does not short-circuit. Evaluation continues, the deny rule fires, and the permit decision is deny with reason_code = policy.rule_denied. The deny is decisive — allow rules cannot suppress a later objection.

For an internal-tier request without PII, only the first rule matches. The engine records the allow rule’s metadata in the pending-allow accumulator, no later rule denies, throttles, or reviews, and the permit’s final decision is allow — attributed to the matched allow rule.

deny

Terminal. A matching deny rule ends evaluation with:

  • outcome = deny
  • reason_code = policy.rule_denied

require_human_review

Terminal. A matching review rule ends evaluation with:

  • outcome = challenge
  • reason_code = policy.review_required

The rule may carry an optional typed approval_requirement that names who can satisfy the challenge:

{ "approval_requirement": { "type": "org_role", "role": "admin", "timeout_seconds": 1800 } }

Supported type values: org_role, user, approver_group, team, and service_principal.

Plan tier availability:

  • Business — legacy require_attestation, plus typed org_role or user with a single approver
  • Enterprise (entitlement: organization_dashboard_enabled)approver_group, team, service_principal, multi-approval (min_approvals > 1), separation-of-duties, delegated approval, and external workflow metadata

For the full schema, the audit record per approval, and the authorization model, see Approver Groups.

deny_if_model_not_in

Terminal. Denies when the requested model is absent from the provided allow-list.

{ "action": "deny_if_model_not_in", "params": { "allowed": ["gpt-4o-mini", "claude-3-5-haiku-latest"] } }

Reason code: policy.model_not_allowed

deny_if_cost_exceeds

Terminal. Denies when current or projected spend exceeds the configured cap.

{ "action": "deny_if_cost_exceeds", "params": { "window": "weekly", "cap_micros": 5000000 } }

Supported windows: request, daily, weekly, monthly, quarterly.

Reason codes by window:

WindowReason code
requestbudget.request_cap_exceeded
dailybudget.daily_cap_exceeded
weeklybudget.weekly_cap_exceeded
monthlybudget.monthly_cap_exceeded
quarterlybudget.quarterly_cap_exceeded

budget.pricing_unavailable fires when pricing is not configured for the requested model and cost cannot be safely estimated.

deny_if_rate_exceeds

Terminal. Hard deny when recent permit volume reaches the configured limit within the rolling window.

{ "action": "deny_if_rate_exceeds", "params": { "window_seconds": 60, "max_requests": 100 } }

Reason code: budget.rate_limit_exceeded

throttle_if_rate_exceeds

Terminal. Requires Business or higher (entitlement: policy_full_authoring). Returns throttle (HTTP 429 + Retry-After) rather than a hard deny when recent permit volume reaches the configured limit.

{ "action": "throttle_if_rate_exceeds", "params": { "window_seconds": 60, "max_requests": 50 } }

Reason code: budget.rate_limit_throttled

The reason_detail.outcome_detail field carries retry_after_seconds, window_seconds, limit, and observed for programmatic use. See Decision Model › Throttling for the full throttle outcome contract and SDK retry behavior.

deny_if_spike_detected

Terminal. Denies when projected day-to-date spend crosses baseline × multiplier, where the baseline is computed from the prior baseline_days days.

{ "action": "deny_if_spike_detected", "params": { "multiplier": 2.0, "baseline_days": 7 } }

Reason code: budget.daily_spike_detected

deny_if_projected_monthly_ratio_exceeds

Terminal. Requires Business or higher (entitlement: policy_full_authoring). Denies when projected monthly spend reaches a configured percentage of a monthly cap.

{ "action": "deny_if_projected_monthly_ratio_exceeds", "params": { "ratio_pct": 85, "monthly_cap_micros": 10000000, "projection": "estimated" } }

projection may be current (actual spend to date) or estimated (spend to date plus the current request estimate).

Reason code: budget.monthly_threshold_exceeded

constrain_max_output_tokens

Non-terminal. Emits a max_output_tokens constraint and continues evaluating later rules.

{ "action": "constrain_max_output_tokens", "params": { "cap_tokens": 2048 } }

Constraint merge is most-restrictive-wins: if multiple matching rules emit max_output_tokens, the lowest cap survives. The final constraint is carried in the permit response. On Keel-managed execution surfaces, this constraint is enforced before provider dispatch. On permit-first flows, your application is responsible for honoring it.

Two-rule merge example

{ "name": "tiered-output-caps", "rules": [ { "if": {"all": []}, "action": "constrain_max_output_tokens", "params": {"cap_tokens": 2048} }, { "if": {"field": "context.account_tier", "op": "eq", "value": "free"}, "action": "constrain_max_output_tokens", "params": {"cap_tokens": 512} } ] }

For a free-tier request, both rules match and emit max_output_tokens. The merged constraint is 512 — the lower cap. For a non-free-tier request, only the first rule matches, and the constraint is 2048. Because constrain_* actions are non-terminal, evaluation continues past each match and accumulates constraints from every rule that fires.

Validation

Keel validates policy documents at write time. The following are rejected:

  • unknown top-level keys
  • malformed condition nodes
  • unknown operators
  • unknown actions
  • invalid params shapes
  • unsafe matches_regex patterns

Regex authoring is intentionally constrained:

  • maximum pattern length: 500 characters
  • backreferences are rejected
  • lookaround assertions are rejected
  • patterns that can trigger catastrophic backtracking are rejected
  • runtime matching is capped at 5 ms and fails closed

Examples

Unconditional model allow-list

{ "name": "approved-models-only", "rules": [ { "if": {"all": []}, "action": "deny_if_model_not_in", "params": { "allowed": ["gpt-4o-mini", "claude-3-5-haiku-latest"] } } ] }

Business-hours review gate

{ "name": "after-hours-review", "rules": [ { "if": { "any": [ {"field": "context._keel.request_hour_utc", "op": "lt", "value": 9}, {"field": "context._keel.request_hour_utc", "op": "gte", "value": 17} ] }, "action": "require_human_review", "approval_requirement": { "type": "org_role", "role": "admin", "timeout_seconds": 1800 } } ] }

Free-tier throttle plus output cap

{ "name": "free-tier-throttle", "rules": [ { "if": {"field": "context.account_tier", "op": "eq", "value": "free"}, "action": "constrain_max_output_tokens", "params": {"cap_tokens": 512} }, { "if": {"field": "context.account_tier", "op": "eq", "value": "free"}, "action": "throttle_if_rate_exceeds", "params": { "window_seconds": 60, "max_requests": 20 } } ] }

Monthly budget threshold with cost window cap

{ "name": "monthly-controls", "rules": [ { "if": {"all": []}, "action": "deny_if_projected_monthly_ratio_exceeds", "params": { "ratio_pct": 90, "monthly_cap_micros": 50000000, "projection": "estimated" } }, { "if": {"all": []}, "action": "deny_if_cost_exceeds", "params": { "window": "daily", "cap_micros": 2000000 } } ] }

Accurate scope

  • Policy rows are real and authoritative, but they are not the entire permit decision path. See Decision Model for the full picture.
  • Project policy rows take precedence over organization rows. They do not stack.
  • The evaluation context is intentionally compact. Do not assume field namespaces beyond those documented in Policy Conditions.
Last updated on Edit this page on GitHub