01Overview
Users type what they need, such as fixing a field across a product range, reconciling a warehouse count sheet, or listing contracts that expire this quarter. The agent works out which Odoo models and fields are involved, drafts a plan, shows it for approval, executes it against the live database and reads the records back to confirm what changed. Most LLM-over-database tools stop at read-only questions, because writes are where a hallucination gets expensive. This one creates, updates and deletes records, so most of the engineering went into making each write safe to approve. One backend serves a chat workspace, a dashboard builder and an admin console, and it was built with an eight-person team over about four months.

02The Challenge
The work this replaces is not looking things up in Odoo. It is the hours-long job of getting a spreadsheet into Odoo correctly: open the file, find the matching records one screen at a time, retype fields, then re-check everything. Odoo's import wizard only helps when a file already matches the database, with exact column names, resolved references and no rows that are really updates. Real files never look like that. Two properties of Odoo made the translation harder, and both were found by testing a live database rather than by reading documentation. First, Odoo silently ignores filters on computed fields that are not stored: a filter that should have matched nothing returned all 2,983 product templates, and 28 of 34 such fields on sales orders and 33 of 40 on invoices behaved the same way. Asking for invoices with an amount due above zero returned every invoice, so a naive agent would have reported all of them as unpaid. Second, a stock install has more than 500 models before any custom modules. Putting all of them in one prompt produced confident tool-selection errors, such as confusing invoices with payments, so context size was a correctness problem, not just a cost problem.
Pain points we set out to solve
- Spreadsheet imports took hours of manual lookup and retyping
- Odoo silently drops some filters, so wrong answers look confident
- 500+ models per database: too much schema for one prompt
- Every write lands in live accounting and inventory data

03Objectives
- 01Let business users read and write Odoo data in plain language, including bulk spreadsheet work
- 02No write reaches Odoo without a person approving the exact values
- 03Confirm every write by reading the records back, not by trusting the tool's success response
- 04Work on any customer's schema, including custom modules, with nothing hard-coded
04Approach
How it was delivered: phased, with clear checkpoints and evidence at each step.
- Foundation
Schema search and specialists
Each connected database is indexed at connect time and searched in natural language. Requests are routed to one of seven specialists (Purchasing, Sales, CRM, Accounting, Payroll, Inventory, General), each with its own model allow-list, so a purchasing request never sees CRM models and the prompt stays small.
- Agent loop
One planner, one pause
A LangGraph state graph loops on a single shared planner. Reads fan out in parallel; a write stops the graph until a person approves it. State is checkpointed in Postgres, so a paused approval survives a redeploy. After 25 iterations, one final call with no tools answers from what was gathered instead of ending mid-sentence.
- Bulk work
A task ledger for spreadsheets
The model declares the shape of the work once: which model, which column maps to which field, with fallbacks, regex extraction and concatenation. The backend expands every row into a ledger of tasks, approved on one card and run outside the chat, so a 5,140-row import survives a closed browser tab.
- Measurement
Ground truth for routing
A golden corpus of 211 real, hand-labelled prompts is replayed offline in CI with no API key. Precision and coverage are reported together, alongside roughly 3,000 automated tests and full-scope runs that use a real model and real writes, cleaned up afterwards through the ledger.

05The Solution
Six independent mechanisms stand between a sentence and a wrong record, and none of them is the model promising to be careful. Writes pause on an approval card that shows the model and values in business words, and bulk plans preview mapped values before a single row is written. Afterwards the agent reads the records back and compares them with what was asked. A separate check can re-read Odoo later to answer whether an import really landed, which is a different question from whether the call succeeded.

Approval on a card that shows the work
Writes pause for a person. The card names the model in business words and lists the values, so a wrong mapping is caught before 5,000 rows are written, not after.
Scope enforced in code
Each specialist owns an allow-list of models. A write outside it is rejected by the dispatcher, not by the prompt.
Three-layer deduplication
A task fingerprint catches repeats across sessions, a pre-check finds records that already exist in Odoo, and a per-turn hash stops a looping model from firing the same call twice.
A cache that forgets on write
Repeated reads are skipped only while nothing has changed. The cache resets the moment the turn writes, so the read-back that verifies a write always runs.
A facts ledger for numbers
Every figure a read returns is stored as an exact decimal string before any truncation, so totals can be checked. Before this, 299 of 346 stored previews were cut off before their totals.
Rollback as a plan
Undo queues compensating steps for approval and writes nothing until they are approved. It also lists what cannot be undone, such as a posted journal entry.
06Technology stack
Picked for latency, cost, and long-term maintainability — not for novelty.
AI / Agent
- LangGraph
- DeepSeek (chat and vision)
- Gemini embeddings
- LlamaIndex
- MiniLM schema search
ERP Integration
- Odoo XML-RPC
- Per-organization Odoo connections
- Odoo server actions
Backend / Data
- FastAPI
- Postgres 17
- LangGraph Postgres checkpointer
- nginx + gunicorn
Frontend
- React 19
- Vite
- HeroUI
- Tailwind 4
- assistant-ui
Quality / Ops
- pytest (~3,000 tests)
- 211-prompt routing corpus
- GitHub Actions CI/CD
- Per-turn traces
07Results
- 95.2%Routing precision on 211 labelled prompts (from 80.3%)
- 5,140Rows in the largest approved import
- 6Independent safeguards on every write
- 2,987Automated tests guarding the agent

These are engineering measurements, not sales figures. What they add up to: the hours-long job of reconciling a spreadsheet against Odoo becomes a plan the user reads, approves and can undo, on a platform with organizations, per-organization Odoo connections, metered credits and an admin console.
08Key takeaways
- The hard part of an agent that writes is not the write call. It is the safeguards, each of which assumes the model is wrong
- Test the ERP, not its docs: a filter that should have matched nothing returned all 2,983 rows
- A cache that skips repeated reads must reset on every write, or verification quietly lies
- Report router precision and coverage together. Either one alone can be improved by refusing to decide
- Memory for a system that writes must be earned: no stored belief without an event behind it