← Back to DevBytes

Building a Data Extraction Pipeline with Claude Code: Complete Guide

Introduction to Data Extraction Pipelines with Claude Code

Modern applications sit on top of oceans of unstructured data — PDFs, emails, web pages, scanned receipts, and legacy database dumps. Building a reliable pipeline that converts this chaos into clean, structured records has traditionally required brittle regex patterns, fragile OCR integrations, and endless manual tuning. Claude Code changes the equation by giving you an AI agent that can read, reason about, and transform data directly from your terminal, orchestrating extraction logic with the same tooling you already use for software development.

In this guide, you'll learn what a Claude Code-powered extraction pipeline is, why it matters, and how to build one end-to-end with practical, runnable examples.

What Is a Data Extraction Pipeline with Claude Code?

A data extraction pipeline is a sequence of steps that ingests raw, heterogeneous documents and emits structured, validated data ready for storage or downstream analysis. When powered by Claude Code, the pipeline delegates the "understanding" step — parsing ambiguous layouts, inferring schemas, normalizing values — to Claude's language and vision capabilities, while keeping orchestration, validation, and persistence in deterministic code.

Claude Code is Anthropic's CLI-based agentic coding assistant. It can read files, run shell commands, edit source, and call the Claude API. This makes it uniquely suited for extraction work because it can:

Why It Matters

Traditional extraction pipelines fail in predictable ways. Regex breaks when a vendor changes an invoice layout. OCR libraries misread tables. Hand-coded parsers require constant maintenance. By placing Claude in the loop, you gain three concrete advantages:

The result is a pipeline that is cheaper to build, easier to maintain, and dramatically more accurate on messy real-world inputs.

Prerequisites and Project Setup

Before building, ensure you have the following installed and configured:

Create the project skeleton:

mkdir extraction-pipeline && cd extraction-pipeline
mkdir -p input output src schemas
echo '{"version":"0.1.0"}' > package.json
pip install anthropic pydantic python-dotenv

Verify Claude Code is available:

claude --version

Designing the Pipeline Architecture

A robust pipeline separates concerns into discrete stages. The architecture we'll build has five stages:

This separation lets you retry individual stages, swap out implementations, and monitor each step independently.

Defining Schemas with Pydantic

Schemas are the contract between Claude's flexible output and your deterministic downstream code. Pydantic models give you runtime validation and can be serialized to JSON Schema for the Claude API.

Create src/schemas.py:

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


class DocumentType(str, Enum):
    INVOICE = "invoice"
    RECEIPT = "receipt"
    CONTRACT = "contract"
    UNKNOWN = "unknown"


class LineItem(BaseModel):
    description: str = Field(..., description="Item or service description")
    quantity: float = Field(1, ge=0)
    unit_price: float = Field(..., ge=0)
    total: float = Field(..., ge=0)


class InvoiceRecord(BaseModel):
    document_type: DocumentType = DocumentType.INVOICE
    invoice_number: str
    vendor_name: str
    vendor_address: Optional[str] = None
    issue_date: str = Field(..., description="ISO 8601 date YYYY-MM-DD")
    due_date: Optional[str] = None
    currency: str = Field("USD", max_length=3)
    line_items: List[LineItem]
    subtotal: float
    tax: float = 0.0
    total: float
    notes: Optional[str] = None

This model will be converted into a JSON schema that Claude must satisfy. The field descriptions guide Claude toward correct extraction.

Building the Extraction Core

The extraction core calls the Claude API with the document text and the JSON schema. We use the tool_use mechanism to enforce structured output, which is more reliable than prompting for raw JSON.

Create src/extractor.py:

import json
from anthropic import Anthropic
from schemas import InvoiceRecord

client = Anthropic()

SCHEMA = InvoiceRecord.model_json_schema()

EXTRACTION_TOOL = {
    "name": "save_invoice",
    "description": "Save a structured invoice record extracted from the document.",
    "input_schema": SCHEMA,
}

SYSTEM_PROMPT = (
    "You are a precise data extraction engine. Read the provided document "
    "and extract invoice fields exactly as defined by the save_invoice tool. "
    "If a field is not present, omit it rather than guessing. Always use "
    "ISO 8601 dates and numeric amounts without currency symbols."
)


def extract_invoice(document_text: str) -> dict:
    response = client.messages.create(
        model="claude-sonnet-4-5-20250929",
        max_tokens=4096,
        system=SYSTEM_PROMPT,
        tools=[EXTRACTION_TOOL],
        tool_choice={"type": "tool", "name": "save_invoice"},
        messages=[
            {
                "role": "user",
                "content": f"Extract the invoice from this document:\n\n{document_text}",
            }
        ],
    )

    for block in response.content:
        if block.type == "tool_use" and block.name == "save_invoice":
            return block.input

    raise RuntimeError("Claude did not return a tool_use block")

Forcing tool_choice guarantees Claude returns a structured object conforming to the schema. This eliminates most JSON parsing failures.

Handling Different File Formats

Real pipelines receive PDFs, images, and HTML, not just plain text. Add a loader module that normalizes inputs into text or base64-encoded images for Claude's vision support.

Create src/loaders.py:

import base64
from pathlib import Path


