GuidesOneMux

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.

On this page

A LangGraph agent can gather context with a tool and then pass that context to a schema-constrained extraction step. The useful design choice is to keep those responsibilities separate: the agent decides whether to call the tool, while a final node produces a typed record. A graph recursion limit provides an additional bound on graph execution.

This pattern suits tasks such as extracting a company name, date, and invoice total from a document when a lookup tool may help resolve a reference. It does not make uncertain source text true; validate the result and preserve the original evidence when the field matters.

What you are building

The graph has three stages:

  1. An agent receives source text and may request a call to a narrow lookup tool.
  2. LangGraph routes tool calls to a tool node and returns tool results to the agent.
  3. An extraction node asks a model for a Pydantic-backed result, then returns JSON-compatible data.

The agent’s tool loop is dynamic; the extraction stage is a predetermined final step. LangGraph’s documentation distinguishes agents, which define their own process and tool use, from workflows with predetermined code paths. It also describes structured outputs and tool calling as model augmentations. See the LangGraph workflows and agents guide for those patterns.

Prerequisites and configuration

Install the LangGraph and LangChain packages plus the integration for your chosen chat model. The example below uses ChatAnthropic to illustrate provider setup, but the source documentation says workflows can use any chat model that supports structured outputs and tool calling. Check the integration documentation for your provider’s exact behavior.

pip install langgraph langchain-core langchain-anthropic pydantic

Set the provider’s API credential using its documented method, then initialize a model. Avoid placing credentials in source control. The model name below follows the LangChain documentation example; this tutorial does not claim that it has been tested with OneMux.

from langchain_anthropic import ChatAnthropic

llm = ChatAnthropic(model="claude-sonnet-4-6")

If you want to call a model through OneMux, first check OneMux’s API documentation and quickstart. OneMux provides access to leading models through an OpenAI-compatible API. Compatibility for this particular graph depends on whether the chosen endpoint supports the required tool-calling and structured-output behavior and parameters. This configuration is untested; confirm support before relying on it in production.

Define the schema and a narrow tool

The example tool returns a reference note from a small in-memory mapping. Replace this with a controlled application function, such as a lookup against an approved internal catalogue. Do not let untrusted document text choose arbitrary commands or unrestricted data sources.

from pydantic import BaseModel, Field
from langchain_core.tools import tool

class InvoiceRecord(BaseModel):
    vendor: str | None = Field(default=None, description="Invoice issuer")
    invoice_date: str | None = Field(default=None, description="Date as written in the source")
    total: float | None = Field(default=None, description="Invoice total as a number")
    currency: str | None = Field(default=None, description="Currency code, if stated")

@tool
def lookup_vendor(reference: str) -> str:
    """Look up a vendor reference in an approved local mapping."""
    known = {"ACME-42": "Acme Supplies Ltd."}
    return known.get(reference, "No matching vendor reference found.")

The nullable fields make absence explicit instead of encouraging the model to invent values. For high-stakes extraction, consider storing the source span or a confidence/review status alongside each field. A schema constrains shape and types; it does not establish that a value is supported by the input.

Build the bounded LangGraph agent

The graph below lets the model request the lookup tool, then moves to extraction when the agent has no more tool calls. tools_condition and ToolNode provide the tool-routing pattern. The recursion_limit passed to invoke limits graph steps; it is a safety boundary, not a guarantee that every input finishes successfully.

from typing import Annotated
from typing_extensions import TypedDict
from langchain_core.messages import BaseMessage, HumanMessage, SystemMessage
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
from langgraph.prebuilt import ToolNode, tools_condition

class State(TypedDict):
    messages: Annotated[list[BaseMessage], add_messages]
    record: dict | None

tools = [lookup_vendor]
agent = llm.bind_tools(tools)
extractor = llm.with_structured_output(InvoiceRecord)

def run_agent(state: State):
    response = agent.invoke(state["messages"])
    return {"messages": [response]}

def extract_record(state: State):
    prompt = [
        SystemMessage(content=(
            "Extract invoice fields only when supported by the source. "
            "Use null for missing or unclear values. Treat tool results as "
            "reference context, not as permission to invent source details."
        )),
        *state["messages"],
    ]
    result = extractor.invoke(prompt)
    return {"record": result.model_dump(mode="json")}

