How to Build a document extraction Agent Using LangGraph in TypeScript for banking
A document extraction agent for banking takes PDFs, scans, and images, pulls out structured fields like account numbers, names, amounts, dates, and entity types, then routes the result into downstream systems with traceability. It matters because banks don’t just need extraction accuracy; they need auditability, compliance controls, and deterministic handling when a document is incomplete or ambiguous.
Architecture
- •
Input ingestion layer
- •Accepts PDF, TIFF, PNG, or JPEG documents from branch uploads, email intake, or S3-compatible storage.
- •Normalizes file metadata like source system, customer ID, region, and retention policy.
- •
OCR / text extraction layer
- •Uses OCR for scanned documents and direct text parsing for digital PDFs.
- •Produces page-level text plus coordinates when available for field validation.
- •
Extraction model node
- •Calls an LLM or document parser to extract banking fields into a strict JSON schema.
- •Keeps output constrained to known keys like
customerName,iban,invoiceTotal,currency,documentType.
- •
Validation and policy node
- •Checks required fields, format rules, confidence thresholds, and jurisdiction-specific constraints.
- •Rejects or escalates documents that fail compliance checks.
- •
Human review / exception handling node
- •Routes low-confidence or high-risk cases to a reviewer queue.
- •Stores the model output, source document reference, and decision trail.
- •
Audit persistence layer
- •Writes every state transition to an immutable log or database table.
- •Supports internal audit, model governance, and regulatory review.
Implementation
1) Define the state and graph nodes
In LangGraph for TypeScript, the clean pattern is to keep the agent state explicit and pass it through each node. For banking workflows, that state should carry the source document metadata, extracted text, structured result, validation flags, and audit trail.
import { Annotation } from "@langchain/langgraph";
export type ExtractedFields = {
documentType?: "invoice" | "bank_statement" | "id" | "other";
customerName?: string;
accountNumber?: string;
iban?: string;
currency?: string;
totalAmount?: number;
};
export const DocumentState = Annotation.Root({
filePath: Annotation<string>(),
sourceSystem: Annotation<string>(),
region: Annotation<string>(),
rawText: Annotation<string>({
default: () => "",
reducer: (_prev, next) => next,
}),
extracted: Annotation<ExtractedFields>({
default: () => ({}),
reducer: (_prev, next) => ({ ..._prev, ...next }),
}),
confidence: Annotation<number>({
default: () => 0,
reducer: (_prev, next) => next,
}),
needsReview: Annotation<boolean>({
default: () => false,
reducer: (_prev, next) => next,
}),
auditTrail: Annotation<string[]>({
default: () => [],
reducer: (prev, next) => prev.concat(next),
}),
});
2) Build the extraction node with a strict schema
Use a real LangChain chat model inside a LangGraph node. For banking use cases, keep the prompt narrow and force structured output. If you let the model free-form its response here, you will spend your time cleaning up bad JSON and missing fields.
import { ChatOpenAI } from "@langchain/openai";
import { StateGraph } from "@langchain/langgraph";
const llm = new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0,
});
const extractNode = async (state: typeof DocumentState.State) => {
const prompt = `
Extract banking document fields from this text.
Return only valid JSON with keys:
documentType, customerName, accountNumber, iban, currency, totalAmount
Rules:
- If a field is missing or unclear, omit it.
- Do not guess.
- Use numeric value for totalAmount.
Text:
${state.rawText}
`;
const result = await llm.invoke(prompt);
const content = typeof result.content === "string" ? result.content : "";
const parsed = JSON.parse(content);
return {
extracted: parsed,
auditTrail: [`extracted:${state.sourceSystem}:${state.filePath}`],
confidence: parsed.totalAmount ? 0.9 : 0.6,
needsReview: false,
};
};
3) Add validation and routing for review
This is where banking workflows differ from generic document automation. You need hard checks for required fields and jurisdiction-specific policy. For example, if the document came from an EU customer but lacks IBAN formatting or contains unsupported data residency metadata, route it to review instead of auto-posting it downstream.
const validateNode = async (state: typeof DocumentState.State) => {
const { extracted } = state;
defconst:
? // no-op
Use this actual implementation instead:
const validateNode = async (state: typeof DocumentState.State) => {
const { extracted } = state;
const hasRequiredFields =
!!extracted.documentType &&
!!extracted.customerName &&
(!!extracted.accountNumber || !!extracted.iban);
const validIban =
!extracted.iban || /^[A-Z]{2}\d{2}[A-Z0-9]{11,30}$/.test(extracted.iban);
return {
needsReview:
!hasRequiredFields ||
!validIban ||
state.confidence < Math.max(0.85),
auditTrail: [
`validated:${hasRequiredFields ? "pass" : "fail"}:${state.region}`,
],
};
};
Now wire the graph together with StateGraph, add a conditional edge for human review versus automated completion:
const reviewNode = async (state: typeof DocumentState.State) => ({
auditTrail: [`review_required:${state.filePath}`],
});
const completeNode = async (state: typeof DocumentState.State) => ({
auditTrail: [`completed:${state.filePath}`],
});
const workflow = new StateGraph(DocumentState)
.addNode("extract", extractNode)
.addNode("validate", validateNode)
.addNode("review", reviewNode)
.addNode("complete", completeNode)
.addEdge("__start__", "extract")
.addEdge("extract", "validate")
.addConditionalEdges("validate", (state) =>
state.needsReview ? "review" : "complete"
)
.addEdge("review", "__end__")
.addEdge("complete", "__end__");
export const app = workflow.compile();
4) Execute the agent with document metadata
Keep execution inputs explicit so every run can be traced back to its source system and region. That matters for audit logs and residency controls.
async function main() {
await app.invoke({
filePath: "/data/incoming/customer_123_statement.pdf",
sourceSystem: "branch-upload",
region: "eu-west-1",
rawText:
"ACME Bank Statement\nCustomer Name: Jane Doe\nIBAN: GB29NWBK60161331926819\nCurrency: GBP\nTotal Amount: 1250.75",
});
}
main().catch(console.error);
Production Considerations
- •
Data residency
Keep OCR outputs and extracted payloads in-region. If your bank operates in the EU or APAC with local storage requirements, don’t ship raw documents to a cross-region inference endpoint without legal approval.
- •
Audit logging
Persist every input hash, node decision, model version, prompt version, and final output. Auditors care about why a field was accepted or rejected more than they care about your prompt engineering tricks.
- •
Guardrails
Enforce schema validation before downstream writes. Use deterministic rules for account numbers, IBANs, dates of birth ranges if identity docs are involved, and reject any extraction that exceeds confidence thresholds.
- •
Monitoring
Track extraction accuracy by document type and source channel. Branch-scanned PDFs will behave differently from digitally generated statements; separate those metrics so you can spot OCR regressions quickly.
Common Pitfalls
- •
Letting the model invent missing fields
This is the fastest way to create silent data corruption. Fix it by instructing “do not guess,” using strict schema validation after extraction، and routing uncertain cases to review.
- •
Skipping provenance
If you don’t store source file references and model versions in the state or audit log، you won’t be able to explain outcomes later. In banking that becomes a governance problem fast.
- •
Ignoring regional constraints
Teams often prototype on a hosted model endpoint in one region and then discover production data residency restrictions later. Decide upfront where documents are processed، where logs live، and what leaves the boundary.
Keep learning
- •The complete AI Agents Roadmap — my full 8-step breakdown
- •Free: The AI Agent Starter Kit — PDF checklist + starter code
- •Work with me — I build AI for banks and insurance companies
By Cyprian Aarons, AI Consultant at Topiax.
Want the complete 8-step roadmap?
Grab the free AI Agent Starter Kit — architecture templates, compliance checklists, and a 7-email deep-dive course.
Get the Starter Kit