Home / Articles / Building Safe AI Agents with LangChain Guardrails and Middleware

This article is published in English.

Building Safe AI Agents with LangChain Guardrails and Middleware

Learn how deterministic and model-based guardrails work in LangChain to catch PII leaks, enforce business rules, and add human approval steps to AI agents.

3372 words

What are guardrails?

A guardrail is a mechanism that monitors what an AI system is doing and stops it from taking actions you don't want it to take.

Consider an agent equipped with the following tools:

search()
sendEmail()
deleteUser()
makePayment()

The model might decide that calling deleteUser() is the right move.

But is that something you actually want to permit?

A guardrail sits between the agent's decision and the actual execution of that action:

User
  ↓
Agent
  ↓
Guardrail
  ↓
Is this allowed?
  ├── Yes → Execute
  └── No  → Block / Ask for approval

Guardrails are commonly used to:

  • Stop personally identifiable information from leaking out
  • Spot and stop prompt injection attempts
  • Filter out harmful or inappropriate content
  • Apply business logic and regulatory constraints
  • Check that outputs meet quality and correctness standards
  • Pause execution until a human approves a sensitive action

In LangChain, guardrails are built mainly through middleware, which gives you a way to hook into the agent's execution flow at specific points.

Why do AI agents need guardrails?

Traditional software follows logic that a developer wrote explicitly.

For instance:

if (!user.isAdmin) {
  throw new Error("Unauthorized");
}

LLMs don't work this way.

You provide instructions and tools, but the model itself decides what action to take.

Take a support agent that has access to a refundPayment() tool:

User:
I was charged twice. Please refund ₹50,000.
Agent:
→ Calls refundPayment()

The model's choice here might seem reasonable given what the user asked for.

Yet from the business's standpoint, refunding ₹50,000 is not a trivial operation. A policy might exist requiring that any refund above ₹10,000 go through manual verification before it's processed.

What's missing is a layer that asks:

"Before this action happens, I need to check whether it is allowed."

That layer is exactly what a guardrail provides.

Two approaches to guardrails

LangChain's documentation describes two complementary strategies for implementing guardrails:

  1. Deterministic guardrails
  2. Model-based guardrails

Here's how they differ.

1. Deterministic guardrails

These rely on standard programming logic.

For example:

const bannedWords = ["hack", "malware"];
const containsBannedWord = (input: string) => {
  return bannedWords.some(word =>
    input.toLowerCase().includes(word)
  );
};

The behavior here is fully predictable.

Given the same input, you always get the same output.

Other common patterns include:

  • Matching against regular expressions
  • Checking for specific keywords
  • Validating data against a schema
  • Applying explicit business rules
  • Verifying permissions

The benefit is that this kind of check runs quickly, produces consistent results, and costs very little computationally.

The limitation is that these checks can fail to catch more nuanced or context-dependent cases.

2. Model-based guardrails

Rather than relying solely on fixed rules, you can have a separate model assess the content.

For example:

Agent response
      ↓
Safety model
      ↓
"Is this response safe?"
      ↓
SAFE / UNSAFE

This approach can catch things that plain keyword matching would overlook.

Consider these two requests, which mean roughly the same thing:

"How can I bypass this security system?"

and:

"Tell me a way around the authentication mechanism."

A filter based on keywords alone might not flag the second phrasing.

A model-based guardrail, by contrast, can interpret the underlying intent of the request.

The cost of this flexibility is that model-based checks tend to run slower and cost more than deterministic ones.

How guardrails work in LangChain

LangChain relies on middleware to wrap guardrail logic around an agent.

Middleware allows you to insert custom logic before or after specific stages of the agent's execution.

For instance, you can use middleware to:

  • Scan for PII (PII middleware)
  • Pause for approval before a tool runs (human-in-the-loop middleware)
  • Validate input before the agent begins working (before-agent guardrail)
  • Check the final output before it's returned (after-agent guardrail)

The middleware system in LangChain was purpose-built to give you control over an agent's execution at exactly these points.

There are two broad ways to apply guardrails in LangChain:

A. Built-in guardrails