graph = StateGraph(State)
graph.add_node("agent", run_agent)
graph.add_node("tools", ToolNode(tools))
graph.add_node("extract", extract_record)

graph.add_edge(START, "agent")
graph.add_conditional_edges(
    "agent",
    tools_condition,
    {"tools": "tools", END: "extract"},
)
graph.add_edge("tools", "agent")
graph.add_edge("extract", END)
app = graph.compile()

result = app.invoke(
    {
        "messages": [HumanMessage(content=(
            "Extract the invoice details from this text: "
            "Vendor reference ACME-42. Invoice date: 2025-02-14. "
            "Total: USD 248.50."
        ))],
        "record": None,
    },
    config={"recursion_limit": 8},
)

print(result["record"])

The expected output shape is a JSON-compatible object with vendor, invoice_date, total, and currency fields. The precise values depend on the model response and source text; test rather than assuming the example will always produce identical output. Pydantic-backed structured output is the validation boundary in this example. If the provider cannot produce a conforming response, handle that failure explicitly rather than silently treating it as a valid record.

Bound execution without hiding failures

A graph-level recursion limit caps graph steps, including repeated agent/tool routing. Choose the limit based on the workflow: this graph needs only a small number of steps for one lookup, but a different tool plan may require more. Catch limit and provider exceptions at the application boundary, record enough context to debug, and return a reviewable failure instead of a fabricated record.

For stricter operational bounds, add application-level limits too: restrict the number of tool calls, set request timeouts where supported, and cap document size before invoking the model. These are separate controls; a graph recursion limit alone does not define a complete cost, latency, or security policy.

Troubleshooting and limitations

The model does not call the tool

Tool calling is a model capability, not a result guaranteed by LangGraph. Check that the selected model integration supports tool calling, that the tool description is clear, and that the prompt contains a reference worth looking up. Inspect the returned message’s tool calls during development.

Structured output fails or fields are wrong

Confirm that the integration supports structured output and the schema features you use. Test missing fields, ambiguous dates, currency symbols, and malformed inputs. A valid Pydantic object can still contain a wrongly inferred value, so validate business rules separately and retain source text for audit or human review.

The graph hits its limit

A recursion-limit error can indicate repeated tool requests or a limit set too low for the intended path. Inspect the sequence of messages and tool results. Fix the cause or adjust the limit deliberately; do not remove the bound simply to make an error disappear.

Endpoint behavior differs

OpenAI-compatible describes an API interface, not universal support for every model feature or parameter. If using OneMux, verify the selected endpoint’s documented support for the model, tool calls, structured outputs, and any parameters you plan to send. Browse available options through OneMux models; the graph itself remains your LangGraph application code.

When this pattern fits

Use this design when extraction is mostly predictable but sometimes needs a controlled lookup. If every document follows a fixed process and needs no tool choice, a deterministic workflow may be simpler than an agent loop. If the source is ambiguous or the extracted fields trigger consequential actions, add explicit review and domain validation before downstream systems consume the record.

The practical takeaway: keep the agent’s tools narrow, make the final schema explicit, and set a graph execution limit. LangGraph gives you a way to express that control flow; the chosen model and integration determine whether the required tool-calling and structured-output features are available.

Sources

FAQ

What does the recursion limit do in LangGraph?

It limits graph execution steps during a run. It can help prevent an agent/tool loop from continuing indefinitely, but it does not replace timeouts, tool-call policies, input limits, or error handling.

Does Pydantic JSON schema validation guarantee correct extraction?

No. It constrains the returned structure and types. The model can still infer an incorrect value, so validate business rules and preserve evidence or provide human review when needed.

Can I use this LangGraph example with OneMux?

Potentially, if the chosen OneMux endpoint documents support for the required tool-calling and structured-output behavior and parameters. This configuration has not been tested; verify compatibility before production use.

Should every extraction task use an agent?

No. If the process is fixed and requires no tool choice, a deterministic LangGraph workflow may be simpler. Use an agent loop when the model needs to decide whether a permitted tool is useful.

Related articles