EvalLayerTry the demo

Adding commercial guardrails to OpenAI Agents SDK agents

Published 2026-07-27 · EvalLayer

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_id enter 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_id anchors 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.

Frequently asked questions

The SDK already has guardrails. Why add another layer?

Native guardrails are tripwires: input and output guardrails raise an exception when triggered, and tool guardrails can allow, reject the content, or halt. None of them can substitute a compliant rewrite or route an over-authority action to a human queue, and none carries a commercial policy object. EvalLayer adds those two decisions plus the policy and the audit record; the native tripwires remain useful for hard abort conditions.

Should the check live in a tool guardrail or in the function body?

The function body, in most cases. A tool input guardrail can map allow and block cleanly, but rewrite means sending different text and require_approval means queueing and reporting back, both of which are actions the function itself is best placed to take. Keeping all four branches in one place also keeps the audit trail coherent.

What does this cost per action?

Deterministic-only policies return in milliseconds; semantic checks take one to two seconds, comparable to one extra model call on the sends that matter. Pricing starts at $0.01 per authorization with a Bearer sk_ key or x402 micropayment, and /demo/authorize gives three free checks a day for development.

See the authorization layer live

Paste a message, set a policy, get allow / rewrite / require approval / block in seconds.

Try the demo