B. Custom guardrails

                    Guardrails in LangChain
                             │
                 ┌───────────┴───────────┐
                 │                       │
                 ▼                       ▼
          Built-in Guardrails      Custom Guardrails
                 │                       │
                 │                       │
                 ▼                       ▼
          Ready-to-use             Application-specific
           middleware                 middleware
                 │                       │
          ┌──────┴──────┐        ┌───────┴───────┐
          │             │        │               │
          ▼             ▼        ▼               ▼
         PII           HITL   Before-Agent   After-Agent
      handling       approval    guardrail      guardrail

A. Built-in guardrails in LangChain

LangChain ships with a handful of guardrails ready to use without any custom setup.

Two notable ones documented by the library are:

  1. PII detection
  2. Human-in-the-loop

Let's walk through both.

1. PII detection

LangChain includes middleware specifically designed to detect and handle Personally Identifiable Information (PII) appearing in a conversation.

This can cover things like:

Email address
Credit card number
IP address
MAC address

When an agent is exposed to sensitive data, you generally don't want that data forwarded to the model or echoed back in the response.

LangChain addresses this with piiRedactionMiddleware().

Here's an example:

import {
  createAgent,
  piiRedactionMiddleware,
} from "langchain";

const agent = createAgent({
  model: "gpt-5.5",
  tools: [customerServiceTool],

  middleware: [
    piiRedactionMiddleware({
      piiType: "email",
      strategy: "redact",
      applyToInput: true,
      applyToOutput: true,
    }),
  ],
});

const result = await agent.invoke({
  messages: [{
    role: "user",
    content: "My email is john.doe@example.com"
  }]
});

Suppose a user submits:

My email is john.doe@example.com

The middleware intercepts and rewrites this before the model ever sees it:

My email is [REDACTED_EMAIL]

In other words, the model works with sanitized placeholders rather than the raw sensitive data.

Note: Setting applyToOutput: true ensures that even if the model happens to generate PII in its reply, the middleware strips it out before the response reaches the user. If your tools might leak PII in their results, applyToToolResults: true extends the same protection to tool outputs as well.

PII handling strategies

LangChain supports four distinct ways of dealing with detected PII:

To see the difference, let's apply each strategy to the same sample input.

Assume the user enters:

My email is john.doe@example.com

1. redact

With redact, any detected PII is fully swapped out for a generic placeholder.

piiRedactionMiddleware({
  piiType: "email",
  strategy: "redact",
  applyToInput: true,
});

What the model actually receives is:

My email is [REDACTED_EMAIL]

This strategy fits situations where the model has no real need to know the underlying value.

2. mask

The mask strategy obscures part of the value while leaving enough visible for context.

piiRedactionMiddleware({
  piiType: "email",
  strategy: "mask",
  applyToInput: true,
});

The email might come out looking like:

My email is j***@example.com

This is handy when either the model or the end user needs a partial reference to the data without exposing it in full.

3. hash

hash swaps the PII for a consistent, deterministic hash value.

piiRedactionMiddleware({
  piiType: "email",
  strategy: "hash",
  applyToInput: true,
});

The transformed email could look something like:

My email is 8f14e45fceea167a5a36dedd4bea2543...

Since hashing is deterministic, identical inputs always yield identical hashes. This makes it possible to track or match repeated occurrences of the same value without ever revealing the original data.

4. block

Unlike the other three, block doesn't transform the PII at all — it simply rejects the request outright once that type of PII is found.

For instance, you could configure a custom detector to catch API keys:

piiRedactionMiddleware({
  piiType: "api_key",
  detector: /sk-[a-zA-Z0-9]{32}/,
  strategy: "block",
  applyToInput: true,
});

If a user then sends:

My API key is sk-abcdefghijklmnopqrstuvwxyz123456

the middleware recognizes the API key pattern and halts the request before the value can propagate any further.

This strategy suits cases where certain categories of sensitive data — API keys, credentials, and similar secrets — must never be allowed into the agent pipeline in the first place.

2. Human-in-the-loop

Certain operations carry too much risk to be handed over entirely to an autonomous agent.

Consider actions such as:

delete production database
send an external email
make a financial transaction
modify production data

Rather than banning these actions outright, you can route them through a human approval step.

The resulting flow looks like this:

Agent
  ↓
Tool call
  ↓
Guardrail
  ↓
Human approval
  ↓
┌───────────────┐
│               │
Approved      Rejected
│               │
↓               ↓
Execute        Stop

