I Built a Self-Healing AI Layer for Odoo — Here's What Actually Broke A developer built a self-healing AI middleware layer for Odoo that introspects the live schema, semantically maps data using vector search, and autonomously repairs failed writes via a local LLM. The system, running entirely on local models, achieved a 66.7% first-try success rate in a small eval, with failures revealing that the 3B model sometimes hallucinated outdated Odoo knowledge and that ambiguous fields like 'notes' were correctly flagged for human review. The developer also noted that the self-healing loop cannot invent foreign keys, as demonstrated by an invalid country_id that failed all repair attempts. Enterprise ERP platforms like Odoo get customized constantly — every business adds its own fields x studio client tax id, x vendor code v2, whatever they need . The problem: hardcoded integrations break the instant a database schema diverges from what they expect. A missing field, an invalid selection value, a type mismatch — and the whole transaction crashes. So I built a middleware layer that doesn't assume a fixed schema. It introspects Odoo's live schema at runtime, semantically maps messy incoming data to the right fields using vector search, and when Odoo rejects a write, it reads the actual error, asks a local LLM to propose a fix, and retries — autonomously, with hard limits. This post isn't a tutorial. It's what actually happened when I built it, including the parts that didn't work the way I expected — because those turned out to be the most useful parts. The Architecture, Briefly Next.js Dashboard <--WebSocket-- FastAPI Backend <--XML-RPC-- Odoo 17 Docker | +-- Qdrant local embeddings, field search +-- Ollama local LLM, payload repair Schema introspection via Odoo's fields get over XML-RPC — including custom fields, no hardcoded field lists anywhere Semantic field matching — every Odoo field gets embedded locally sentence-transformers, zero API cost and indexed in Qdrant. Incoming messy keys get embedded the same way and matched by cosine similarity A real LangGraph state machine for the self-healing loop — not a for loop with a try/except wearing a costume. Nodes for schema fetch, create attempt, and repair; a conditional edge that routes to repair-and-retry on failure, capped at 3 attempts FastAPI + WebSocket streaming the whole process live to a Next.js dashboard, so you can watch the agent fail, reason about the error, and retry in real time I deliberately ran this entirely on local models — sentence-transformers for embeddings, Ollama Qwen2.5 3B for the repair agent. Zero API cost, zero external dependency. That choice has real consequences, which I'll get to. The Eval Numbers I built a small labeled eval harness 6 cases, 5 deliberately broken across different Odoo validation rules, 1 clean control rather than eyeballing whether it "seemed to work": That first-try number matters more than it looks like it should. It's the real cost of choosing a 3B local model over a hosted API — and honestly, seeing it in black and white was more useful than pretending the system "just worked." What Actually Broke the interesting part 1. The model hallucinated from outdated Odoo knowledge I fed the local LLM an invalid type selection field, along with the live schema explicitly listing valid values. It proposed "Customer" and "Supplier". Those were valid Odoo field values — in Odoo 8, which the model apparently remembers from pretraining. It ignored the live schema I explicitly gave it in the prompt and pattern-matched from general "knowledge" of Odoo instead. That's a genuinely instructive failure: smaller instruction-tuned models will sometimes trust their priors over your context, even when the context is right there. 2. "notes" matched the wrong field, and it was the right call Querying "notes" against the schema, the system matched Odoo's built-in comment field 0.4969 similarity more strongly than the custom field it was actually meant to represent, x studio delivery notes 0.4707 . Neither score cleared my confidence threshold 0.75 for auto-apply. Both got flagged for human review instead of the system guessing. That's the entire point of confidence gating — a single generic word like "notes" should produce ambiguous results, and the system correctly refused to pretend otherwise. 3. The self-healing loop can't invent a foreign key A payload with an invalid country id — pointing to a record that doesn't exist — failed all 3 repair attempts. Dropping or emptying an invalid selection value is a viable fix an LLM can reason its way to. Inventing a valid foreign-key ID isn't — the model has no way to know what a real one would be. This is a legitimate architectural boundary, not a bug I need to apologize for. 4. PDF invoices broke the pipeline until I routed them through the same guardrail as everything else Early version: extract text from a PDF invoice, pull out "Label: Value" pairs, send straight into the self-healing loop. Result: the LLM guessed blindly at field names vendor name → partner name, itself also wrong across all 3 attempts. Fix: route PDF-extracted labels through the same semantic confidence gate as any other input, instead of assuming they're already clean field keys. Now the system correctly says "I'm not confident about any of these, here's what I found, a human should look" — which is a worse UX in the moment but the actually correct behavior for a system that might be writing to a real database. 5. Odoo sometimes just... accepts garbage I sent "is company": "yes" a string expecting Odoo to reject it since the field wants a boolean. It didn't — Odoo's ORM silently coerced it. Not every "invalid" value I threw at the system actually reached the self-healing loop; some got absorbed upstream by Odoo itself. This reshaped how I designed later test cases — targeting Selection and many2one fields specifically, since Odoo validates those strictly, unlike loose type coercion elsewhere. Why This Matters More Than "It Worked" Any of these failures could have been quietly avoided by picking easier test cases and writing a README that only shows the happy path. I think that's the wrong instinct. A system that never fails in its own documentation either wasn't tested hard enough, or isn't being honest about its limits — and "I know exactly where and why this breaks" is a more useful thing to be able to say than "it works." The confidence-gating layer specifically exists because of failures like 2 and 4 — it's not decoration, it's the actual answer to "how do you know this won't silently write bad data." What I'd Do With More Time Swap in a hosted structured-output model GPT-4o-mini or Claude as a configurable option and directly compare eval numbers against the local model — right now the local-vs-hosted trade-off is described, not measured Expand the eval set well past 6 cases A resolution mechanism for many2one fields search-by-name instead of failing outright — would directly fix failure case 3 Full code, architecture doc, and eval report: github.com/Nida-shafiq/odoo-shadow-schema-ai Happy to talk through any part of this in the comments especially curious if anyone's hit similar local-model-hallucinates-from-pretraining issues in a different domain.