How to Build a compliance checking Agent Using LangChain in TypeScript for insurance
A compliance checking agent for insurance reviews policy wording, customer communications, claims notes, and underwriting outputs against internal rules and regulatory constraints. It matters because small wording mistakes can trigger regulatory exposure, bad disclosures, or inconsistent treatment of customers across regions.
Architecture
- •
Input layer
- •Accepts text from policy docs, claim summaries, email drafts, or chat transcripts.
- •Normalizes the payload into a structured request with metadata like jurisdiction, product line, and document type.
- •
Compliance rule store
- •Holds insurer-specific rules: mandatory disclosures, prohibited phrases, escalation thresholds, and regional constraints.
- •Backed by a vector store or a deterministic rules engine depending on the rule type.
- •
Retriever
- •Pulls the most relevant policies, regulations, and internal controls for the current case.
- •Filters by jurisdiction and line of business to avoid cross-region leakage.
- •
LLM reasoning layer
- •Uses LangChain to compare the input against retrieved rules.
- •Produces a structured compliance assessment with findings, severity, and remediation steps.
- •
Audit logger
- •Persists prompts, retrieved sources, model outputs, and final decisions.
- •Required for traceability in regulated insurance workflows.
- •
Human review handoff
- •Escalates borderline cases to compliance officers.
- •Keeps the agent advisory rather than fully autonomous for high-risk decisions.
Implementation
1) Install dependencies and define your data model
Use LangChain JS packages that are actually maintained in TypeScript projects. For insurance compliance checks, keep the output schema strict so downstream systems can trust it.
npm install langchain @langchain/openai @langchain/core zod
import { z } from "zod";
export const ComplianceResultSchema = z.object({
compliant: z.boolean(),
severity: z.enum(["low", "medium", "high"]),
findings: z.array(
z.object({
ruleId: z.string(),
issue: z.string(),
recommendation: z.string(),
})
),
summary: z.string(),
});
export type ComplianceResult = z.infer<typeof ComplianceResultSchema>;
export type ComplianceRequest = {
text: string;
jurisdiction: "US" | "UK" | "EU";
productLine: "life" | "health" | "motor" | "property";
documentType: "policy" | "claim_note" | "customer_email" | "underwriting_note";
};
2) Load insurance compliance rules into a retriever
In production you would source these from approved legal/compliance content. For this example, we use MemoryVectorStore with OpenAIEmbeddings, but the same pattern works with Pinecone, pgvector, or Elasticsearch.
import { Document } from "@langchain/core/documents";
import { MemoryVectorStore } from "langchain/vectorstores/memory";
import { OpenAIEmbeddings } from "@langchain/openai";
const ruleDocs = [
new Document({
pageContent:
"US life insurance communications must not imply guaranteed approval before underwriting review.",
metadata: { ruleId: "US-LIFE-001", jurisdiction: "US", productLine: "life" },
}),
new Document({
pageContent:
"Claims correspondence must not promise payment timelines unless confirmed by claims operations.",
metadata: { ruleId: "GEN-CLAIM-002", jurisdiction: "US", productLine: "property" },
}),
];
const embeddings = new OpenAIEmbeddings({ model: "text-embedding-3-small" });
const vectorStore = await MemoryVectorStore.fromDocuments(ruleDocs, embeddings);
export async function getRelevantRules(req: ComplianceRequest) {
const retriever = vectorStore.asRetriever(4);
const docs = await retriever.getRelevantDocuments(req.text);
return docs.filter((doc) => {
const j = doc.metadata.jurisdiction as string;
const p = doc.metadata.productLine as string;
return (!j || j === req.jurisdiction) && (!p || p === req.productLine);
});
}
3) Build the LangChain chain with structured output
This is the core pattern. We pass the request plus retrieved rules into a prompt and force a typed JSON response using withStructuredOutput.
import { ChatOpenAI } from "@langchain/openai";
import { ChatPromptTemplate } from "@langchain/core/prompts";
import { RunnableLambda } from "@langchain/core/runnables";
import { ComplianceResultSchema } from "./schema";
const model = new ChatOpenAI({
model: "gpt-4o-mini",
temperature: 0,
});
const prompt = ChatPromptTemplate.fromMessages([
[
"system",
[
"You are an insurance compliance reviewer.",
"Use only the provided rules.",
"If the text is ambiguous or missing required disclosures, mark it non-compliant.",
"Return concise findings suitable for audit review.",
].join(" "),
],
[
"human",
`Jurisdiction: {jurisdiction}
Product line: {productLine}
Document type: {documentType}
Text:
{text}
Relevant rules:
{rules}`,
],
]);
const structuredModel = model.withStructuredOutput(ComplianceResultSchema);
export const complianceChain = RunnableLambda.from(async (req) => {
const relevantRules = await getRelevantRules(req);
const rulesText = relevantRules
.map((doc) => `[${doc.metadata.ruleId}] ${doc.pageContent}`)
.join("\n");
const result = await prompt.pipe(structuredModel).invoke({
jurisdiction: req.jurisdiction,
productLine: req.productLine,
documentType: req.documentType,
text: req.text,
rules: rulesText || "No matching rules found.",
});
return {
...result,
matchedRules: relevantRules.map((d) => d.metadata.ruleId),
};
});
4) Execute it and persist audit data
For insurance workflows you need an immutable trail. At minimum log request metadata, matched rules, model version, timestamp, and final decision.
async function auditLog(entry: unknown) {
console.log(JSON.stringify(entry));
}
const request = {
text:
"Your claim will be paid within five business days after submission of all documents.",
jurisdiction: "US",
productLine: "property",
documentType: "customer_email",
} satisfies ComplianceRequest;
const result = await complianceChain.invoke(request);
await auditLog({
timestamp: new Date().toISOString(),
model: "gpt-4o-mini",
request,
result,
});
console.log(result);
Production Considerations
- •
Data residency
- •Keep EU policy data and customer communications in-region if your regulatory obligations require it.
- •Use region-specific vector stores and separate API keys per deployment boundary.
- •
Auditability
- •Persist prompt inputs, retrieved rule IDs, model output, and human overrides.
- •Store hashes of source documents so you can prove what was reviewed without duplicating sensitive content everywhere.
- •
Guardrails
- •Add deterministic checks before the LLM step for hard rules like prohibited phrases or missing mandatory disclosures.
- •Route high-severity findings to humans instead of auto-releasing customer-facing messages.
- •
Monitoring
- •Track false positives by product line and jurisdiction.
- •Alert on retrieval failures, empty rule matches, or spikes in “high” severity outcomes after policy updates.
Common Pitfalls
- •
Using one global rule index for all regions
- •This causes cross-jurisdiction contamination.
- •Split indexes by region or enforce strict metadata filtering before generation.
- •
Letting the model decide hard compliance facts
- •The LLM should interpret context, not invent regulatory requirements.
- •Encode non-negotiable checks in code or policy engines first.
- •
Skipping structured output
- •Free-form answers are hard to audit and brittle in downstream systems.
- •Use
withStructuredOutput()with Zod so every response has predictable fields.
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