LangGraph for Financial Analysis Agents
Ask a plain chat model to "analyze this company" and you get a confident essay that mixes real reasoning with invented numbers, skips the balance sheet entirely, and forgets what it said three paragraphs ago. That is not analysis. Real financial analysis is a workflow: gather data, verify it, compute ratios, compare against peers, form a view, and then challenge that view before anyone acts on it. Every one of those steps has different failure modes, and a single prompt cannot handle them all. This is exactly the class of problem LangGraph was built for. In this guide, we will design and build a langgraph financial analysis agent from scratch — a stateful, multi-node system that fetches data through tools, routes work conditionally, critiques its own output, and pauses for human approval before producing a final report. Everything here is patterns you can run today, not slideware.
Why Financial Analysis Breaks Single-Prompt Agents
Before reaching for a framework, it is worth being precise about why the naive approach fails. Financial analysis has four properties that fight against a single LLM call.
First, it is multi-step with dependencies. You cannot compute a debt-to-equity ratio before you have the balance sheet figures, and you cannot compare valuation multiples before you have decided which peer set is relevant. Steps depend on the outputs of earlier steps, which means you need somewhere to store intermediate results — state — and a way to sequence work.
Second, it is numerically unforgiving. If a travel-planning agent hallucinates a restaurant, someone eats somewhere else. If a financial agent hallucinates free cash flow, someone makes a capital decision on fiction. Numbers must come from tools and data sources, never from the model's parametric memory, and the system needs structural guarantees around that, not just a polite instruction in the prompt.
Third, the workflow is conditional. A profitable SaaS company and a pre-revenue biotech need completely different analytical paths. An analysis that flags a liquidity red flag should trigger deeper solvency checks that a healthy balance sheet would skip. Static chains — do A, then B, then C — cannot express "do D only if B looked bad."
Fourth, it demands accountability. In any serious finance context you need to answer: what data did the agent see, what did it compute, where did each claim come from, and who approved the output? That means persistent state, checkpointing, and human-in-the-loop gates are not nice-to-haves. They are requirements.
LangGraph addresses all four directly: shared typed state, explicit nodes and edges, conditional routing, and built-in checkpointing with interrupts. Let us map those primitives to the finance domain.
LangGraph Core Concepts, Translated to Finance
LangGraph models an agent as a state machine expressed as a graph. Three ideas carry the whole framework, and each has a natural financial interpretation.
The state is a shared, typed data structure that flows through the graph. For a financial agent, the state is your working file on a company: the ticker, the raw fundamentals you fetched, computed ratios, peer comparisons, the draft thesis, critique notes, and a running message history. Every node reads from this state and returns updates to it. Because the state schema is explicit, you always know what the agent knows.
Nodes are units of work — plain Python functions that take the state and return a partial update. In our domain, nodes are analyst roles: a data-gathering node, a quantitative node that computes ratios, a qualitative node that reads filings and news, a synthesis node that drafts the thesis, and a critic node that attacks it. Some nodes call an LLM, some call pure Python. That mix matters: ratio arithmetic should be deterministic code, not token prediction.
Edges define control flow. Normal edges say "after data gathering, run quantitative analysis." Conditional edges are where the intelligence lives: a routing function inspects the state and decides what happens next — retry data fetching, escalate to a deep-dive, or proceed to synthesis. This is how you encode an analyst's judgment about process, separately from the analysis itself.
On top of these, LangGraph provides checkpointers, which persist the state after every node execution. A checkpointed graph can be paused, resumed, replayed, and audited — which, as we will see, is precisely what compliance-adjacent workflows need.
Designing the Agent: An Analyst Team as a Graph
Good graph design starts on paper, not in code. Think about how a competent human equity research team actually operates, then translate roles into nodes.
A workable first architecture for a financial analysis agent looks like this:
- Planner node. Takes the user's request ("analyze NVDA's balance sheet risk" versus "give me a full fundamental review of a mid-cap bank") and produces a structured analysis plan: which data to pull, which analyses to run, what depth is required.
- Data node. Calls tools to fetch fundamentals, prices, and filings. It does no interpretation — its only job is populating the state with verified raw material and recording the source of each item.
- Quant node. Runs deterministic Python over the fetched data: liquidity ratios, leverage ratios, margin trends, growth rates, valuation multiples. No LLM involved in the arithmetic.
- Qualitative node. Uses the LLM to summarize management discussion, risk factors, and recent news into structured observations, each tagged with its source document.
- Synthesis node. Combines quantitative results and qualitative observations into a draft report with an explicit thesis and confidence level.
- Critic node. A separate LLM call with an adversarial prompt: find unsupported claims, check that every number in the draft exists in the state, flag missing analyses. Its output drives a conditional edge — revise or approve.
- Human review gate. An interrupt point where the graph pauses and waits for a person before finalizing.
Notice what this structure buys you. Each node has one job, so each prompt is short and testable. The critic is a different invocation from the writer, so it is not grading its own homework. And because the quant node is pure Python, an entire class of numerical hallucination is structurally impossible rather than merely discouraged.
Building the Graph: State Schema and Skeleton
Let us make this concrete. First, the state. In LangGraph you typically define state as a TypedDict, with reducer annotations for fields that accumulate.
from typing import Annotated, TypedDict
from langgraph.graph import StateGraph, START, END
from langgraph.graph.message import add_messages
class AnalysisState(TypedDict):
messages: Annotated[list, add_messages]
ticker: str
analysis_plan: dict # produced by the planner
fundamentals: dict # raw statements, with sources
ratios: dict # computed by pure Python
qualitative_notes: list # tagged observations from filings/news
draft_report: str
critique: dict # issues found by the critic
revision_count: int
approved: boolThe add_messages reducer appends new messages instead of overwriting, giving you a durable conversation and tool-call history. Everything else is plain data that nodes overwrite or extend.
Now the skeleton of the graph:
def planner(state: AnalysisState) -> dict:
plan = plan_llm.invoke(
f"Create an analysis plan for {state['ticker']}. "
f"User request: {state['messages'][-1].content}"
)
return {"analysis_plan": plan.model_dump()}
def quant(state: AnalysisState) -> dict:
f = state["fundamentals"]
ratios = {
"current_ratio": f["current_assets"] / f["current_liabilities"],
"debt_to_equity": f["total_debt"] / f["shareholder_equity"],
"gross_margin": f["gross_profit"] / f["revenue"],
"fcf_margin": f["free_cash_flow"] / f["revenue"],
}
return {"ratios": ratios}
builder = StateGraph(AnalysisState)
builder.add_node("planner", planner)
builder.add_node("data", data_node)
builder.add_node("quant", quant)
builder.add_node("qualitative", qualitative_node)
builder.add_node("synthesis", synthesis_node)
builder.add_node("critic", critic_node)
builder.add_edge(START, "planner")
builder.add_edge("planner", "data")
builder.add_edge("data", "quant")
builder.add_edge("quant", "qualitative")
builder.add_edge("qualitative", "synthesis")
builder.add_edge("synthesis", "critic")The quant node deserves a second look because it embodies the most important principle in this entire article: the LLM decides what to compute; Python computes it. The model never performs division. If a required field is missing from fundamentals, the node raises or records an explicit gap — it does not let the model improvise a plausible-looking number.
Wiring Up Tools: Market Data, Filings, and Calculators
The data node is where your agent touches the outside world, and LangGraph's tool-calling pattern keeps this clean. You define tools as decorated functions and bind them to the model; a dedicated tool-execution node runs whatever the model requests.
from langchain_core.tools import tool
@tool
def get_fundamentals(ticker: str, period: str = "annual") -> dict:
"""Fetch income statement, balance sheet, and cash flow
for a ticker from the fundamentals provider."""
return fundamentals_client.fetch(ticker, period)
@tool
def get_price_history(ticker: str, days: int = 365) -> dict:
"""Fetch daily OHLCV price history."""
return market_client.history(ticker, days)
@tool
def search_filings(ticker: str, query: str) -> list:
"""Search recent regulatory filings for passages
matching the query. Returns text with citations."""
return filings_index.search(ticker, query)
@tool
def compute_dcf(fcf: float, growth: float, discount: float,
terminal_growth: float, years: int = 5) -> dict:
"""Discounted cash flow calculation. All arithmetic in Python."""
flows = [fcf * (1 + growth) ** t / (1 + discount) ** t
for t in range(1, years + 1)]
terminal = (flows[-1] * (1 + terminal_growth)
/ (discount - terminal_growth)
/ (1 + discount) ** years)
return {"pv_flows": sum(flows), "terminal_value": terminal,
"enterprise_value": sum(flows) + terminal}Three design rules keep tool usage trustworthy in a financial context.
- Every tool returns provenance. The fundamentals tool should return not just numbers but the reporting period and source identifier, and the filings search should return document references alongside text. Provenance flows into state and eventually into the report's citations.
- Calculators are tools too. Giving the model a
compute_dcftool instead of asking it to "estimate intrinsic value" converts a hallucination surface into a deterministic function with inspectable inputs. The model's job shrinks to choosing reasonable assumptions — which is exactly the part you want a human to review later. - Fail loudly. If a provider times out or returns partial data, the tool should say so explicitly in its output. The routing logic can then decide to retry or degrade gracefully, rather than letting the synthesis node write around a silent gap.
For agent-driven tool use, LangGraph's prebuilt ToolNode and tools_condition give you the standard loop — model proposes tool calls, tools execute, results return to the model — with a few lines of wiring.
Conditional Routing: Encoding Analyst Judgment
Conditional edges are where a LangGraph agent stops being a fancy pipeline and starts behaving like an analyst. The routing function is ordinary Python that inspects state and returns the name of the next node.
The most valuable router in this system sits after the critic:
MAX_REVISIONS = 2
def route_after_critic(state: AnalysisState) -> str:
issues = state["critique"].get("blocking_issues", [])
if not issues:
return "human_review"
if state["revision_count"] >= MAX_REVISIONS:
return "human_review" # escalate with issues attached
return "synthesis" # revise the draft
builder.add_conditional_edges(
"critic",
route_after_critic,
{"synthesis": "synthesis", "human_review": "human_review"},
)This tiny function encodes three real policies: drafts with blocking issues get revised, revision loops are bounded so the graph always terminates, and anything that cannot be fixed automatically escalates to a human with the critique attached rather than being silently shipped.
You can apply the same pattern earlier in the graph. A router after the quant node can send companies with alarming leverage into a dedicated solvency deep-dive branch. A router after the data node can detect that a company is a bank or insurer — where standard ratios mislead — and route to sector-specific analysis nodes. Each such branch is just more nodes and edges; the graph grows to match the true shape of the domain instead of forcing every company through one path.
The critic node itself is worth dwelling on. Its prompt should be adversarial and mechanical: verify that every figure in the draft appears in state["ratios"] or state["fundamentals"], that every qualitative claim cites a note in qualitative_notes, and that the thesis acknowledges the strongest counterargument. Ask it to return structured JSON — a list of blocking issues and a list of suggestions — so the router consumes data, not vibes. In practice, this reflection loop is the single highest-leverage addition you can make to output quality, because it catches exactly the unsupported-claim failure mode that makes people distrust LLM analysis.
Checkpointing, Human-in-the-Loop, and the Audit Trail
Finance is a domain where "the agent did something, we think" is unacceptable. LangGraph's persistence layer is what makes the system defensible.
Compile the graph with a checkpointer, and every node execution writes a snapshot of the full state:
from langgraph.checkpoint.sqlite import SqliteSaver
checkpointer = SqliteSaver.from_conn_string("analysis.db")
graph = builder.compile(
checkpointer=checkpointer,
interrupt_before=["human_review"],
)
config = {"configurable": {"thread_id": "acme-2026-q2-review"}}
result = graph.invoke(
{"messages": [("user", "Full fundamental review of ACME")],
"ticker": "ACME", "revision_count": 0, "approved": False},
config,
)The interrupt_before argument is the human-in-the-loop mechanism: when execution reaches human_review, the graph stops and persists. A reviewer can then inspect the draft report, the critique, the computed ratios, and the raw data — the entire state — through whatever UI you build on top. To resume, you update the state with the reviewer's decision and invoke the graph again on the same thread_id:
graph.update_state(config, {"approved": True})
final = graph.invoke(None, config) # resumes from the checkpointThis buys you three things that matter enormously in this domain.
- Durability. A long analysis that dies at step six resumes from step six, not from zero. Data providers flake; your agent should not care.
- Auditability. The checkpoint history is a step-by-step record: what data arrived, what was computed, what the draft said before and after revision, and who approved it. When someone asks "why did the report say that?", you replay the thread instead of shrugging.
- Time-travel debugging. You can fork a thread from any historical checkpoint, tweak state, and re-run — invaluable when you are iterating on prompts for the synthesis or critic nodes and want to test against a fixed, known state.
A practical convention: make thread_id meaningful (entity plus review cycle, as above) so the persistence store doubles as a searchable archive of analyses.
Keeping the Numbers Honest: Guardrails That Actually Work
Even with tools and a critic, you should build in defense-in-depth against numerical and reasoning failures. These patterns have the best effort-to-payoff ratio.
Structured outputs everywhere. Have LLM nodes return Pydantic models rather than prose wherever possible — the planner's plan, the qualitative node's observations, the critic's findings. Validation failures then surface as retryable errors at the node boundary instead of corrupt prose flowing downstream.
A numeric reconciliation pass. Add a cheap deterministic node before human review that extracts every number from the draft report with a regex and checks each against the values present in state, within a tolerance. Any orphan number is a blocking issue. This is trivially simple and embarrassingly effective, because the failure it targets — the model rounding, transposing, or inventing a figure while writing fluent prose — is common and otherwise invisible.
Freshness and unit checks in the data node. Record the as-of date of every dataset and refuse to proceed (or loudly caveat) when data is stale relative to the plan's requirements. Normalize units and currency at ingestion, once, in code — mixing millions and billions or reporting currencies is a classic silent killer that no prompt reliably prevents.
Bounded loops with escalation. Every cycle in the graph — tool retry loops, revision loops — needs a counter in state and a hard ceiling, with the escape edge pointing at a human, not at END. An agent that gives up visibly is safe; one that loops forever or exits silently is not.
Scope the agent's authority. A financial analysis agent should analyze. Order execution, fund movement, or anything transactional belongs outside the graph, behind human action. Keeping the tool set read-only plus pure calculators makes the worst-case failure a bad document rather than a bad trade.
Running It in Production: Streaming, Memory, and Cost
A few operational notes from moving graphs like this beyond the notebook stage.
Stream progress, not just tokens. A full analysis run involves multiple LLM calls and data fetches, and users will not stare at a spinner for that long. LangGraph's streaming modes let you emit node-level events — "fetching fundamentals", "computing ratios", "critique found 2 issues, revising" — which turns dead air into a legible narrative of what the agent is doing. For financial users especially, watching the process builds exactly the trust the output needs.
Split memory into thread and store. Thread-level state (this analysis) lives in the checkpointer. Cross-thread knowledge — house style for reports, sector-specific ratio thresholds, a user's preferred peer sets — belongs in LangGraph's long-term store, keyed by namespace, so every new analysis starts informed without dragging along stale conversational baggage.
Match model size to node difficulty. Nothing forces every node to use the same model. Synthesis and critique benefit from a frontier model; extracting structured observations from a filing section or classifying a routing decision often works fine on a smaller, cheaper one. Because each node is independent, this optimization is a one-line change per node, and it typically cuts cost dramatically for a pipeline that runs many times a day.
Test nodes like functions, graphs like systems. Unit-test each node with synthetic states — a quant node is trivially testable, and even LLM nodes can be tested for schema compliance. Then keep a small suite of end-to-end runs on fixed input data with a checkpointer, and diff the resulting state trajectories when you change prompts. Regressions in agent behavior are much easier to catch as state diffs than as vibes about output quality.
Where to Go Next
You now have the full blueprint for a langgraph financial analysis agent: a typed state that acts as the working file, nodes that separate deterministic computation from language work, tools that carry provenance, conditional edges that encode process judgment, a critic loop that challenges drafts before humans see them, and checkpointing that turns every run into an auditable record. The deeper lesson travels beyond finance — whenever a task is multi-step, conditional, numerically sensitive, and accountable, the graph is the right abstraction, and the same seven-node skeleton adapts to legal review, medical literature summaries, or engineering incident analysis with different tools and prompts.
The best way to internalize these patterns is to build the graph yourself, break it, and watch the checkpoints to understand why. If you want a structured path — from your first two-node graph through tool calling, conditional routing, persistence, human-in-the-loop, and multi-agent patterns, with projects you can put in a portfolio — the LangGraph Tutorial course on teachyou.ai walks through every concept in this article step by step, with Ira Menon and me building working agents on camera. Start with the state schema, keep the math in Python, and let the graph do what prompts alone never could.
BootcampA 30-day guided bootcamp: build, harden and ship a production autonomous agent from scratch.
AI AgentsUnderstand how AI agents really work: the loop, the tools, the memory, and why most agent projects fail.