Guardrails Are Nothing but Fancy Name of If-Else or Try-Catch Block
Inspiration: Guardrails is another important topic in the Agentic AI world. I want to share my mental model on Guardrails. What they are, why they matter, and how to implement them.
In software engineering, if you are writing say a function, you often add lots of If conditions before you run your main code. There could be various reasons why you have to do that but one of those reasons is to make sure the main logic has everything it needs before it runs. Those are Guardrails in my sense. So when you are building an agentic system, all the cases where you want your main-logic or agent to work perfectly, all the information it needs, all edge-cases, you can add guardrails to make sure your agent runs smoothly.
For example: is_valid(), is_authenticated() etc.
Now, you can also give me a counter point that we can add try/catch to handle those cases, not always if conditions. And that is a valid point. But if you look closely in a try/catch block you handle exceptions and that is also a Guardrail. The block is different, the intent is the same.
So to summarise, when you are building an agentic system and you get this thought that “what if”, there are high chances that you have to add guardrails. That’s what I do.
You want your agentic system to run on production. It will be used not only by you but other users, so you want to make sure its main job works smoothly without any error. You can handle that by adding Guardrails to the system. And that is why Guardrails are so important. Skipping this step is what causes agentic systems to behave unpredictably in production and that is something you really don’t want.
How to Implement it?
There are two ways to go about it: use what your framework already gives you, or write your own. Let’s look at both.
Based on which framework you are using, you can read their documentation and use the pre-built classes. For example, frameworks like LangChain give you pre-built guardrails out of the box. From LangChain’s official documentation, you can use their “middleware” to add guardrails. You can either add it before the call to the agent is made, or after the agent has generated the response. They have some prebuilt Classes like PIIMiddleware to handle the Personally Identifiable Information. There are various strategies they have listed like Masking, Redact, hash or block that you can use based on the use case.
LangChain also provides HumanInTheLoopMiddleware for requesting human approval before executing an operation. You can select on which operation you would want LangChain to seek for Human approval, like send_email, delete_database, search etc.
You can also create your custom guardrails and attach it to middleware layer of the LangChain call. This is where I feel we spend most of our time “writing custom guardrails”. And that is what we are going to learn today.
In one of my posts I explained node-pool architecture. It is a basic LangGraph based agentic system, I will use that to add some guardrails. You can find the post here to understand how that is implemented if you wish. But even without that you will be able to understand how to implement Guardrails, as I said they are nothing but fancy name of If-Else or Try-Catch Block.
We will add two guardrails to our mentoring agent node-pool system. Let’s get started.
Guardrail 1 — Input Scope Validator
In your system, when a user is querying for something, it can also make queries for things which your system is not designed to do. We can call these OUT OF SCOPE queries.
Mental Model: How to handle out of scope cases in our function logic? Add If-Else block or Try-Catch Block. Gotcha!!
So, in the code I handled it by adding one more node just before the Router. It will check for out-of-scope cases and return a proper message to the user if it finds one. Here is how it is wired up:
builder.add_node("input_scope_validator", input_scope_validator)
builder.add_node("router", router)
builder.add_edge(START, "input_scope_validator")
builder.add_conditional_edges("input_scope_validator", after_input_validation, {
"router": "router",
"__end__": END
})
And here is the main logic:
INPUT_SCOPE_PROMPT = """
You are a scope validator for the mentoring system. Your only job is to decide if the user's request is within scope.
In-scope: questions about employees, mentors, skills, availability and mentor matching.
Out-of-scope: questions about salaries, promotions, personal information, or any other unrelated topics.
Reply with ONLY a JSON object: {"valid": true} or {"valid": false, "reason": "..."}
"""
def input_scope_validator(state: AgentState) -> dict:
messages = [SystemMessage(content=INPUT_SCOPE_PROMPT)] + list(state["messages"])
response = llm.invoke(messages)
content = response.content if isinstance(response.content, str) else response.content[0]["text"]
match = re.search(r'\{.*?\}', content, re.DOTALL)
if match:
result = json.loads(match.group(0))
valid = result.get("valid", False)
if not valid:
reason = result.get("reason", "Input is out of scope.")
return {
"input_valid": False,
"messages": [AIMessage(content=f"I can only help with mentor matching. {reason}")]
}
return {"input_valid": True}
def after_input_validation(state: AgentState) -> str:
if state.get("input_valid", True):
return "router"
else:
return "__end__"
Guardrail 2 — Output PII Guard
What if our system outputs sensitive information to the user like Employee_ID or Mentor_ID. How do we handle this situation?
Mental Model: Thinking of If-Else Block or Try-Catch block? Perfect!! You have become the champ. This is another classic case of adding Guardrails.
What is the correct step in our existing flow to add this guard? Right after the system has generated the output response, we can check if it contains any sensitive information. If it does we will handle it with our guardrail.
builder.add_node("output_pii_guard", output_pii_guard)
builder.add_edge("format_response", "output_pii_guard")
builder.add_edge("output_pii_guard", END)
Main Logic:
_ID_PATTERN = re.compile(r'\b[EM]\d+\b')
def output_pii_guard(state: AgentState) -> dict:
last_message = state["messages"][-1]
if not last_message or not isinstance(last_message, AIMessage):
return {}
content = last_message.content if isinstance(last_message.content, str) else last_message.content[0]["text"]
if not _ID_PATTERN.search(content):
return {}
return {"messages": [AIMessage(content=_ID_PATTERN.sub("###", content))]}
In Agentic Engineering, you can relate any new buzzword with what we already have been doing. It is not that complicated. It is just a new language that we have to learn to communicate the same thoughts.
The thought process remains the same, to build robust software systems, only the lingo has changed.
In my next article I will cover how to do Evaluations or Evals in Agentic System. Stay Tuned!