LangChain offers humanInTheLoopMiddleware() to implement this pattern.

For example:

import { createAgent, humanInTheLoopMiddleware } from "langchain";
import { MemorySaver, Command } from "@langchain/langgraph";

const agent = createAgent({
  model: "gpt-5.5",
  tools: [
    searchTool,
    sendEmailTool,
    deleteDatabaseTool,
  ],
  middleware: [
    humanInTheLoopMiddleware({
      interruptOn: {
        send_email: {
          allowAccept: true,
          allowEdit: true,
          allowRespond: true,
        },
        delete_database: {
          allowAccept: true,
          allowEdit: true,
          allowRespond: true,
        },
        search: false,
      },
    }),
  ],
  // A checkpointer is required so the paused run can be resumed later
  checkpointer: new MemorySaver(),
});

With this in place, the agent halts before executing send_email or delete_database and waits on a human verdict. To resume execution afterward, the call needs both a thread ID and a Command:

const config = { configurable: { thread_id: "some_id" } };
// First call pauses and waits for approval
await agent.invoke(
  { messages: [{ role: "user", content: "Send an email to the team" }] },
  config
);
// Resume after a human approves the tool call
await agent.invoke(
  new Command({ resume: { decisions: [{ type: "approve" }] } }),
  config
);

Important: without a checkpointer and a thread_id, there's nothing for the middleware to resume from, and the pause-then-approve mechanism simply won't function. This is by far the most common configuration mistake.

Here, the configuration effectively states:

search → automatically allowed
send_email → require human approval
delete_database → require human approval

This pattern proves especially valuable for agents running in production environments.

If you'd like a deeper look at the human-in-the-loop pattern, a separate practical walkthrough covers exactly how the graph pauses via interrupt(), waits on a human choice, and then continues execution through Command.

B. Custom guardrails

The middleware LangChain ships with out of the box won't fit every scenario your application faces.

When your requirements go beyond what's built in, LangChain lets you write your own middleware and wire in custom guardrail behavior.

Two lifecycle hooks are especially handy for this purpose:

beforeAgent
afterAgent

These hooks give you a way to inject guardrail logic at specific moments during an agent's run.

1. Before-agent guardrails

A beforeAgent hook fires at the very start of an agent invocation. You can use it to build a before-agent guardrail that inspects or filters an incoming request before the agent even starts working on it.

Typical use cases include:

  • Authentication
  • Rate limiting
  • Input filtering
  • Rejecting inappropriate requests
  • Checks scoped to a session

Here's an illustration:

import { createMiddleware, AIMessage } from "langchain";

const sensitiveDataFilterMiddleware = (sensitiveKeywords: string[]) => {
  const keywords = sensitiveKeywords.map((kw) => kw.toLowerCase());

  return createMiddleware({
    name: "SensitiveDataFilterMiddleware",

    beforeAgent: {
      hook: (state) => {
        // Check if messages exist
        if (!state.messages || state.messages.length === 0) {
          return;
        }

        // Get the first user message
        const firstMessage = state.messages[0];

        // Make sure the message is from the user
        if (firstMessage._getType() !== "human") {
          return;
        }

        const content = firstMessage.content.toString().toLowerCase();

        // Check for sensitive keywords
        for (const keyword of keywords) {
          if (content.includes(keyword)) {
            // Stop the agent before it starts processing
            return {
              messages: [
                new AIMessage(
                  "I cannot process requests containing sensitive information. " +
                  "Please remove passwords, API keys, or secrets and try again."
                ),
              ],
              jumpTo: "end",
            };
          }
        }

        // No sensitive content found
        return;
      },

      canJumpTo: ["end"],
    },
  });
};


// Create the agent
import { createAgent } from "langchain";

const agent = createAgent({
  model: "gpt-5.5",
  tools: [searchTool, calculatorTool],

  middleware: [
    sensitiveDataFilterMiddleware([
      "password",
      "api_key",
      "secret",
      "private_key",
    ]),
  ],
});


// This request will be blocked
const result = await agent.invoke({
  messages: [
    {
      role: "user",
      content: "Show me how to store my production password securely.",
    },
  ],
});

console.log(result);

