In the first four months of 2025, U.S. Customs and Border Protection completed 200 import compliance audits and recovered $134 million in underpaid duties β a 67% jump in audit activity over the same period in 2024. Misclassification is the single largest driver of those penalties, accounting for roughly 42% of all CBP assessments. Ford Motor Company found out how expensive a wrong code can get: a $365 million settlement over the misclassification of around 162,000 cargo vans. In July 2025, a federal court upheld a separate $26 million jury verdict against an importer over false customs statements. (Steinshostak, Customs & International Trade Law Blog)
- Why LangGraph + UiPath, Not LangGraph Alone
- Architecture: The HTS Classification Agent
- Prerequisites
- Step 1: Scaffold the Project
- Step 2: Write the Graph
- Step 3: Authenticate and Initialize
- Step 4: Run and Debug Locally
- Step 5: Evaluate Before You Ship
- Step 6: Package and Deploy
- Wiring It Into Production
- Production Considerations
- Common Mistakes
- Key Takeaways
- FAQs
- References
Underneath every one of those numbers is the same bottleneck: a human β or a spreadsheet full of βbest guessβ codes β trying to match a product description against a 99-chapter tariff schedule that changes constantly, without perfect memory of a decade of prior rulings and without a paper trail that would survive an audit.
Thatβs a textbook agentic AI problem: unstructured input, a huge reference corpus, a judgment call, and a real cost to getting it wrong. Itβs also a textbook argument for not running that agent as a bare Python script. A classification agent making real customs decisions needs an audit trail, a way for a compliance officer to intervene before a bad classification goes out the door, and a deployment target your IT and compliance teams can actually govern.
This guide walks through exactly that: building a tariff (HTS) classification agent with LangGraph for the reasoning logic, and the UiPath Python SDK (uipath-langchain) to give it retrieval over your own classification corpus, a human-in-the-loop escalation path, and a governed home in UiPath Orchestrator. The same pattern β classify, score confidence, escalate below a threshold, log everything β applies directly to invoice coding, ticket triage, KYC review, or any other classification workflow youβre running today.
If youβre newer to LangGraph itself, our 250 LangGraph interview questions guide is a good primer on the concepts referenced here (StateGraph, checkpointing, conditional edges) before you dive into the build.