def load_file(path: Path) -> dict:
    suffix = path.suffix.lower()
    if suffix in {".txt", ".md", ".csv", ".json", ".eml"}:
        return {"type": "text", "content": path.read_text(encoding="utf-8", errors="replace")}
    if suffix in {".png", ".jpg", ".jpeg", ".webp"}:
        media = "image/png" if suffix == ".png" else "image/jpeg"
        data = base64.standard_b64encode(path.read_bytes()).decode("ascii")
        return {"type": "image", "media_type": media, "data": data}
    if suffix == ".pdf":
        # Use pdftotext or pdfplumber as a fallback text layer
        import subprocess
        text = subprocess.check_output(["pdftotext", "-layout", str(path), "-"]).decode("utf-8", "replace")
        return {"type": "text", "content": text}
    raise ValueError(f"Unsupported file type: {suffix}")

For image-based inputs, modify the extractor to send a content block with type: "image" alongside the prompt. Claude's vision capabilities then handle OCR implicitly.

Orchestrating the Full Pipeline

The orchestrator ties ingestion, loading, extraction, validation, and persistence together. It also handles retries and logs failures for later review.

Create src/pipeline.py:

import json
import logging
from pathlib import Path
from datetime import datetime
from pydantic import ValidationError
from loaders import load_file
from extractor import extract_invoice
from schemas import InvoiceRecord

logging.basicConfig(level=logging.INFO, format="%(asctime)s %(levelname)s %(message)s")
logger = logging.getLogger("pipeline")

INPUT_DIR = Path("input")
OUTPUT_FILE = Path("output/records.jsonl")
ERROR_FILE = Path("output/errors.jsonl")


def run():
    OUTPUT_FILE.parent.mkdir(exist_ok=True)
    records = []
    errors = []

    for path in sorted(INPUT_DIR.iterdir()):
        if not path.is_file():
            continue
        logger.info(f"Processing {path.name}")
        try:
            loaded = load_file(path)
            raw = extract_invoice(loaded["content"]) if loaded["type"] == "text" else extract_invoice_image(loaded)
            record = InvoiceRecord(**raw)
            records.append(record.model_dump(mode="json"))
            logger.info(f"  OK: invoice {record.invoice_number} -> ${record.total}")
        except ValidationError as e:
            logger.error(f"  Validation failed for {path.name}: {e}")
            errors.append({"file": path.name, "error": e.json(), "ts": datetime.utcnow().isoformat()})
        except Exception as e:
            logger.error(f"  Extraction failed for {path.name}: {e}")
            errors.append({"file": path.name, "error": str(e), "ts": datetime.utcnow().isoformat()})

    with OUTPUT_FILE.open("w") as f:
        for r in records:
            f.write(json.dumps(r) + "\n")
    with ERROR_FILE.open("w") as f:
        for e in errors:
            f.write(json.dumps(e) + "\n")

    logger.info(f"Done. {len(records)} records, {len(errors)} errors.")


def extract_invoice_image(loaded: dict) -> dict:
    from anthropic import Anthropic
    from schemas import InvoiceRecord
    client = Anthropic()
    tool = {"name": "save_invoice", "description": "Save invoice.", "input_schema": InvoiceRecord.model_json_schema()}
    resp = client.messages.create(
        model="claude-sonnet-4-5-20250929",
        max_tokens=4096,
        tools=[tool],
        tool_choice={"type": "tool", "name": "save_invoice"},
        messages=[{
            "role": "user",
            "content": [
                {"type": "image", "source": {"type": "base64", "media_type": loaded["media_type"], "data": loaded["data"]}},
                {"type": "text", "text": "Extract the invoice fields using the save_invoice tool."},
            ],
        }],
    )
    for b in resp.content:
        if b.type == "tool_use":
            return b.input
    raise RuntimeError("No tool_use returned")


if __name__ == "__main__":
    run()

Run the pipeline from the project root:

python -m src.pipeline

Using Claude Code to Build and Iterate

Instead of writing all this by hand, you can ask Claude Code to scaffold and refine the pipeline. From the project directory, launch the agent:

claude
> Read the files in input/ and design a Pydantic schema that captures the fields present across all invoices. Write it to src/schemas.py.
> Now write src/extractor.py that calls the Claude API with tool_use to enforce that schema.
> Run the pipeline against input/ and fix any validation errors you find.

Claude Code will inspect the actual documents, propose a schema grounded in real data, generate the extractor, and self-correct by reading error output. This iterative loop is where the productivity gains compound — the agent closes the gap between intent and working code in minutes rather than hours.

Best Practices

Extending the Pipeline

Once the core works, common extensions include adding a classification stage that routes documents to different schemas, integrating a queue (Redis, SQS) for asynchronous processing at scale, writing records directly to a warehouse like Snowflake or BigQuery, and adding a human-in-the-loop review step for low-confidence extractions. Each of these is a small addition because the pipeline's modular design keeps stages independent.

A classification stage might look like this:

def classify(document_text: str) -> str:
    resp = client.messages.create(
        model="claude-haiku-4-5",
        max_tokens=20,
        messages=[{"role": "user", "content": f"Classify this document as invoice, receipt, or contract. Reply with one word.\n\n{document_text[:2000]}"}],
    )
    return resp.content[0].text.strip().lower()

Route the result to the matching schema and extractor, and your pipeline now handles heterogeneous document streams.

Conclusion

Building a data extraction pipeline with Claude Code combines the flexibility of large language models with the rigor of typed, validated code. By letting Claude handle the messy work of understanding unstructured documents and keeping orchestration deterministic, you get a system that is accurate on day one and adaptable as inputs evolve. Start with a clear schema, enforce structure through tool use, validate with Pydantic, and let Claude Code iterate on the glue code — you'll have a production-grade extraction pipeline running in an afternoon rather than a sprint.

— Ad —

Google AdSense will appear here after approval

← Back to all articles