Decision Model
An AI Permit is a pre-execution decision record. It may allow, deny, throttle or require review. AI Permit issuance and supported managed-execution routes produce an AI Permit that captures outcome, reason, constraints and budget state at evaluation time. Dry-run evaluation does not issue or persist one.
POST /v1/permits exposes this decision seam directly. All managed execution
surfaces (/v1/execute, /v1/executions, /v1/proxy/*) apply the same Permit
evaluation before provider dispatch. The AI Permit is the durable governance
record regardless of which supported surface originated the request.
An AI Permit is not the same as approval. Only allow authorizes execution.
Deny, throttle and review-required outcomes are also AI Permits.
Managed execution also rechecks the authoritative permit and its exact request binding at the final dispatch boundary. An expired, revoked, mismatched, denied, throttled, or still-under-review permit cannot open the provider call. Permit-first applications own their downstream call and must enforce the permit decision, constraints, and any blocking review action themselves.
Outcomes
Keel’s policy engine produces four outcomes:
| Outcome | Meaning | HTTP signal |
|---|---|---|
allow | Request may proceed, possibly with accumulated constraints | 200 |
deny | Request is rejected | 200 on permits; 403 on execution routes |
review | Public API outcome meaning the request requires review or attestation | 200 on permits; held before managed dispatch |
throttle | Request is rate-throttled | 200 on permits; 429 with Retry-After on execution routes |
throttle is a first-class public outcome, not a variant of deny. It carries
retry_after_seconds inside decision_details.outcome_detail, which Keel uses
as the source for the HTTP Retry-After header. Unlike deny, throttled
requests may be retried after the specified delay.
The internal policy protocol calls the review-required state challenge.
Current public response models serialize that state as review. New clients
should branch on the public values in this table. Historical records may retain
legacy protocol terminology in explicitly labeled compatibility fields; those
fields do not turn a review-required Permit into authorization.
What Keel evaluates
For every permit request, Keel considers:
- Policy rows — active rules authored for the project or its organization
- Cost controls — budget caps, rate limits, spike guards, and threshold guardrails
- Platform preconditions — whether the requested operation has pricing support and billing access
- Platform safety nets — governed-request quota and overage enforcement
Policy rows are the primary authoring surface. Cost controls can be expressed as policy rules or as project-level configuration. Platform preconditions run independently of authored rules.
Policy scope
When a project has active policy rows, those rows are evaluated. When a project has no active rows but belongs to an organization with active organization-scoped rows, the organization rows apply. Project rows and organization rows do not stack — project rows take precedence.
Within the evaluated scope:
- rules are evaluated in authoring order
- the first terminal match wins
allowrules are non-terminal — a matching allow rule preserves rule-level attribution but lets later rules deny, review, throttle, or emit constraints. See Policy Reference ›allow(non-terminal).constrain_*rules are non-terminal and continue accumulating constraints after a match
Constraints
When a policy rule emits a constraint, the constraint is carried in the permit response. The current constraint type is max_output_tokens.
Constraint merge is most-restrictive-wins: if multiple matching rules emit max_output_tokens, the lowest cap survives.
On Keel-managed execution surfaces, supported constraints are enforced before provider dispatch. On permit-first flows, your application is responsible for honoring any constraints carried in the permit decision.
Example constraint output in a permit response:
{
"schema_version": 1,
"max_output_tokens": 512
}Budget snapshot
Keel builds a hierarchical budget snapshot from project configuration and live spend state. When budget caps or guardrails are configured, permits carry a budget snapshot. All monetary values are in usd_micros. Sections appear only when the relevant cap or guardrail is active.
{
"schema_version": 1,
"currency_unit": "usd_micros",
"request": {
"estimated_cost": 120000,
"cap": 150000,
"remaining": 30000
},
"daily": {
"cap": 3000000,
"current_spend": 2200000,
"projected_spend": 2320000,
"remaining": 800000
},
"monthly": {
"cap": 10000000,
"current_spend": 7800000,
"projected_spend": 7920000,
"remaining": 2200000,
"threshold_ratio": 0.85,
"threshold_amount": 8500000
},
"rate_limit": {
"window_seconds": 60,
"limit": 50,
"observed": 50,
"retry_after_seconds": 12
}
}Structured reason codes
Every permit-creation response carries a machine-readable reason_code, including
allowed decisions. These codes appear in permit responses, Timeline Replay, the
dashboard, and governance audit events.
See Errors › Permit reason codes for the full locked vocabulary.
Decision details
In addition to reason_code, non-allow Permit decisions can carry a structured
decision_details object. Its core fields are decision, code, and reason.
Policy and budget decisions can also include these nested objects:
reason_detail— why the decision was reached; its fields can include acategory,kind,outcome, and reason-specific evidenceoutcome_detail— how the outcome should be served, including retry timing for throttles
The contents depend on the outcome and reason code. For example, a
budget.daily_cap_exceeded deny can carry spend and cap evidence:
{
"decision_details": {
"decision": "deny",
"code": "budget.daily_cap_exceeded",
"reason": "Projected daily spend would exceed the configured cap.",
"reason_detail": {
"category": "budget",
"kind": "daily_cap_exceeded",
"outcome": "deny",
"cap_usd_micros": 3000000,
"current_spend_usd_micros": 2200000,
"projected_spend_usd_micros": 3050000,
"window": "daily"
}
}
}Filter on reason_code for categorization. Read the nested detail objects for
diagnostic and response data. Treat unknown fields as additive.
Throttling
Throttling is a first-class permit outcome, not a variant of deny. A permit with outcome = throttle represents a decision that the request would have been allowed except for a recent rate ceiling — and that the caller should retry after a bounded delay rather than treating the request as permanently denied.
Throttling is produced by the throttle_if_rate_exceeds policy action. See Policy Reference › throttle_if_rate_exceeds for the action contract.
Throttle versus deny
deny_if_rate_exceeds | throttle_if_rate_exceeds | |
|---|---|---|
| Outcome | deny | throttle |
| HTTP status on execution surfaces | 403 | 429 |
Retry-After header | Not set | Set from decision_details.outcome_detail.retry_after_seconds |
| Reason code | budget.rate_limit_exceeded | budget.rate_limit_throttled |
| Caller intent | ”This is rejected; do not retry without changing something" | "Try again after the bounded delay” |
Use deny_if_rate_exceeds when exceeding the rate cap is itself a violation. Use throttle_if_rate_exceeds when the rate cap exists to smooth load and the caller is expected to back off and retry.
Throttle detail shape
A throttled Permit can carry these response fields:
{
"decision": "throttle",
"reason_code": "budget.rate_limit_throttled",
"decision_details": {
"decision": "throttle",
"code": "budget.rate_limit_throttled",
"reason": "The request rate reached the configured limit.",
"reason_detail": {
"category": "budget",
"kind": "rate_limit",
"outcome": "throttle",
"window_seconds": 60,
"limit": 50,
"observed": 50
},
"outcome_detail": {
"retry_after_seconds": 12,
"window_seconds": 60,
"limit": 50,
"observed": 50
}
}
}retry_after_seconds— how long the caller should wait before retrying. This value is the source for the HTTPRetry-Afterheader on execution surfaces.window_seconds— the size of the trailing rate window the rule evaluates.limit— the configured ceiling for that window.observed— the count Keel saw in the window when the rule fired.
HTTP behavior
On execution surfaces (/v1/execute, /v1/executions, /v1/proxy/*), throttle decisions return HTTP 429 with a Retry-After header. The response body is the normalized execution envelope with status: "denied" and an error block. The routing.reason_code and the permit reason_code both carry budget.rate_limit_throttled.
On /v1/permits, the throttle decision is a Permit record with HTTP 200 —
Permit decisions do not branch HTTP status. Branch on
decision === "throttle" and read
decision_details.outcome_detail.retry_after_seconds.
SDK retry behavior
Keel’s Python and JavaScript SDKs recognize throttle responses on execution surfaces:
- The SDKs detect the
429response and theRetry-Afterheader. - When automatic retries are enabled, they use the configured retry limit and
honor the server’s retry timing. The JavaScript client enables its default
retry policy unless configured otherwise; the Python client retries only when
a
RetryConfigis supplied. - After the configured attempts are exhausted, the SDK surfaces the throttle to the caller; neither client retries indefinitely.
Permit-only flows do not auto-retry, because the application controls the
provider call and the retry decision belongs in application code. Read
decision_details.outcome_detail.retry_after_seconds and decide whether to
retry, queue, or surface the throttle to your end user.
When throttling appears in audit evidence
Throttled permits are stored as deny-category records with a throttle outcome tag. They appear in:
- Permit list and detail responses (
GET /v1/permits,GET /v1/permits/{permit_id}) - Timeline Replay as a
permit.deniedevent withreason_code: budget.rate_limit_throttled - The dashboard activity stream
- Signed compliance exports
Throttled permits do not consume billable cost. They consume a request count for plan-quota purposes, the same as denied requests.
Decision artifacts on the permit
The AI Permit records decision evidence including:
decision,reason_code, and structureddecision_details- constraints when constraint rules matched
- budget snapshot when budget caps or guardrails are configured
- routing metadata when the request carried routing context
- policy ID and version when a policy row matched
- estimated usage fields and, after closeout, actual usage fields
Where supported, Keel cryptographically signs and binds the AI Permit and its decision evidence. Signing and independently verifiable artifacts vary by outcome, route, and plan entitlement.
These fields are the canonical decision record. Managed execution events, usage records and supported closeout evidence add lifecycle or execution evidence around the Permit; they do not replace it or independently prove the ultimate external side effect.
Accurate scope
- The AI Permit is Keel’s canonical pre-execution decision record.
- Policy rows are the primary enforcement mechanism, but the full permit decision also includes cost controls, billing gates, and platform preconditions that are not expressed as authored policy rules.
- Permit-first mode produces a decision record before execution; it does not provide execution-bound proof. See Permits for the full permit-first trust boundary.