Why LangGraph + UiPath, Not LangGraph Alone
LangGraph is the reasoning engine: it models your agent as a state machine β nodes that do work, edges that decide what happens next, and a shared state object that flows between them. Thatβs the right abstraction for a classification workflow with a confidence gate and a human review branch. But LangGraph doesnβt ship an opinion on how you authenticate to your companyβs identity provider, where you store the corpus the agent retrieves from, whoβs allowed to approve an escalated classification, or how you prove to an auditor what the agent did and why.
Thatβs the gap uipath-langchain closes. Itβs an open-source Python package, maintained by UiPath, that wraps a LangGraph project with:
ContextGroundingRetrieverβ a LangChain-compatible retriever backed by UiPathβs Context Grounding (RAG) service, so your agent can search an indexed corpus (in our case, HTS chapter notes and prior rulings) without you standing up a vector database.- Human-in-the-loop primitives (
CreateTask,WaitTask,CreateEscalation,interrupt) that pause a running graph, open a task in UiPath Action Center, and resume the graph with the reviewerβs decision once itβs actioned. - A CLI (
uipath) that packages the graph, publishes it to UiPath Automation Cloud as a versioned process, and lets you invoke, monitor, and trace runs from Orchestrator. - Governance for free: every run is traced, every package is versioned, every human decision is logged against the job, and access is controlled through the same identity and folder permissions your RPA processes already use.
This is the same βharness vs. raw prompt-and-hopeβ argument we made in Agent Harness vs. Context Engineering β LangGraph gives you the graph, UiPath gives you the harness it runs inside. And the harness matters more than it sounds: LangChainβs own 2025 State of Agent Engineering survey found that 57.3% of organizations already have agents in production, with quality named as the top blocker (cited by roughly a third of respondents) and security close behind for large enterprises (24.9%). Only 62% of teams have detailed step-by-step tracing of what their agents actually did. (LangChain, State of Agent Engineering 2025) A deployment target that gives you tracing, versioning, and human review by default directly addresses the two biggest reasons agent projects stall β a pattern we cover in more depth in 16 Reasons Why Agentic Automation Programs Fail.
| Raw LangGraph (self-hosted) | LangGraph + UiPath (uipath-langchain) | |
|---|---|---|
| Reasoning / graph logic | Yes | Yes β unchanged, itβs still your LangGraph code |
| RAG over internal documents | Bring your own vector store | ContextGroundingRetriever over Context Grounding indexes |
| Human-in-the-loop review | Build your own approval UI | Action Center tasks via interrupt() |
| Auth to internal systems | Custom per integration | UiPath connections + Integration Service |
| Versioned deployment | Your own CI/CD + hosting | uipath pack / uipath publish β Orchestrator |
| Execution trail for audit | You build it | Built-in tracing per job, per node |
| Trigger patterns | You build queue/schedule logic | Orchestrator queues, triggers, or embed in a Maestro flow |
Architecture: The HTS Classification Agent
The agent takes a product description (and optionally a declared value or country of origin), searches an indexed corpus of HTS chapter notes and prior classification rulings, proposes a 10-digit HTS code with a confidence score, and routes based on that score:
- High confidence β the code is attached to the shipment record automatically.
- Below the threshold β the agent opens an Action Center task for a licensed customs broker or trade compliance officer, waits for their decision, and only then finalizes the record β with the humanβs decision captured against the job for audit purposes.
graph TD
A[START] --> B[prepare_input]
B --> C[classify: LLM + HTS retriever tool]
C --> D{confidence >= 0.85?}
D -- yes --> E[finalize: auto-approved]
D -- no --> F[escalate: interrupt + CreateTask]
F --> G[Action Center: compliance officer review]
G --> H[resume: apply human decision]
H --> E
E --> I[END]
This is structurally the same pattern as UiPathβs own official ticket-classification sample: classify with a structured output, branch on a confidence field, and use interrupt(CreateTask(...)) to hand off to a human when the model isnβt sure. Weβre adapting that exact pattern to tariff classification, but it generalizes to any βclassify, then escalate low-confidence casesβ workflow.
Prerequisites
- Python 3.11 or later, plus
piporuv(UiPathβs docs recommenduv) - A UiPath Automation Cloud account with Orchestrator access and permission to publish processes
- An LLM provider β by default the agent uses UiPathβs LLM Gateway (no separate API key needed); you can point it at Anthropic or OpenAI instead if you prefer
- An HTS reference corpus (chapter notes, prior ruling letters, your internal classification guide) uploaded to a UiPath Storage Bucket and indexed as a Context Grounding index in Orchestrator β this is what the agent retrieves against
- A UiPath Action Center app for the human review step (a simple form with the proposed code, confidence, and an approve/override field is enough to start)
Step 1: Scaffold the Project
mkdir hts-classifier && cd hts-classifier
uv init . --python 3.11
uv venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
uv add uipath-langchain
uipath -lv # confirms uipath-langchain is installed
Generate the starting project:
uipath new hts-classifier
This creates main.py (your LangGraph agent code), langgraph.json (LangGraphβs own config, pointing at your graph), and pyproject.toml (project metadata and dependencies). (UiPath SDK, Quickstart)
Step 2: Write the Graph
Replace the generated main.py with the classification graph. This follows the same shape as UiPathβs official ticket-classification sample β StateGraph, a Pydantic input/output contract, a classify node, a confidence-gated conditional edge, and an interrupt-based escalation node β retargeted to HTS codes and wired to a ContextGroundingRetriever for the tariff corpus:
import logging
from typing import Literal, Optional
from langchain_core.messages import HumanMessage, SystemMessage
from langchain_core.output_parsers import PydanticOutputParser
from langchain_core.tools.retriever import create_retriever_tool
from langgraph.graph import START, END, StateGraph, MessagesState
from langgraph.types import Command, interrupt
from pydantic import BaseModel, Field
from uipath.platform import UiPath
from uipath.platform.common import CreateTask
from uipath_langchain.retrievers import ContextGroundingRetriever
from uipath_langchain.chat.models import UiPathAzureChatOpenAI
logger = logging.getLogger(__name__)
uipath = UiPath()
# --- Contracts -------------------------------------------------------------
class GraphInput(BaseModel):
product_description: str
declared_value_usd: Optional[float] = None
country_of_origin: Optional[str] = None
assignee: Optional[str] = None # compliance officer to notify if escalated
class GraphOutput(BaseModel):
hts_code: str
confidence: float
auto_approved: bool
class GraphState(MessagesState):
product_description: str
declared_value_usd: Optional[float]
country_of_origin: Optional[str]
assignee: Optional[str]
hts_code: Optional[str] = None
confidence: Optional[float] = None
human_approval: Optional[bool] = None
class HTSClassification(BaseModel):
hts_code: str = Field(description="Proposed 10-digit HTS code, e.g. 8517.13.0000")
rationale: str = Field(description="Short justification citing the retrieved chapter notes")
confidence: float = Field(ge=0.0, le=1.0)
CONFIDENCE_THRESHOLD = 0.85
output_parser = PydanticOutputParser(pydantic_object=HTSClassification)
# --- RAG tool over the indexed HTS corpus -----------------------------------
retriever = ContextGroundingRetriever(index_name="HTS Reference Corpus")
hts_lookup_tool = create_retriever_tool(
retriever,
"search_hts_reference",
"Search HTS chapter notes and prior classification rulings for the "
"product category being classified. Use a specific query β e.g. the "
"product type and key material or function β and cite what you find.",
)
# --- Nodes -------------------------------------------------------------------
def prepare_input(graph_input: GraphInput) -> GraphState:
system_message = (
"You are a licensed customs classification assistant. Given a product "
"description, search the HTS reference corpus, then propose a 10-digit "
"HTS code with a rationale and a confidence score.\n\n"
f"{output_parser.get_format_instructions()}"
)
return GraphState(
product_description=graph_input.product_description,
declared_value_usd=graph_input.declared_value_usd,
country_of_origin=graph_input.country_of_origin,
assignee=graph_input.assignee,
messages=[
SystemMessage(content=system_message),
HumanMessage(content=graph_input.product_description),
],
)
async def classify(state: GraphState) -> Command:
llm = UiPathAzureChatOpenAI(model="gpt-4.1-mini-2025-04-14", temperature=0)
llm_with_tools = llm.bind_tools([hts_lookup_tool])
chain = llm_with_tools | output_parser
try:
result = await chain.ainvoke(state["messages"])
logger.info(f"Proposed HTS {result.hts_code} at confidence {result.confidence}")
return Command(update={"hts_code": result.hts_code, "confidence": result.confidence})
except Exception as e:
logger.error(f"Classification failed: {e}")
return Command(update={"hts_code": None, "confidence": 0.0})
def route_on_confidence(state: GraphState) -> Literal["finalize", "escalate"]:
if state["confidence"] is not None and state["confidence"] >= CONFIDENCE_THRESHOLD:
return "finalize"
return "escalate"
async def escalate(state: GraphState) -> Command:
task_output = interrupt(
CreateTask(
app_name="hts_classification_review_app",
title=f"Review HTS classification: {state['product_description'][:60]}",
data={
"ProductDescription": state["product_description"],
"ProposedCode": state["hts_code"],
"Confidence": state["confidence"],
},
assignee=state.get("assignee"),
app_folder_path="Trade Compliance",
)
)
return Command(
update={
"human_approval": True,
"hts_code": task_output.get("CorrectedCode", state["hts_code"]),
}
)
async def finalize(state: GraphState) -> GraphOutput:
return GraphOutput(
hts_code=state["hts_code"],
confidence=state["confidence"],
auto_approved=state.get("human_approval") is None,
)
# --- Graph -------------------------------------------------------------------
builder = StateGraph(GraphState, input=GraphInput, output=GraphOutput)
builder.add_node("prepare_input", prepare_input)
builder.add_node("classify", classify)
builder.add_node("escalate", escalate)
builder.add_node("finalize", finalize)
builder.add_edge(START, "prepare_input")
builder.add_edge("prepare_input", "classify")
builder.add_conditional_edges("classify", route_on_confidence)
builder.add_edge("escalate", "finalize")
builder.add_edge("finalize", END)
from langgraph.checkpoint.memory import MemorySaver
graph = builder.compile(checkpointer=MemorySaver())
A few things worth calling out:
ContextGroundingRetrieveris doing the same job a self-hosted Pinecone or Chroma store would, except the index lives in Orchestrator, gets its documents from a Storage Bucket, and inherits your folder permissions β no separate vector DB to secure or pay for. UiPathβs own docs use almost this exact scenario as their example query: βWhat is the ECCN for a laptop?β β an export-control classification lookup structurally identical to HTS classification. (UiPath SDK, Context Grounding)interrupt(CreateTask(...))is the human-in-the-loop primitive. It suspends the graph, opens a task against a UiPath Action Center app, and β critically β the graph doesnβt resume until a human actions it. The state persists via the checkpointer in the meantime, so this can be minutes or days.- The confidence threshold (
0.85here) is a business decision, not a technical one. Set it with your compliance team, and expect to tune it after your first evaluation run (Step 5).
Step 3: Authenticate and Initialize
uipath auth
This opens a browser for OAuth login and lets you select the target tenant. For CI/CD pipelines, use unattended auth instead with a Confidential External Applicationβs client ID and secret:
uipath auth --client-id <id> --client-secret '<secret>' \
--base-url https://cloud.uipath.com/<org>/<tenant>
Then initialize the project:
uipath init
uipath init generates the metadata UiPath needs to deploy the graph: uipath.json (entry points and the projectβs stable GUID β never hand-edit this ID once it exists, or youβll orphan your deployment history), bindings.json (overridable references to the resources your code calls β the Context Grounding index and Action Center app in this case), entry-points.json (the input/output schema, derived from your Pydantic models), and a .mermaid diagram of the graph for the Orchestrator UI. Re-run uipath init any time you change the graphβs input/output schema or add a new SDK resource call. (UiPath SDK, CLI Reference)
Step 4: Run and Debug Locally
uipath run agent '{"product_description": "Wireless noise-cancelling over-ear headphones with active Bluetooth 5.3, retail packaged"}'
For step-through debugging with breakpoints per node:
uipath debug agent '{"product_description": "..."}'
# commands: c (continue), s (step), b <node> (breakpoint), l (list), q (quit)
debug is genuinely useful here β you can set a breakpoint on classify, inspect exactly what the retriever returned before the LLM call, and confirm the model is actually grounding on your chapter notes rather than guessing.
Step 5: Evaluate Before You Ship
Donβt skip this step for a compliance-facing agent. Build a small evaluation set of product descriptions with known-correct HTS codes (your compliance team almost certainly already has a list of tricky historical cases) and run:
uipath eval
uipath eval auto-discovers your entry point and eval set, runs each case, and scores it against evaluators like LLM Judge Output, Tool Call Order, and Tool Call Count β useful for confirming the agent is actually calling search_hts_reference before proposing a code, not skipping straight to a guess. Results report back to UiPath if UIPATH_PROJECT_ID is set, so your eval history is versioned alongside the agent. (UiPath SDK, CLI Reference) This is also where you calibrate the confidence threshold from Step 2 β if your eval set shows the model is overconfident on a certain product category, tighten the gate for that case before it reaches production.
Step 6: Package and Deploy
uipath pack
uipath publish --my-workspace
uipath pack builds a .nupkg containing your .py, .json, .yaml, and .md files (plus uv.lock by default, so the executor installs the exact dependency versions you tested with β use --nolock only if you deliberately want floating versions). uipath publish pushes the most recent package to a feed; --my-workspace auto-creates a runnable process for testing, --tenant (or --folder) publishes to a shared Orchestrator feed for team or production use. uipath deploy does both steps in one command. (UiPath SDK, CLI Reference)
After publishing, Orchestrator gives you a process configuration link β use it to set environment variables and confirm the bindings.json resources (your Context Grounding index, your Action Center app) resolve to the right folder in this environment. This is the point where a dev-environment agent becomes a production one without touching code: swap the binding, not the source.
Invoke it in the cloud the same way you ran it locally:
uipath invoke agent '{"product_description": "..."}'
This starts a real Orchestrator job and gives you a monitoring link with full node-by-node tracing β the exact retrieval results, the exact prompt, the exact confidence score, and (if it escalated) the Action Center task and the reviewerβs decision, all attached to that job.

