Prebuilt Agents
Learn how to implement Human-in-the-Loop (HITL) with LangGraph prebuilt agents.
What is this?
LangGraph's prebuilt agents (like create_agent) support Human-in-the-Loop (HITL) through a middleware-based approach.
This allows you to require human approval before certain tools are executed.
This guide covers HITL with prebuilt agents. If you're building a custom graph with manual interrupt() calls,
see Interrupt based instead.
Important: This integration passes data in LangGraph's native format. You should be familiar with LangChain's HITL documentation before proceeding.
When should I use this?
Use this approach when:
- You're using LangGraph's prebuilt agents (
create_react_agent,create_agent, etc.) - You want to require human approval before specific tools are executed
- You want to allow humans to edit or reject tool calls before execution
Understanding the Data Format
What you receive (event.value)
When a tool requires approval, CopilotKit's useLangGraphInterrupt hook receives the interrupt payload directly from LangGraph.
The event.value contains LangGraph's native structure, which can be found in LangChain's HITL documentation:
// event.value structure from LangGraph
{
action_requests: [
{
name: string, // Tool name (e.g., "send_email")
arguments: object, // Tool arguments (e.g., { to: "user@example.com", body: "..." })
description: string, // Human-readable description of the action
},
// ... more action requests if multiple tools need approval
],
review_configs: [
{
action_name: string, // Tool name
allowed_decisions: string[], // e.g., ["approve", "edit", "reject"]
},
]
}What you must return (resolve())
When calling resolve(), you must provide a response in LangGraph's expected decisions format.
Each decision corresponds to an action request (in the same order):
// Decision types
type Decision =
| { type: "approve" } // Approve the tool call as-is
| { type: "edit", edited_action: { name: string, args: object } } // Approve with modified arguments
| { type: "reject", message: string } // Reject the tool call
// resolve() expects { decisions: [...] }, one decision per action_request
resolve({
decisions: [
{ type: "approve" },
{ type: "edit", edited_action: { name: "send_email", args: { to: "other@example.com" } } },
{ type: "reject", message: "Not authorized to delete files" },
]
})Implementation
Run and connect your agent
This guide assumes you already have a LangGraph prebuilt agent set up, similar to this:
from langgraph.prebuilt import create_agent
from langchain_openai import ChatOpenAI
graph = create_agent(
model=ChatOpenAI(model="gpt-4o"),
tools=[send_email, search_web],
)import { createAgent } from "@langchain/langgraph/prebuilt";
import { ChatOpenAI } from "@langchain/openai";
const graph = createAgent({
llm: new ChatOpenAI({ model: "gpt-4o" }),
tools: [sendEmail, searchWeb],
});Configure HITL in your prebuilt agent
Add the HumanInTheLoopMiddleware to require approval for specific tools.
Configure interrupt_on with a mapping of tool names to approval settings.
from langchain.agents import create_agent
from langchain.agents.middleware import HumanInTheLoopMiddleware
from langgraph.checkpoint.memory import InMemorySaver
agent = create_agent(
model="gpt-4o",
tools=[send_email, search_web],
middleware=[
HumanInTheLoopMiddleware(
interrupt_on={
"send_email": True, # All decisions (approve, edit, reject) allowed
"search_web": False, # Auto-approve, no interruption
},
),
],
checkpointer=InMemorySaver(),
)import { createAgent, humanInTheLoopMiddleware } from "langchain";
import { MemorySaver } from "@langchain/langgraph";
const agent = createAgent({
model: "gpt-4o",
tools: [sendEmail, searchWeb],
middleware: [
humanInTheLoopMiddleware({
interruptOn: {
send_email: true, // All decisions (approve, edit, reject) allowed
search_web: false, // Auto-approve, no interruption
},
}),
],
checkpointer: new MemorySaver(),
});A checkpointer is required to persist state across interrupts. Use InMemorySaver/MemorySaver for testing
or a persistent checkpointer like AsyncPostgresSaver for production.
Handle the interrupt in your frontend
Use the useLangGraphInterrupt hook to render approval UI and respond with decisions.
import { useLangGraphInterrupt } from "@copilotkit/react-core";
const YourMainContent = () => {
useLangGraphInterrupt({
render: ({ event, resolve }) => {
// event.value contains LangGraph's native structure
const actionRequests = event.value.action_requests;
return (
<div className="p-4 border rounded-lg">
<h3 className="font-bold mb-4">Tool Approval Required</h3>
{actionRequests.map((request, index) => (
<div key={index} className="mb-4 p-3 bg-gray-100 rounded">
<p className="font-medium">Tool: {request.name}</p>
<pre className="text-sm mt-2">
{JSON.stringify(request.arguments, null, 2)}
</pre>
</div>
))}
<div className="flex gap-2 mt-4">
<button
className="px-4 py-2 bg-green-500 text-white rounded"
onClick={() => {
// Approve all actions - one decision per action_request
resolve({
decisions: actionRequests.map(() => ({ type: "approve" }))
});
}}
>
Approve All
</button>
<button
className="px-4 py-2 bg-red-500 text-white rounded"
onClick={() => {
// Reject all actions
resolve({
decisions: actionRequests.map(() => ({
type: "reject",
message: "User declined"
}))
});
}}
>
Reject All
</button>
</div>
</div>
);
}
});
return <div>{/* Your app content */}</div>;
};Give it a try!
When the agent attempts to call a tool that requires approval, the UI will pause and show your approval component. The agent will resume once you approve, edit, or reject the action.
Reference
For complete details on LangGraph's HITL format and options, see:
