This tutorial demonstrates how to build an agentic pipeline for automated medical coding using Pydantic AI and LangGraph to reduce revenue leakage and improve billing accuracy.

Introduction

The healthcare revenue cycle management (RCM) industry is currently facing a significant bottleneck: the manual translation of clinical documentation into standardized medical codes. Medical coding involves assigning specific alphanumeric codes—primarily ICD-10-CM (International Classification of Diseases, 10th Revision, Clinical Modification) for diagnoses and CPT (Current Procedural Terminology) for services—to patient encounters.

Traditional methods rely on human coders reviewing electronic health records (EHR), which is time-consuming, prone to error, and expensive. Errors in this stage lead to claim denials, delayed payments, and potential compliance audits. While legacy Optical Character Recognition (OCR) systems have attempted to automate this, they often fail to capture the clinical context required for accurate code selection, such as distinguishing between a primary diagnosis and a secondary complication.

This tutorial explores a modern technical solution: Intelligent Document Processing (IDP) using an agentic architecture. By leveraging Pydantic AI for structured data extraction and LangGraph for workflow orchestration, we can build a system that not only extracts data but also validates it against medical logic and incorporates human-in-the-loop (HITL) checkpoints for high-stakes decisions.

Objectives

By the end of this tutorial, you will:

  1. Define robust Pydantic schemas for medical entities to ensure type-safe LLM outputs.
  2. Implement an extraction agent using Pydantic AI that handles unstructured clinical notes.
  3. Construct a multi-stage validation workflow using LangGraph to manage state and human-in-the-loop transitions.
  4. Understand how to integrate automated ICD-10 validation logic into an agentic pipeline.

Prerequisites

To follow this tutorial, you will need the following tools and environment:

  • Python 3.12+: The latest stable version of Python is recommended for better type hinting support. Python Documentation.
  • Pydantic AI: A data-centric agent framework for building production-grade applications. Pydantic AI Docs.
  • LangGraph: A library for building stateful, multi-actor applications with LLMs. LangGraph Docs.
  • Docker: For containerizing the application and managing local development environments. Docker Docs.
  • OpenAI or Gemini API Key: To power the underlying Large Language Model (LLM).

Implementation / Step-by-Step

Step 1: Project Initialization and Environment Setup

Begin by creating a structured project directory. In professional environments, maintaining a clear separation between schemas, agents, and workflow logic is critical for maintainability.

$ mkdir medical-coding-automation
$ cd medical-coding-automation
$ python -m venv venv
$ source venv/bin/activate
$ pip install pydantic-ai langgraph openai python-dotenv

Create a .env file to store your credentials:

OPENAI_API_KEY=your_api_key_here
LOG_LEVEL=INFO

Step 2: Defining the Medical Schema with Pydantic

The foundation of any IDP system is the data model. In medical billing, we cannot afford "hallucinations" or malformed JSON. We use Pydantic to define the exact structure we expect from the LLM. This ensures that every extracted field adheres to the required data types and constraints.

from typing import List, Optional
from pydantic import BaseModel, Field

class Diagnosis(BaseModel):
    code: str = Field(description="The ICD-10-CM code, e.g., 'E11.9'")
    description: str = Field(description="The clinical description of the diagnosis")
    is_primary: bool = Field(description="Whether this is the primary reason for the encounter")
    confidence_score: float = Field(ge=0, le=1, description="Model's confidence in this code")

class Procedure(BaseModel):
    cpt_code: str = Field(description="The 5-digit CPT code for the procedure")
    description: str = Field(description="Description of the service provided")
    modifiers: List[str] = Field(default_factory=list, description="CPT modifiers if applicable")

class MedicalRecord(BaseModel):
    patient_id: str
    encounter_date: str
    diagnoses: List[Diagnosis]
    procedures: List[Procedure]
    summary: str = Field(description="A brief clinical summary of the encounter")

Using Field descriptions is not just for documentation; Pydantic AI uses these descriptions as part of the system prompt to guide the LLM toward more accurate extraction.

Step 3: Building the Extraction Agent with Pydantic AI

Pydantic AI simplifies the process of forcing an LLM to return structured data. Unlike standard LangChain wrappers, Pydantic AI is built specifically for data validation and type safety.

We will create an agent that takes a raw clinical note and returns a populated MedicalRecord object.

import os
from pydantic_ai import Agent
from dotenv import load_dotenv

load_dotenv()

# Define the extraction agent
extraction_agent = Agent(
    'openai:gpt-4o',
    result_type=MedicalRecord,
    system_prompt=(
        "You are an expert medical coder. Your task is to extract ICD-10-CM "
        "and CPT codes from clinical notes. Ensure that the primary diagnosis "
        "is correctly identified. If a code is ambiguous, provide the most "
        "specific code possible based on the documentation."
    ),
)

async def extract_medical_data(clinical_note: str) -> MedicalRecord:
    """
    Uses the Pydantic AI agent to process clinical text.
    """
    result = await extraction_agent.run(clinical_note)
    return result.data

The result_type=MedicalRecord parameter is the most important part of this block. It instructs the agent to use tool-calling or functional-calling mechanisms to ensure the output matches our Pydantic model exactly. If the LLM returns invalid JSON, Pydantic AI will automatically attempt to retry or raise a validation error.

