Build a LangGraph Customer Support Agent with Tool Calling, Human Approval, and Execution Limits
A practical LangGraph pattern for customer-support triage: route requests, call lookup tools, pause refunds for human approval, and bound graph execution.
On this page
The design decision: let the agent find, but not finalize, a refund
A support agent should be allowed to gather purchase details and find matching records without being allowed to issue a refund on its own. In LangGraph, that distinction can be expressed in the workflow: route the request, call read-only lookup tools as needed, then pause before the refund operation so a person can approve or reject it.
This tutorial builds that pattern around customer-support triage. It uses LangGraph for orchestration, a tool-calling chat model for lookup, a human checkpoint before the consequential node, and a recursion limit to bound graph execution. The code is a pattern to adapt, not a tested OneMux integration.
The LangSmith tutorial supplied for this article builds a digital music-store support bot with separate lookup and refund subgraphs. It describes extracting customer and purchase details, asking for missing information, and evaluating final responses, tool trajectories, and individual steps. Its refund example uses database deletion and explicitly skips authentication and other production security measures. Do not copy that demo’s destructive behavior into a live support system. Read the LangSmith complex-agent evaluation tutorial.
Prerequisites and boundaries
Install LangGraph and the relevant LangChain integration for your chosen model provider. The source tutorial uses langgraph and langchain[openai], but the example below deliberately leaves model initialization provider-specific. You also need a database or service with a read-only purchase lookup and a refund operation that your application controls.
Before connecting a model, define what each tool can do:
- Lookup: return only records the authenticated customer is authorized to see.
- Refund preparation: identify the exact transaction and amount; do not change records.
- Refund execution: perform the approved action in application code, with authorization and audit logging.
Keep customer authentication, authorization, idempotency, and refund policy outside the model’s discretion. A model-generated customer name or invoice ID is not proof of identity.
Build a graph with a human checkpoint
The example uses a model with tool-calling support, a lookup tool, and a separate refund node. Replace the marked application functions with your own authorized service calls. The model and tool schema must be supported by the endpoint you configure; compatibility is conditional on documented endpoint and parameter support. This snippet does not establish that a particular model or OneMux configuration has been tested.
from typing import Annotated, Literal
from langchain.tools import tool
from langchain_core.messages import AnyMessage
from langgraph.graph import END, START, StateGraph
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition
from langgraph.checkpoint.memory import MemorySaver
from langgraph.types import interrupt
from typing_extensions import TypedDict
# Replace with your provider's documented LangChain model setup.
# Confirm that the chosen endpoint supports tool calling and the parameters used.
model = make_your_chat_model().bind_tools([]) # bind the tools below in your application
class State(TypedDict):
messages: Annotated[list[AnyMessage], add_messages]
refund_request: dict | None
approval: str | None
@tool
def find_purchases(customer_reference: str) -> list[dict]:
"""Find purchases for an already authenticated customer."""
return authorized_purchase_lookup(customer_reference)
tools = [find_purchases]
model = make_your_chat_model().bind_tools(tools)
def assistant(state: State):
return {"messages": [model.invoke(state["messages"])]}
def prepare_refund(state: State):
# Build this from validated application data, not model-invented values.
request = build_validated_refund_request(state["messages"])
decision = interrupt({
"type": "refund_approval",
"request": request,
"instruction": "Approve or reject this exact refund request.",
})
if decision != "approve":
return {"approval": "rejected", "refund_request": request}
return {"approval": "approved", "refund_request": request}
def execute_refund(state: State):
if state.get("approval") != "approved":
return {"messages": [{"role": "assistant", "content": "The refund was not approved."}]}
result = issue_refund_safely(state["refund_request"])
return {"messages": [{"role": "assistant", "content": format_refund_result(result)}]}
builder = StateGraph(State)
builder.add_node("assistant", assistant)
builder.add_node("tools", ToolNode(tools))
builder.add_node("prepare_refund", prepare_refund)
builder.add_node("execute_refund", execute_refund)
builder.add_edge(START, "assistant")
builder.add_conditional_edges("assistant", tools_condition)
builder.add_edge("tools", "assistant")
builder.add_edge("prepare_refund", "execute_refund")
builder.add_edge("execute_refund", END)
graph = builder.compile(checkpointer=MemorySaver())
This is a structural example, not a complete routing graph: your application must route a verified refund intent to prepare_refund and ordinary questions to the assistant/lookup path. The interrupt() call pauses the graph and returns the approval payload to the caller. A human review interface should display the customer, transaction, amount, and policy context before sending a decision. Resume the same thread with LangGraph’s Command(resume=...) mechanism and the same thread identifier. Persist checkpoints appropriately; in-memory checkpointing is for development, not durable production workflows.
A crucial control-flow detail: an interrupt is a pause, not a permission system. Enforce the approval value again in the refund execution node, and ensure the refund service itself checks authorization, transaction eligibility, and duplicate requests.
Bound execution and make failures visible
Pass a recursion limit when invoking the graph. It limits graph supersteps; it does not cap model tokens, tool runtime, database query cost, or the number of user requests. Set separate timeouts and resource limits for model calls and tools.
from langgraph.errors import GraphRecursionError
config = {
"configurable": {"thread_id": "support-case-123"},
"recursion_limit": 12,
}
try:
result = graph.invoke(
{"messages": [{"role": "user", "content": "I need help with a purchase."}]},
config=config,
)
except GraphRecursionError:
# Return a safe handoff or ask for clarification; log the trace for review.
result = {"status": "needs_human_review"}
Choose a limit based on the expected workflow, then test both normal completion and loop-prone inputs. Avoid silently retrying an exhausted graph: retries can repeat tool calls unless your tools are idempotent. Read-only lookup tools are safer to retry than side-effecting operations.
Evaluate the path, not just the answer
The supplied LangSmith tutorial identifies three useful evaluation targets: final response (was the customer’s answer appropriate?), trajectory (did the agent take the expected tool-call path?), and single step (did it choose the right tool for a particular decision?). For refund triage, create cases for missing identity details, multiple matching purchases, a lookup with no results, a rejected approval, and an approved refund.
Check that the agent asks for required information rather than inventing it; that a lookup does not trigger a refund; and that execution cannot occur without approval. Use mock or test services for evaluation so tests do not alter real customer records. The source tutorial uses a mock mode for its database operations, but describes its own demo setup—not a production safety guarantee.
Using OneMux for model access
OneMux provides access to leading AI models through one OpenAI-compatible API. If you want a single API surface for model access, review the OneMux quickstart, API documentation, and available models. Compatibility for this workflow depends on the selected model endpoint’s documented tool-calling support and accepted parameters. Confirm those details before deployment; this article does not claim the LangGraph example was tested against OneMux.
Limitations and troubleshooting
- The model does not call a tool: check that the selected endpoint supports tool calling and that the tool schema is passed in the format it documents.
- The graph loops: inspect transitions between assistant and tools, cap graph steps, and ensure a completed tool result changes the next decision.
- Approval resumes the wrong case: use a stable thread identifier tied to the support case and verify the checkpoint before resuming.
- A refund runs twice: add idempotency at the service boundary and record the approved request and outcome.
- The agent exposes another customer’s records: fix authorization in the lookup service; a prompt instruction is not access control.
Conclusion
LangGraph makes it possible to express support triage as explicit workflow steps rather than trusting a single model response to decide and act. Keep lookup read-only, pause before refunds, validate approval at execution, and bound graph steps. Then evaluate the answer and the path the agent took. Those controls make the workflow easier to inspect—but production security and policy enforcement remain application responsibilities.
Sources
- LangSmith: Evaluate a complex agent — source for the music-store support example, its lookup/refund structure, mock mode, and three evaluation targets.
FAQ
How do I require human approval before a LangGraph agent issues a refund?
Pause the graph before the refund execution node with a LangGraph interrupt, present the validated request to a reviewer, and resume the same checkpoint only after an explicit decision. Also check approval again inside the execution node and enforce policy in the refund service.
Does a LangGraph recursion limit cap tool runtime or model tokens?
No. It bounds graph supersteps, not model token usage, tool duration, database cost, or incoming request volume. Add independent timeouts and resource limits.
Can the model decide whether a customer is authorized for a refund?
No. Treat model output as untrusted input. Authenticate the customer and validate transaction access and refund eligibility in application and service code.
What should I evaluate in a customer-support agent?
Evaluate the final response, the sequence of tool calls, and individual tool-selection steps. Include missing-information, ambiguous-purchase, rejected-approval, and approved-refund cases, using test services for side effects.
Related articles
Guides
Build a Bounded LangGraph Agent for Schema-Validated Extraction
Build a LangGraph workflow that calls a tool when needed, stops within a defined execution budget, and returns structured data validated against a Pydantic schema.
Guides
LangGraph Agentic RAG: Tool Calling, Source Attribution, and Bounded Execution
Build a LangGraph document question-answering agent that decides when to retrieve, evaluates context, retries weak searches, and returns source-attributed answers without unbounded execution.
Guides
Is Grok 4.20 0309 Reasoning the Right Model for Customer Support?
A practical decision guide to using Grok 4.20 0309 Reasoning for customer support: when reasoning and tool calling help, when the non-reasoning variant is a better fit, and how to implement safe support workflows through OneMux.
Guides
Build an n8n Code-Change Workflow With Human Approval and Test Verification
Design an n8n workflow that proposes repository changes, asks a person to approve the write, and verifies the resulting commit with tests.