EvalLayerTry the demo

Adding commercial guardrails to LangGraph agents

Published 2026-07-27 · EvalLayer

You add commercial guardrails to LangGraph agents by inserting an authorization node between the node that drafts an outbound action and the node that executes it, then routing on the decision with conditional edges: allow and rewrite proceed to the send node, require_approval pauses the graph with interrupt(), and block terminates the path with an audit record. The graph structure makes the guarantee inspectable: there is no edge from draft to send that bypasses the check.

Why LangGraph agents create commercial risk

LangGraph is what teams reach for when agents graduate from demos to production loops: durable state, checkpointing, and explicit control flow for agents that send emails, file tickets, issue refunds, and negotiate with customers over many steps. That is precisely the risk profile. A multi-step agent does not make one decision you can review; it makes dozens, and any tool call that touches a customer or spends money can bind the business that deployed it. The failure mode is rarely a jailbreak. It is an agent correctly following a vague objective into a 30 percent goodwill discount nobody authorized.

LangGraph's native answer is human-in-the-loop: interrupt() pauses execution at a node, the checkpointer preserves state, and Command(resume=...) continues with a human verdict. This is excellent machinery, and the current middleware patterns for intercepting tool calls make the wiring even cleaner. But the framework deliberately leaves the judgment to you: it can stop for a human, it cannot tell you whether this refund is inside authority or that sentence is a delivery guarantee. Pausing on everything buries reviewers; pausing on nothing is hope.

The interception point: a node before the send tool

Model the check as a first-class node. It reads the drafted action from state, posts it to /authorize, and writes the decision back to state; a conditional edge then routes the graph.

import httpx

def authorize_action(state):
    r = httpx.post(
        "https://api.evallayer.ai/authorize",
        headers={"Authorization": "Bearer " + EVALLAYER_KEY},
        json={
            "action_type": "send_email",
            "content": state["draft"],
            "context": {"thread_id": state["thread_id"]},
            "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": [],
            },
        },
        timeout=10,
    )
    data = r.json()
    return {
        "decision": data["decision"],
        "final_text": data.get("approved_rewrite") or state["draft"],
        "authorization_id": data["authorization_id"],
        "violations": data.get("violations", []),
    }

def route_decision(state):
    if state["decision"] in ("allow", "rewrite"):
        return "send"
    if state["decision"] == "require_approval":
        return "human_review"
    return "blocked"

graph.add_node("authorize", authorize_action)
graph.add_conditional_edges("authorize", route_decision,
    {"send": "send_email", "human_review": "human_review", "blocked": "log_block"})

The send node reads final_text, which already holds the rewrite when one was issued. In the human_review node, call interrupt() with the draft, the violations, and the authorization_id; because state is checkpointed, the approval can arrive minutes or days later and Command(resume=...) picks up exactly where the graph paused. If you are on the middleware-style tool interception path instead of an explicit node, the same request and the same four-way switch apply inside the tool wrapper.

Decision handling

  • allow: proceed along the send edge with the draft unchanged.
  • rewrite: proceed along the same edge, sending approved_rewrite instead. The loop keeps moving; nobody is paged for a fixable disclaimer.
  • require_approval: route to the interrupt node. The graph sleeps until a human resumes it with an approve or reject.
  • block: route to a terminal logging node. The draft is never sent; the authorization_id and violations become the audit record.

Latency and placement

Gate side effects, not cognition. Planning, retrieval, and drafting nodes run at full speed; only edges into consequential tools pass through authorize. Deterministic policies cost milliseconds, semantic checks one to two seconds, roughly one additional model hop on the one edge where it matters. Set an explicit timeout and choose the failure mode per action type; for refunds, failing closed is the defensible default.

What to log

Write the authorization_id into graph state so it lands in your checkpointer alongside everything else about the run. That gives you a replayable trace where every consequential action carries its policy verdict: what was drafted, what decision came back, what was actually sent. When someone asks why the agent gave a customer 15 percent off, the answer is a state lookup, not an archaeology project.

Try it on one edge

Pick your highest-risk send edge and wire the node against /demo/authorize (three free checks per day, no key), or preview decisions in the interactive demo. The decision model is specified in the authorization guide, and policy design covers translating real signing authority into the policy object.

Frequently asked questions

How is this different from LangGraph's interrupt()?

interrupt() is the mechanism, not the judgment. It pauses the graph for a human, but something has to decide which actions deserve a pause. EvalLayer supplies that decision: most actions come back allow or rewrite and flow through untouched, and interrupt() fires only on require_approval, so humans see the small fraction that exceeds authority instead of every send.

Where should the authorization node sit in the graph?

Directly before any node with external side effects: sending email, posting a reply, issuing a refund, publishing content. Draft nodes and internal reasoning need no gate. If several send paths exist, one shared authorize node with conditional edges keeps the policy in exactly one place.

What does a check cost in latency and money?

Deterministic-only policies (caps, authority limits, required text) return in milliseconds. Semantic checks add one to two seconds, comparable to one extra model call in the graph. Pricing starts at $0.01 per authorization with a Bearer key or x402, and /demo/authorize offers three free checks a day for testing.

See the authorization layer live

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

Try the demo