Wiring It Into Production
A one-off invoke call is fine for testing; production traffic needs a trigger. Three patterns cover most cases:
- Orchestrator queue trigger β each new SKU or shipment line lands in a queue item, which invokes the agent per item. This is the natural fit for classification workloads and gives you built-in retry and SLA tracking.
- Scheduled batch β run against a dayβs or hourβs backlog of unclassified items on a schedule, useful when you donβt need per-item latency.
- Embedded in a flow or as a tool for another agent β the coded agent can be called from a Maestro flow (see our UiPath Maestro Case tutorial for the orchestration layer around agents) or invoked as a sub-process from another agent via
InvokeProcess, which is exactly how youβd chain this into a broader import-compliance pipeline that also validates country-of-origin documentation or screens against restricted-party lists.
Production Considerations
Guardrails. Add PII and harmful-content middleware on the classify node before this touches real shipment data β see UiPathβs guardrails documentation for the decorator and middleware patterns.
Security. Use unattended client-credentials auth with scoped OAuth permissions (OR.Execution, plus whatever queue or asset scopes you actually need) for any CI/CD pipeline β never embed interactive-login credentials in automation. Security is already the second-largest blocker large enterprises report for agent projects, behind quality. (LangChain, State of Agent Engineering 2025)
Observability. Every job is traced by default, but pull --trace-file output into your existing observability stack if you have one β only 62% of teams surveyed have detailed step-level tracing today, and that gap is a top reason agents get pulled back out of production after launch. Our Agent Quality & Evaluation guide covers building this out further with LLM-as-judge scoring.
Cost and latency. Structured-output classification with a retrieval step typically runs a few seconds per item on a small-to-mid model; batch low-priority volume on a schedule rather than real-time if your SLA allows it, and reserve real-time invocation for the shipments actually blocking a release.
Memory across runs. MemorySaver in this example is fine for local testing, but for a long-running production agent that needs to recall prior classifications for similar products, see our Agent Memory and RAG guide on persistent, queryable agent memory.
Common Mistakes
- Skipping
uipath initafter changing the schema. If you add a field toGraphInputand forget to re-runinit,entry-points.jsongoes stale and the next deploy ships the old contract. - Hand-editing the
idinuipath.json. Itβs a stable GUID minted on firstinit. Changing it disconnects the project from its publish history β UiPath will treat it as brand new. - No confidence threshold tuning. A model thatβs βusually rightβ without a calibrated escalation gate is exactly how a $365 million misclassification settlement happens. Set the threshold from real eval data, not a guess.
- Treating
interruptas fire-and-forget. The graph genuinely pauses β build your Action Center app and test the resume path before you trust it in production; an untested escalation path is worse than no escalation path, because it creates false confidence.
Key Takeaways
- LangGraph supplies the reasoning graph;
uipath-langchainsupplies the enterprise harness β RAG via Context Grounding, human-in-the-loop via Action Center, and governed deployment via Orchestrator. - The core lifecycle is five commands:
uipath newβuipath initβuipath run/uipath debugβuipath evalβuipath pack+uipath publish(oruipath deployfor both). - Confidence-gated escalation (
interrupt(CreateTask(...))) is the pattern to reach for any time a wrong automated decision has real cost β tariff codes, invoice coding, KYC, ticket routing. - Evaluate before you deploy:
uipath evalscores your agent against a real test set and reports results back to UiPath, so quality is tracked, not assumed. - Bindings, not code changes, move an agent from dev to production β swap the Context Grounding index or Action Center app reference per environment.
FAQs
Do I need to know UiPathβs low-code tools to build a coded agent? No. Coded agents are pure Python/LangGraph; UiPathβs low-code Agent Builder is a separate, prompt-configured path for teams that donβt want to write code. They can call each other, but you donβt need Studio Web skills to follow this guide.
Can I use Anthropic or OpenAI directly instead of UiPathβs LLM Gateway? Yes β set the appropriate API key as an environment variable and configure the chat model accordingly; UiPathβs LLM Gateway is the default because it avoids managing separate provider keys, not a requirement.
What happens if nobody actions the escalated task? The graph stays suspended (state is held by the checkpointer) until the task is actioned β build SLA reminders into your Action Center app or wrap the wait in a timeout pattern if indefinite suspension isnβt acceptable for your workflow.
Is this the same thing as a UiPath βlow-code agentβ? No. Low-code agents are configured through agent.json and Agent Builder without Python. This guide covers coded agents, which give you full programmatic control over the graph β the right choice when your logic (like a confidence-gated classification pipeline) doesnβt fit a prompt-and-tools template.
How is this different from just using LangGraphβs own deployment platform? LangGraph Platform is a fine choice if you donβt need to plug into existing enterprise identity, RPA processes, or human-review workflows. uipath-langchain is the right choice when the agent needs to sit alongside RPA jobs, Action Center approvals, and Orchestratorβs existing governance β which is the common case in regulated, compliance-heavy workflows like trade classification.
References
- U.S. Customs enforcement data and Ford settlement β Steinshostak: HTS Codes Explained
- July 2025 $26M misclassification verdict β Customs & International Trade Law Blog, July 2026
- LangChain State of Agent Engineering 2025 (production adoption, top blockers, tracing coverage) β langchain.com/state-of-agent-engineering
- UiPath LangChain SDK Quickstart β uipath.github.io/uipath-python/langchain/quick_start
- UiPath SDK CLI Reference β uipath.github.io/uipath-python/cli
- UiPath Context Grounding (RAG) for LangChain β uipath.github.io/uipath-python/langchain/context_grounding
- UiPath Human-in-the-Loop reference (
interrupt,CreateTask) β uipath.github.io/uipath-python/langchain/human_in_the_loop - Official ticket-classification sample this pattern is adapted from β GitHub: uipath-langchain-python/samples/ticket-classification










