Adding commercial guardrails to OpenAI Agents SDK agents
You add commercial guardrails to OpenAI Agents SDK agents by performing the authorization inside the function tools that touch the outside world: before the tool sends, spends, or publishes, it posts the intended content to EvalLayer's /authorize endpoint and branches on the decision, sending as-is, sending the approved rewrite, queueing for human approval, or returning the block reason to the model. The SDK's native guardrails stay in place for hard abort conditions; this layer adds the two decisions they cannot express.
What the SDK's guardrails do, and where they stop
The Agents SDK is OpenAI's production successor to Swarm: a lightweight loop with handoffs, sessions, tracing, and a first-class guardrail system. That system is genuinely useful and worth using: input guardrails screen what enters the first agent, output guardrails validate the final agent's answer, and tool guardrails run on function-tool invocations with three behaviors, allow the call, reject the content with a message the model sees, or raise an exception that halts the run.
Notice the shape of every branch: pass, refuse, or stop. That is a tripwire model, built for "never let this through" conditions. Commercial reality needs two more verbs. A quote 2 percent over the discount cap should not kill the run; it should go out corrected. A refund above the agent's authority should not be silently rejected; it should reach a human with context. And all of it should check against an explicit policy, not a guardrail function's hardcoded opinion, so the same agent can serve different customers under different caps. Those are the decisions the SDK leaves to you.
The interception point: inside the function tool
Every consequential action in an Agents SDK app already flows through a function tool you wrote. Put the check there, before the side effect:
import httpx
from agents import function_tool
POLICY = {
"max_discount_pct": 10,
"commitment_authority_usd": 1000,
"forbidden_commitments": ["delivery_date", "indemnification"],
"approved_claims": ["soc2_certified"],
"required_disclaimers": ["pricing_subject_to_change"],
"blocked_topics": [],
}
@function_tool
def send_quote_email(to: str, body: str) -> str:
"""Send a quote email to a customer."""
r = httpx.post(
"https://api.evallayer.ai/authorize",
headers={"Authorization": "Bearer " + EVALLAYER_KEY},
json={"action_type": "send_quote", "content": body,
"context": {"recipient": to}, "policy": POLICY},
timeout=10,
)
data = r.json()
if data["decision"] == "allow":
deliver_email(to, body)
return "Sent. Authorization " + data["authorization_id"]
if data["decision"] == "rewrite":
deliver_email(to, data["approved_rewrite"])
return "Sent with a policy-compliant revision. Authorization " + data["authorization_id"]
if data["decision"] == "require_approval":
queue_for_human(to, body, data["authorization_id"])
return "Quote exceeds authority and is queued for human approval: " + data["violations"][0]["detail"]
log_blocked(data["authorization_id"], data["violations"])
return "Blocked by commercial policy: " + data["violations"][0]["detail"]
Returning a string on every branch matters. The model reads tool results, so a block becomes information it can act on, apologize, offer an alternative, or hand off, instead of an unexplained exception ending the session. If you also want a belt-and-suspenders abort for categorical violations, a tool guardrail with the halt behavior in front of this function is a reasonable addition; treat the exemption paths as the function's job.
Decision handling
- allow: the send executes with the original text.
- rewrite: the send executes with
approved_rewrite. No human is interrupted for a fixable disclaimer or an over-cap number the rewrite corrected. - require_approval: nothing sends; the draft and
authorization_identer your review queue, and the agent can tell the user the offer is being confirmed. - block: nothing sends; the violation detail returns to the model, and the
authorization_idanchors the audit record.
Latency and handoffs
Check side-effect tools only; retrieval and computation tools run free. Deterministic policies cost milliseconds, semantic checks one to two seconds. In multi-agent setups with handoffs, keep the guarded tools on whichever agent owns external communication, so a handoff cannot route around the check; the policy travels with the tool, not with the agent that happened to call it.
What to log
The SDK's tracing already captures every tool call. Embed the authorization_id in the tool result (as above) and it appears in your traces automatically, pairing each consequential action with its policy verdict, the violations found, and the risk_score. That is the difference between a trace that shows what the agent did and one that shows it was authorized to do it.
Start with one tool
Guard your riskiest tool against /demo/authorize first (three free checks per day, no key), or watch the interactive demo run a quote with planted violations. The decision model is documented in the authorization guide, and policy design covers building the policy object itself.