Case study · ERP / Operations · 18 weeks

ERP Operations Agent - Letting an LLM Write to Odoo Safely

A chat-first operations layer for Odoo ERP that turns a sentence or a messy spreadsheet into approved, verified writes, with six safeguards that each assume the model is wrong.

  • 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
Client
Multi-tenant SaaS for Odoo users
Industry
ERP / Operations
Engagement
18 weeks
Timeline
May 2026 - Sep 2026

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.

Architecture of one chat turn: interpret, route to a specialist, plan, run reads in parallel, pause writes for approval, execute on Odoo and verify by reading back
One chat turn. Reads run on their own; every write stops the graph and waits for a person.

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
A filter on a non-stored Odoo field returned all 2,983 rows for every condition; 28 of 34 such fields on sales orders and 33 of 40 on invoices behave the same way
Tested on a live Odoo 18 database: the ORM dropped the condition instead of raising an error. Tap to open full size.

03Objectives

  1. 01Let business users read and write Odoo data in plain language, including bulk spreadsheet work
  2. 02No write reaches Odoo without a person approving the exact values
  3. 03Confirm every write by reading the records back, not by trusting the tool's success response
  4. 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.

  1. 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.

  2. 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.

  3. 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.

  4. 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.

Bulk planner lifecycle in seven stages from file upload to verification, with three ways back: revise, retry failed rows, and roll back as a plan
Bulk work runs through a task ledger, not the chat loop, with three ways back. Tap to open full size.

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.

Six safeguards between a sentence and a record in Odoo: approval card, scope enforced in code, three-layer deduplication, a cache that forgets on write, a facts ledger, and rollback as a plan
Six independent safeguards. None of them relies on the model being careful. Tap to open full size.

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
Router precision on 211 labelled prompts rose from 80.3% with 14 wrong commits to 95.2% with 4 wrong commits
Precision and coverage are always reported together. Tap to open full size.
Business impact

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

Ready to start something similar? Start with one call.

A 30-minute call, no pitch deck. If it’s not a fit, I’ll point you to someone it is.

Book a free scoping call
What happens after you book
  1. A 30-minute call about your workflow
  2. A one-page scope with price and timeline, free
  3. You decide: build, or keep the scope and walk away