Enforce rules with OPA
OPA (Open Policy Agent) lets you write rules that every returned answer must satisfy. Its rule language is Rego. Use it when acceptable output depends on relationships between values, application facts or permissions—not just a field's type or format.
Examples include a total equalling its line items, an end date following its start, or an action requiring a supplied OAuth scope. JSON Schema defines the output; Rego checks whether a candidate answer meets the additional requirements.
What a policy guarantees
Struct checks answers against your policy. If an answer fails, it searches for alternatives within the configured budget. It returns an answer only when all policy requirements are satisfied.
Think of this as searching for a likely answer inside the allowed set. It is guided by the model's predictions, not an exhaustive search or a guarantee of the globally most probable answer.
Struct is a system-one model. It can search more effectively when it understands the task and has a useful idea of what a solution looks like. A needle-in-a-haystack puzzle, an impossible requirement or a task beyond its knowledge may exhaust the budget. In that case it returns 422 POLICY_SEARCH_EXHAUSTED, without a noncompliant output.
Choose between a description, schema and policy
| Need | Use |
|---|---|
| Explain what a field means or how to interpret evidence | description |
| Restrict types, labels, formats or supported combinations of output fields | JSON Schema |
| Make finite choices eligible based on supplied facts | State in schema conditions |
| Enforce a calculation or a richer relationship with input, state or permissions | OPA |
Clear descriptions help the model find valid answers. Put the task's meaning and relevant rules in them, as well as enforcing mandatory rules in Rego. More retries alone cannot supply missing knowledge or resolve contradictory requirements.
Example: keep an extracted total consistent
The input is a short receipt. The model extracts the amounts into integer cents; the policy requires the reported total to equal the sum. A wrong interpretation could still pass the arithmetic check, so this rule guarantees consistency, not perfect extraction.
Save the schema and policy files:
{
"type": "object",
"properties": {
"amounts_cents": {
"type": "array",
"minItems": 1,
"maxItems": 8,
"description": "Extract each receipt amount in order as integer cents. EUR 12.50 means 1250. Do not include a separate total as a line item.",
"items": {
"type": "integer"
}
},
"total_cents": {
"type": "integer",
"description": "The sum of the extracted amounts, in integer cents."
}
},
"required": [
"amounts_cents",
"total_cents"
],
"additionalProperties": false
}
package totals
default allow := false
allow if {
every amount in input.output.amounts_cents { amount > 0 }
input.output.total_cents == sum(input.output.amounts_cents)
}
default allow := false makes the policy deny by default. The allow rule permits an answer when all its expressions hold. input.output is the candidate answer; sum adds the amounts. Policies can also read the original request through input.input, input.state, input.parameters and input.user_context. Optional context is absent when it is omitted or null.
Send the policy with the request
policy.modules contains the Rego source loaded from the file. entrypoint: "data.totals.allow" names the rule to evaluate; it must return true for a successful answer.
Before you run this example
Call https://api.blatent.ai from your server. Set your API key in the same terminal where you run the example:
export BLATENT_API_KEY='YOUR_SERVICE_KEY'
Python requires Python 3.10+ and HTTPX:
python -m pip install "httpx>=0.28,<1"
TypeScript requires Node.js 24+ and uses its built-in fetch. The cURL examples require curl and jq.
Save the displayed files and the selected client in one directory. Run the client there with python example.py, node example.ts or bash example.sh. Downloads use the example's name in place of example.
- Python
- TypeScript
- cURL
import json
import os
from pathlib import Path
import httpx
request = {
"input": "Extract the amounts from this receipt: train ticket EUR 12.50; lunch EUR 7.50. Return the line amounts and their total in cents.",
"policy": {
"engine": "opa",
"entrypoint": "data.totals.allow",
"modules": {
"checked-sum.rego": Path("checked-sum.rego").read_text(encoding="utf-8"),
},
},
"policy_max_retries": 8,
"schema": json.loads(Path("checked-sum.schema.json").read_text(encoding="utf-8")),
}
with httpx.Client(
base_url="https://api.blatent.ai",
headers={"Authorization": f"Bearer {os.environ['BLATENT_API_KEY']}"},
timeout=195,
) as client:
response = client.post("/v1/resolve", json=request)
response.raise_for_status()
print(json.dumps(response.json()["output"]))
import { readFileSync } from "node:fs";
const request = {
"input": "Extract the amounts from this receipt: train ticket EUR 12.50; lunch EUR 7.50. Return the line amounts and their total in cents.",
"policy": {
"engine": "opa",
"entrypoint": "data.totals.allow",
"modules": {
"checked-sum.rego": readFileSync("checked-sum.rego", "utf8"),
},
},
"policy_max_retries": 8,
"schema": JSON.parse(readFileSync("checked-sum.schema.json", "utf8")),
};
const response = await fetch(
"https://api.blatent.ai" + "/v1/resolve",
{
method: "POST",
headers: {
Authorization: `Bearer ${process.env.BLATENT_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(request),
signal: AbortSignal.timeout(195_000),
},
);
if (!response.ok) throw new Error(`HTTP ${response.status}: ${await response.text()}`);
console.log(JSON.stringify((await response.json()).output));
set -eu
body=$(jq -n --argjson body '{"input":"Extract the amounts from this receipt: train ticket EUR 12.50; lunch EUR 7.50. Return the line amounts and their total in cents.","policy":{"engine":"opa","entrypoint":"data.totals.allow","modules":{}},"policy_max_retries":8}' \
--slurpfile f0 'checked-sum.schema.json' \
--rawfile f1 'checked-sum.rego' \
'$body | setpath(["schema"]; $f0[0]) | setpath(["policy","modules","checked-sum.rego"]; $f1)')
curl --fail-with-body --silent --show-error \
"https://api.blatent.ai/v1/resolve" \
-H "Authorization: Bearer $BLATENT_API_KEY" \
-H 'Content-Type: application/json' \
--data-binary "$body"
Illustrative output: {"amounts_cents":[1250,750],"total_cents":2000}. The same amounts with a total of 1999 are denied.
policy_max_retries: 8 permits up to eight retries after the initial attempt. A retry happens inside this API request; the client does not resubmit each candidate. Search budgets and feedback explains budgets and failure handling.
What Rego can read
| Rego path | Source |
|---|---|
input.output | The candidate JSON answer |
input.input | The request's input |
input.state | Application context attached to the request |
data | Rego rules and any immutable data supplied in the policy definition |
For example, permission claims attached at state.oauth.scopes are available at input.state.oauth.scopes. The oauth name is an application-chosen key, not another API authentication mechanism. State in OPA rules and permission context have complete examples.
The policy source also guides the model. Private policy data is not automatically added to its prompt. If no policy is supplied, there is no policy guidance or policy recovery session.
Reuse and extend
You can send the policy inline as above or register a policy version and use policy_id. This choice is independent of whether the schema is inline or registered.
See date relationships, exact text counts and permissions for further examples. The policy reference describes supported built-ins and limits.