← All Articles

How I Use Claude's Tool Use to Build Self-Correcting Pipelines

Claude's tool use feature is designed to call functions. Most people use it once: call the tool, get the result, done. I use it differently.

I use it to build loops where the model validates its own output and corrects course when something goes wrong.

This is the pattern that catches bugs before they hit your database.

The Problem We're Solving

You're processing documents. Claude extracts structured data. Should be simple.

But Claude hallucinates. It might extract:

Most teams just save it and debug later. That's expensive.

What if Claude validated its own output and fixed it before you saved it?

The Pattern: Structured Tool Calls with Validation

Here's how we do it in production:

import anthropic import json client = anthropic.Anthropic() tools = [ { "name": "extract_invoice", "description": "Extract invoice data from text", "input_schema": { "type": "object", "properties": { "invoice_number": {"type": "string"}, "amount": {"type": "number"}, "customer_email": {"type": "string", "format": "email"}, "due_date": {"type": "string", "format": "date"} }, "required": ["invoice_number", "amount", "customer_email", "due_date"] } }, { "name": "validate_and_retry", "description": "Validate extraction. If invalid, explain what's wrong and ask the model to fix it.", "input_schema": { "type": "object", "properties": { "is_valid": {"type": "boolean"}, "errors": {"type": "array", "items": {"type": "string"}}, "corrected_data": {"type": "object"} }, "required": ["is_valid"] } } ] def validate_invoice(data): """Check if extracted data is valid.""" errors = [] if not isinstance(data.get("amount"), (int, float)) or data["amount"] <= 0: errors.append("amount must be a positive number") if "@" not in data.get("customer_email", ""): errors.append("customer_email must be a valid email") try: from datetime import datetime datetime.strptime(data.get("due_date", ""), "%Y-%m-%d") except ValueError: errors.append("due_date must be in YYYY-MM-DD format") return len(errors) == 0, errors def process_invoice(invoice_text): """Process an invoice with self-correcting loop.""" messages = [ { "role": "user", "content": f"Extract invoice data from this text:\n\n{invoice_text}" } ] max_attempts = 3 attempt = 0 while attempt < max_attempts: attempt += 1 response = client.messages.create( model="claude-opus-4-1-20250805", max_tokens=1024, tools=tools, messages=messages ) # Look for the tool use block tool_use_block = next( (block for block in response.content if block.type == "tool_use"), None ) if not tool_use_block: return None, "Model did not call extract_invoice tool" if tool_use_block.name == "extract_invoice": extracted_data = tool_use_block.input is_valid, errors = validate_invoice(extracted_data) if is_valid: return extracted_data, None # Add the assistant's response and the validation result to messages messages.append({"role": "assistant", "content": response.content}) messages.append({ "role": "user", "content": f"The extracted data has validation errors:\n{json.dumps(errors)}\nPlease fix these and try again." }) return None, f"Failed to extract valid data after {max_attempts} attempts" # Usage invoice_text = """ Invoice #INV-12345 Customer: John@example.com Amount: $1,500.50 Due: May 15, 2026 """ data, error = process_invoice(invoice_text) if error: print(f"Error: {error}") else: print(f"Success: {json.dumps(data, indent=2)}")

How This Works

  1. First call: Claude extracts data using the extract_invoice tool.
  2. Validation: We check if the data is valid (email format, positive amount, date format).
  3. If valid: Return the data. Done.
  4. If invalid: Add the validation errors to the message history and ask Claude to fix it.
  5. Loop: Claude tries again, up to 3 times.

The model sees the errors and adjusts. Most of the time, it gets it right on the second attempt.

Why This Works

Claude understands context. If it gets the email wrong, and you tell it "that's not a valid email," it learns from that feedback and fixes it.

It's deterministic for your business logic. You define what "valid" means (regex, format checks, business rules). Claude handles the fuzzy part (understanding intent) and iterates when it gets the deterministic part wrong.

Reduces downstream errors. Invalid data never reaches your database. You save engineering time debugging garbage data later.

When to Use This Pattern

This pattern works well for:

When not to use it:

Production Considerations

Latency: Each retry adds ~1-2 seconds. For batch processing, that's fine. For real-time APIs, keep attempts low (2-3 max).

Cost: Each API call costs. Three attempts = three calls. If extraction is cheap but validation is expensive, maybe accept some errors and handle them in a second pass.

Logging: Log every attempt. Track which validations fail most often. Use that signal to improve your validation logic or your prompt.

Example log:

{ "invoice_id": "INV-12345", "attempts": 2, "first_attempt_errors": ["customer_email invalid format"], "final_result": "success", "processing_time_ms": 3200 }

The Bigger Picture

This pattern scales. Build a validation layer once, apply it everywhere. Your invoice processor, your contract extractor, your customer intake form - all benefit from the same self-correction loop.

Claude doesn't just extract data. It validates and corrects itself. That's the difference between a brittle system and a resilient one.

Build your first self-correcting pipeline. We've used this pattern on invoice processing, customer data extraction, and contract analysis. The validation layer is reusable - build it once and apply it to every extraction workflow you ship.

Get the free AI Readiness Checklist

15 questions to diagnose your team’s AI readiness, where you’ll see ROI fastest, and what to tackle first.

Takes 5 minutes Actionable next steps No sales pitch

No spam. Unsubscribe anytime.

or

Ready to build AI that actually works?

Let’s talk about how SRE discipline transforms AI from a risky experiment into a reliable business system.

Book Your Free Discovery Call