Flow:

                         User Request
                              │
                              ▼
                  ┌──────────────────────┐
                  │   beforeAgent Hook   │
                  │                      │
                  │ Check user message   │
                  │ for sensitive words │
                  └──────────┬───────────┘
                             │
                             ▼
                    ┌─────────────────┐
                    │ Sensitive       │
                    │ keyword found?  │
                    └───────┬─────────┘
                            │
                   ┌────────┴────────┐
                   │                 │
                  YES                NO
                   │                 │
                   ▼                 ▼
        ┌──────────────────┐   ┌──────────────┐
        │ Return blocked   │   │ Continue to  │
        │ AIMessage        │   │ the agent    │
        └────────┬─────────┘   └───────┬──────┘
                 │                     │
                 ▼                     ▼
          jumpTo: "end"          Agent executes
                 │                     │
                 ▼                     ▼
           Final Response        Final Response

With this hook active, a problematic request gets stopped before the agent has a chance to run or invoke any tools.

2. After-agent guardrails

An afterAgent hook fires once the agent has completed its work. It lets you implement an after-agent guardrail that checks or filters the agent's final answer before it reaches the user.

Common applications include:

  • Safety checks
  • Validating output quality
  • Compliance checks
  • Filtering the output
  • Evaluation performed by another model

As an example, you could route the response through a second model whose only job is to evaluate it:

import {
  createMiddleware,
  AIMessage,
  initChatModel,
} from "langchain";

const toxicityGuardrailMiddleware = () => {
  // Model used only for toxicity evaluation
  const evaluatorModel = initChatModel("gpt-5.4-mini");

  return createMiddleware({
    name: "ToxicityGuardrailMiddleware",

    afterAgent: {
      hook: async (state) => {
        // Get the final AI response
        if (!state.messages || state.messages.length === 0) {
          return;
        }

        const lastMessage =
          state.messages[state.messages.length - 1];

        if (lastMessage._getType() !== "ai") {
          return;
        }

        const response = lastMessage.content.toString();

        // Ask the evaluator model to check for toxicity
        const evaluationPrompt = `
You are a toxicity detection system.

Analyze the following AI response and determine
whether it contains toxic, abusive, hateful, or
harassing language.

Respond with ONLY:
SAFE
or
TOXIC

AI response:
${response}
`;

        const evaluation = await evaluatorModel.invoke([          {            role: "user",            content: evaluationPrompt,          },        ]);

        const result = evaluation.content
          .toString()
          .trim()
          .toUpperCase();

        // Replace the response if it is toxic
        if (result === "TOXIC") {
          return {
            messages: [
              new AIMessage(
                "I'm unable to provide that response because it contains inappropriate language."
              ),
            ],
            jumpTo: "end",
          };
        }

        return;
      },

      canJumpTo: ["end"],
    },
  });
};


// Create the agent
import { createAgent } from "langchain";

const agent = createAgent({
  model: "gpt-5.5",

  tools: [
    searchTool,
    calculatorTool,
  ],

  middleware: [
    toxicityGuardrailMiddleware(),
  ],
});


// Invoke the agent
const result = await agent.invoke({
  messages: [
    {
      role: "user",
      content: "Give me a response to this angry customer.",
    },
  ],
});

console.log(result);
User
 ↓
Main Agent (GPT-5.5)
 ↓
Generates response
 ↓
afterAgent hook
 ↓
Evaluator Model (GPT-5.4-mini)
 ↓
┌───────────────┐
│ Is it toxic?  │
└───────┬───────┘
        │
    ┌───┴───┐
    ↓       ↓
  SAFE     TOXIC
    ↓       ↓
Return    Replace
response  response

The key takeaway here is that you should never automatically trust the agent's output. Instead, you send the generated answer through an evaluator model whose job is to check it against your safety criteria before handing it back to the user.

Combining multiple guardrails

In practice, a single guardrail is rarely enough for a real-world application.

Different phases of an agent's execution call for different kinds of safeguards. Consider this sequence:

1. Input filtering
2. PII protection
3. Tool approval
4. Output safety check

LangChain lets you attach several middleware pieces to a single agent at once.

Here's an example:

const agent = createAgent({
  model: "gpt-5.5",
  tools: [
    searchTool,
    sendEmailTool,
  ],
  middleware: [
    // 1. Before-agent guardrail for input filtering
    sensitiveDataFilterMiddleware([
      "password",
      "api_key",
      "secret",
      "private_key",
    ]),
    // 2. Built-in PII protection middleware
    piiRedactionMiddleware({
      piiType: "email",
      strategy: "redact",
      applyToInput: true,
      applyToOutput: true,
    }),
    // 3. Built-in Human approval middleware for sensitive tools
    humanInTheLoopMiddleware({
      interruptOn: {
        send_email: {
          allowAccept: true,
          allowEdit: true,
          allowRespond: true,
        },
      },
    }),
    // 4. After-agent guardrail
    toxicityGuardrailMiddleware(),
  ],
});

Each middleware layer is responsible for guarding a distinct part of the agent's execution path.

Put together, the overall flow looks like this:

                         User Request
                              │
                              ▼
                  ┌──────────────────────┐
                  │ Before-agent         │
                  │ guardrail            │
                  │                      │
                  │ Input filtering      │
                  └──────────┬───────────┘
                             │
                             ▼
                  ┌──────────────────────┐
                  │ PII protection       │
                  │                      │
                  │ Redact sensitive     │
                  │ information          │
                  └──────────┬───────────┘
                             │
                             ▼
                         AI Agent
                             │
                             ▼
                       Tool call?
                        /       \
                      No         Yes
                      │           │
                      │           ▼
                      │    ┌───────────────┐
                      │    │ Human approval│
                      │    └───────┬───────┘
                      │            │
                      │       ┌────┴────┐
                      │       │         │
                      │    Approved   Rejected
                      │       │         │
                      │       ▼         ▼
                      │   Execute      Stop
                      │       │
                      └───────┤
                              ▼
                       Agent response
                              │
                              ▼
                  ┌──────────────────────┐
                  │ After-agent          │
                  │ guardrail            │
                  │                      │
                  │ Toxicity check       │
                  │ using evaluator model│
                  └──────────┬───────────┘
                             │
                        ┌────┴────┐
                        │         │
                       SAFE      TOXIC
                        │         │
                        ▼         ▼
                   Return      Replace
                   response    response

This produces a layered defense strategy for the agent.

The core idea is that each guardrail is responsible for a different checkpoint:

  • The before-agent guardrail validates the incoming request.
  • The PII middleware shields sensitive data.
  • Human-in-the-loop review blocks risky tool calls until a person approves them.
  • The after-agent guardrail checks the final response before it reaches the user.

Instead of depending on one single safety mechanism, you stack multiple layers so the agent is protected more thoroughly at every stage.

Guardrails are not just about safety

When people hear the term "guardrails," they usually picture blocking harmful or offensive content.

In production systems, though, guardrails also serve to enforce business logic and application-specific policies.

For instance:

Customer support agent

Can:
✓ Search orders
✓ Check delivery status

Cannot:
✗ Refund more than ₹10,000
✗ Delete customer account
✗ Change payment details

These rules have nothing to do with detecting harmful content.

They represent application policies that define the boundaries of what the agent is permitted to do.

Since an LLM makes decisions dynamically at runtime, you need a dependable enforcement point for rules that must hold regardless of what the model decides on its own.

Note: the LLM decides what it wants to do; the guardrails determine what the application actually allows it to do.

Final thoughts

Building an AI agent involves more than wiring an LLM up to a handful of tools.

Once that agent starts handling real user requests and touching real systems, you need clear boundaries around its behavior. That's exactly the role guardrails play.

LangChain supplies several building blocks for this purpose:

  • Deterministic guardrails for rules that must be predictable
  • Model-based guardrails for checks that depend on meaning and context
  • PII middleware for handling sensitive information
  • Human-in-the-loop middleware for actions with real consequences
  • Before-agent middleware for input and session-level checks
  • After-agent middleware for validating the final output
  • Multiple middleware layers combined for defense in depth

The single most important lesson from all of this is: don't rely on the LLM alone to enforce your application's rules.

Let the model handle reasoning and decision-making, but keep the boundaries that truly matter encoded in code and middleware, where you can inspect and verify them directly.

That's what turns an AI agent into something dependable enough for a real production environment.

References

  • The official LangChain guide covering guardrail concepts, available at https://docs.langchain.com/oss/javascript/langchain/guardrails
  • The official LangChain reference describing how middleware works in general, available at https://docs.langchain.com/oss/javascript/langchain/middleware/overview