A2UI Launched: Full CopilotKit support at launch!

A2UI Launched: CopilotKit has partnered with Google to deliver full support in both CopilotKit and AG-UI!

Check it out
LogoLogo
  • Overview
  • Integrations
  • API Reference
  • Copilot Cloud
Slanted end borderSlanted end border
Slanted start borderSlanted start border
Select integration...

Please select an integration to view the sidebar content.

Human in the Loop (HITL)

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:

agent.py
from langgraph.prebuilt import create_agent
from langchain_openai import ChatOpenAI

graph = create_agent(
    model=ChatOpenAI(model="gpt-4o"),
    tools=[send_email, search_web],
)
agent-js/src/agent.ts
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.

agent.py
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(),
)
agent.ts
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.

app/page.tsx
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:

  • LangChain Human-in-the-Loop (Python)
  • LangChain Human-in-the-Loop (TypeScript)
PREV
Frontend Tool Based
Slanted end borderSlanted end border
Slanted start borderSlanted start border
NEXT
Shared State

On this page

What is this?
When should I use this?
Understanding the Data Format
What you receive (event.value)
What you must return (resolve())
Implementation
Run and connect your agent
Configure HITL in your prebuilt agent
Handle the interrupt in your frontend
Give it a try!
Reference