How to Build a document extraction Agent Using CrewAI in TypeScript for fintech
A document extraction agent takes unstructured files like bank statements, invoices, KYC forms, and loan applications, then turns them into structured JSON your downstream systems can trust. In fintech, that matters because manual extraction is slow, expensive, and error-prone, and every bad field can turn into a compliance issue, a fraud miss, or a broken underwriting decision.
Architecture
- •
Input layer
- •Accept PDFs, images, and scanned documents from S3, blob storage, or an upload service.
- •Normalize file metadata early: tenant ID, document type, jurisdiction, retention policy.
- •
Document parsing layer
- •Use OCR or text extraction before the agent sees the content.
- •Keep page boundaries and source offsets so you can trace every extracted field back to the original document.
- •
CrewAI orchestration layer
- •One agent handles classification and extraction.
- •A second agent validates schema completeness and flags ambiguous fields.
- •A third agent can do compliance checks for PII handling and jurisdiction-specific rules.
- •
Schema validation layer
- •Enforce a strict output contract with Zod or JSON Schema.
- •Reject partial outputs that don’t meet required fields for KYC/AML workflows.
- •
Audit and observability layer
- •Store raw input hashes, model version, prompt version, extracted JSON, and confidence scores.
- •Log every decision path for audit review.
Implementation
1) Install dependencies and define the extraction schema
Use CrewAI’s TypeScript package plus a validator. For fintech work, don’t let the model invent fields; define exactly what you expect.
npm install @crewai/crewai zod dotenv
import { z } from "zod";
export const BankStatementSchema = z.object({
accountHolderName: z.string(),
accountNumberLast4: z.string(),
statementPeriodStart: z.string(), // ISO date
statementPeriodEnd: z.string(), // ISO date
bankName: z.string(),
currency: z.string(),
openingBalance: z.number(),
closingBalance: z.number(),
transactions: z.array(
z.object({
date: z.string(),
description: z.string(),
amount: z.number(),
balance: z.number().optional(),
})
),
});
export type BankStatement = z.infer<typeof BankStatementSchema>;
2) Create the CrewAI agents and task
The pattern here is simple: one agent extracts into the schema, another checks for consistency. Keep the prompts narrow. Fintech agents should not “interpret” missing values unless your policy allows it.
import "dotenv/config";
import { Agent, Task, Crew } from "@crewai/crewai";
import { BankStatementSchema } from "./schema";
const extractor = new Agent({
role: "Document Extraction Specialist",
goal: "Extract structured banking data from raw document text without guessing",
backstory:
"You extract financial document fields accurately and preserve source fidelity.",
});
const validator = new Agent({
role: "Financial Data Validator",
goal: "Verify extracted data matches the source text and schema requirements",
backstory:
"You check for missing values, inconsistent dates, and suspicious totals.",
});
const extractionTask = new Task({
description: `
Extract the following from the provided bank statement text:
- accountHolderName
- accountNumberLast4
- statementPeriodStart
- statementPeriodEnd
- bankName
- currency
- openingBalance
- closingBalance
- transactions
Rules:
- Return only valid JSON.
- Do not infer values not present in the source.
- If a field is missing, use null only when allowed by policy; otherwise mark it as an error in notes.
`,
expectedOutput: "JSON matching the bank statement schema",
});
3) Run the crew and validate output before persistence
This is where you connect the agent to actual document text. In production you’d feed OCR output here. After execution, validate with Zod before writing anything to your database or case management system.
async function extractBankStatement(documentText: string) {
const crew = new Crew({
agents: [extractor, validator],
tasks: [extractionTask],
verbose: true,
process: "sequential",
});
const result = await crew.kickoff({
inputs: {
document_text: documentText,
output_schema_hint: BankStatementSchema.toString(),
},
});
const raw = typeof result === "string" ? result : String(result);
const parsed = JSON.parse(raw);
const validated = BankStatementSchema.parse(parsed);
return validated;
}
async function main() {
const ocrText = `
BANK OF EXAMPLE
Account Holder: Jane Doe
Account No.: ****1234
Statement Period: 2024-01-01 to 2024-01-31
Currency: USD
Opening Balance: 1200.50
Closing Balance: 980.25
Transactions:
2024-01-03 Coffee Shop -12.75 Balance=1187.75
2024-01-10 Payroll +2500.00 Balance=3687.75
`;
const data = await extractBankStatement(ocrText);
console.log(data);
}
main().catch(console.error);
4) Add audit metadata around every extraction
Fintech teams need traceability. Store enough context to reconstruct why a field exists without storing more PII than necessary.
type AuditRecord = {
tenantId: string;
documentHash: string;
modelProvider?: string;
promptVersion?: string;
extractedAtIso?: string;
};
function buildAuditRecord(tenantId: string, documentTextHash: string): AuditRecord {
return {
tenantId,
documentHash:
documentTextHash,
extractedAtIso:
new Date().toISOString(),
promptVersion:
"bank-statement-v1",
modelProvider:
process.env.CREWAI_MODEL_PROVIDER ?? "unknown",
};
}
Production Considerations
- •Deployment
Keep OCR and extraction in separate services. That lets you scale OCR-heavy workloads independently from LLM calls and keeps sensitive documents inside your approved network boundary.
- •Monitoring
Track extraction accuracy by document type, tenant, and region. Watch for schema failures, empty extractions, hallucinated values, and latency spikes after prompt changes.
- •Guardrails
Add hard validation for required fields like account number suffixes, dates, totals, and currency codes. For regulated workflows like KYC or lending decisions, route low-confidence outputs to human review instead of auto-persisting them.
- •Compliance and residency
Pin processing to approved regions when documents contain customer PII or financial records. Log retention policies by jurisdiction so GDPR/UK GDPR/CCPA handling does not get mixed across tenants.
Common Pitfalls
- •
Letting the model free-form the output
- •This breaks downstream systems fast.
- •Avoid it by validating against Zod or JSON Schema before anything is saved or sent to underwriting/KYC pipelines.
- •
Ignoring OCR quality
- •Bad OCR means bad extraction no matter how good the agent prompt is.
- •Use preprocessing for skew correction, DPI normalization, and page segmentation before calling CrewAI.
- •
Skipping audit trails
- •In fintech you will eventually be asked why a field was accepted.
- •Persist prompt version, model version, source hash, validation errors, and human override actions for every run.
If you build this pattern correctly, CrewAI becomes the orchestration layer on top of deterministic parsing and strict validation—not a magic box. That’s the right shape for fintech systems where correctness beats creativity every time.
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