Adding commercial guardrails to LangGraph agents
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_rewriteinstead. 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_idand 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.