Step 4: Orchestrating the Workflow with LangGraph

In a real-world medical billing environment, extraction is only the first step. We need a workflow that:

  1. Extracts data.
  2. Validates codes against a database or external API.
  3. Routes the record to a human coder if the confidence score is low.
  4. Finalizes the record for billing.

LangGraph is ideal for this because it treats the workflow as a state machine. This allows for cycles (e.g., sending a record back for re-extraction if validation fails) and persistence.

from typing import TypedDict, Annotated, Union
from langgraph.graph import StateGraph, END

# Define the state of our workflow
class GraphState(TypedDict):
    clinical_note: str
    extracted_data: Optional[MedicalRecord]
    validation_errors: List[str]
    status: str  # 'extracted', 'validated', 'needs_review', 'finalized'

def extraction_node(state: GraphState):
    """Node for extracting data from clinical notes."""
    # In a real app, this would call the async function from Step 3
    # For the tutorial, we simulate the extraction
    note = state['clinical_note']
    # Logic to call extraction_agent.run() goes here
    return {"status": "extracted", "extracted_data": ...}

def validation_node(state: GraphState):
    """Node for validating extracted codes."""
    data = state['extracted_data']
    errors = []
    
    # Example validation: Check if ICD-10 codes follow the correct format
    for diag in data.diagnoses:
        if len(diag.code) < 3:
            errors.append(f"Invalid ICD-10 code: {diag.code}")
            
    if errors:
        return {"status": "needs_review", "validation_errors": errors}
    return {"status": "validated"}

def review_router(state: GraphState):
    """Determines whether to go to human review or finish."""
    if state['status'] == "needs_review":
        return "human_review"
    return "end"

# Initialize the Graph
workflow = StateGraph(GraphState)

# Add Nodes
workflow.add_node("extract", extraction_node)
workflow.add_node("validate", validation_node)

# Define Edges
workflow.set_entry_point("extract")
workflow.add_edge("extract", "validate")

# Add Conditional Logic
workflow.add_conditional_edges(
    "validate",
    review_router,
    {
        "human_review": END, # In a full system, this would point to a UI node
        "end": END
    }
)

app = workflow.compile()

Why use LangGraph's StateGraph over a simple loop?

In medical coding, the process is rarely linear. A simple Python loop or a chain of functions lacks "check-pointing." If a system crashes during a human review phase that takes 24 hours, a standard script loses its state. LangGraph allows us to save the state of the graph to a database (like Postgres or Redis). When the human coder finally reviews the record, the graph can resume exactly where it left off.

Furthermore, StateGraph allows for complex error handling. If the validation_node identifies that the LLM missed a procedure mentioned in the text, it can route the state back to the extraction_node with a specific instruction to look for that procedure, creating a self-correcting loop.

Step 5: Handling Human-in-the-Loop (HITL)

In healthcare, 100% automation is often not the goal due to compliance and liability. The goal is "augmented intelligence." We can use LangGraph's interrupt feature to pause execution when a record is flagged for review.

When the status is set to needs_review, the system can trigger a notification to a dashboard. The process only continues once a human provides an Action to either override the data or approve it.

Step 6: Comparison of Approaches

When implementing AI in medical billing, it is important to evaluate the trade-offs between manual processes, traditional OCR, and the agentic IDP approach described here.

| Metric | Manual Coding | Traditional OCR | Agentic AI (Pydantic AI + LangGraph) | | : | : | : | : | | Accuracy | High (but inconsistent) | Low (template dependent) | High (context-aware) | | Throughput | Low (15-20 mins/chart) | High (seconds) | High (seconds to minutes) | | Contextual Understanding | Excellent | Poor | Excellent | | Cost per Claim | High ($5 - $15) | Low ($0.10 - $0.50) | Moderate ($0.50 - $2.00) | | Scalability | Difficult (requires hiring) | Easy | Easy | | Error Handling | Manual | None (fails silently) | Automated with Human-in-the-Loop |

Step 7: Deployment and Compliance Considerations

When deploying this system for European enterprises, GDPR compliance is paramount. Medical data (Protected Health Information or PHI) must be handled with extreme care.

  1. Data Residency: Ensure that the LLM provider (e.g., Azure OpenAI or Google Vertex AI) is configured to process data within the EU.
  2. Data Minimization: Before sending clinical notes to the LLM, use a local PII (Personally Identifiable Information) de-identification layer (e.g., Presidio) to strip names, phone numbers, and addresses.
  3. Audit Logs: LangGraph's state history serves as an immutable audit log, showing exactly what the AI extracted and what the human coder changed. This is vital for healthcare audits.
  4. Private Cloud: For maximum security, deploy the entire stack—including the LLM (using local models like Llama 3 or Mistral via vLLM)—within a private VPC on AWS or Azure.

Conclusion

Transitioning from manual medical coding to an agentic AI pipeline significantly reduces the administrative burden on healthcare providers while minimizing revenue leakage. By combining the structured validation of Pydantic AI with the stateful orchestration of LangGraph, developers can build systems that are not only fast but also reliable and compliant. This architecture ensures that clinical context is preserved, codes are validated against medical standards, and humans remain in control of high-stakes decisions.

Azura AI assists European enterprises in deploying these high-precision agentic systems within secure, GDPR-compliant private cloud environments.