<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Chandan Sharma</title>
    <description>The latest articles on DEV Community by Chandan Sharma (@chandsharma).</description>
    <link>https://dev.to/chandsharma</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4113414%2F3dade9d8-032f-4456-87c6-ced33485bb3b.jpg</url>
      <title>DEV Community: Chandan Sharma</title>
      <link>https://dev.to/chandsharma</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/chandsharma"/>
    <language>en</language>
    <item>
      <title>Odoo 19 ships a real AI agent. I read the source to see what happens after it writes.</title>
      <dc:creator>Chandan Sharma</dc:creator>
      <pubDate>Sat, 12 Sep 2026 14:04:52 +0000</pubDate>
      <link>https://dev.to/chandsharma/odoo-19-ships-a-real-ai-agent-i-read-the-source-to-see-what-happens-after-it-writes-48fc</link>
      <guid>https://dev.to/chandsharma/odoo-19-ships-a-real-ai-agent-i-read-the-source-to-see-what-happens-after-it-writes-48fc</guid>
      <description>&lt;p&gt;Short answer, so you can leave if that is all you came for: nothing reads the row back. When a tool returns no value, Odoo composes a success sentence from what the action &lt;em&gt;intended&lt;/em&gt; and hands that to the model. The agent then reports success because it was given a string, not because anyone checked the database.&lt;/p&gt;

&lt;p&gt;Here is the longer version, with what I actually found.&lt;/p&gt;

&lt;h2&gt;
  
  
  First, the part most people get wrong
&lt;/h2&gt;

&lt;p&gt;A lot of posts right now claim Odoo's AI is "just an API wrapper." That is false, and you can refute it with one screenshot of the source.&lt;/p&gt;

&lt;p&gt;Odoo 19 Enterprise ships a genuine tool-calling agent loop. Up to 20 successive rounds of up to 20 parallel tool calls, with an injected termination protocol. Retrieval over an indexed corpus with a real vector field type. Unattended execution through cron. And real write capability, through server actions an administrator authors and marks for AI use.&lt;/p&gt;

&lt;p&gt;That is an agent by any reasonable definition. It loops, it calls tools, it acts on its environment, and it runs without a human present. Odoo also made a defensible safety choice: there is no generic write tool. Writes travel only through actions a human wrote, and the ORM access rules still apply.&lt;/p&gt;

&lt;p&gt;So the interesting question is not capability. It is what happens after it acts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What Odoo says about that, in their own words
&lt;/h2&gt;

&lt;p&gt;From the AI server actions documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An AI server action acts as a decision maker, or a manager. It reads the record and its context. It interprets the AI prompt. And it decides which tool to call, and what arguments to use.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;And, in the same documentation:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The server action does not enforce business rules, modify records directly, or guarantee the correctness of the operation. Its role is limited to decision-making.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;Read that second one again if you are about to point this at a production database. Odoo is being straight with you. The agent decides. Everything else is yours to build.&lt;/p&gt;

&lt;p&gt;Also, if you assumed the chat assistant could act:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;The standard Ask AI agent cannot make changes to the database. As such, it can open views and display reports, but it cannot create leads or alter data.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  The four things I found in the source
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;1. Nothing is read back.&lt;/strong&gt; When a server action returns nothing, Odoo substitutes a static description assembled from the action's intent, along the lines of "Activity created for X." The model is told the write worked by a sentence Odoo composed. The database is never re-read to confirm it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;2. There is no savepoint in the module's tool execution.&lt;/strong&gt; Picture a five-step action where step three is denied by a record rule. Steps one and two are already written and stay written. Steps four and five then run against a half-changed state. Nothing tells you the data is now inconsistent.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;3. A denial becomes advice.&lt;/strong&gt; The exception handler catches the error, converts it to a string, and returns it to the model as the tool result so the run can continue. Odoo's own code comment says this is intentional, so a prompt can say "do X, and if it fails, do Y." A permission error is a token the model can reason around rather than a wall.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;4. The evidence is a chatter message.&lt;/strong&gt; It records what was attempted, not the before and after state, and a &lt;code&gt;mail.message&lt;/code&gt; can be edited or deleted like any other record.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why this matters more in an ERP than anywhere else
&lt;/h2&gt;

&lt;p&gt;If an agent gets a blog post wrong, you notice. If it writes to the wrong record in your ERP and reports success, nothing errors, nobody notices, and it compounds. A month later somebody is reconciling accounts and cannot work out why.&lt;/p&gt;

&lt;p&gt;An enterprise prospect described this to me better than I could:&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;An agent does the wrong thing and reports success. Writes against the wrong person or record, or reads a truncated list and answers as if complete. Nothing errors. Nobody notices. It piles up. Checking the draft before it runs does not catch it, because the mistake happens during execution.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;That last sentence is the one that matters. Reviewing what the agent &lt;em&gt;proposed&lt;/em&gt; does not protect you, because the failure happens after approval.&lt;/p&gt;

&lt;h2&gt;
  
  
  What a write actually needs before you can leave it running
&lt;/h2&gt;

&lt;p&gt;Four properties. None exotic, all checkable.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Read-back.&lt;/strong&gt; The result handed to the model is re-read from the database after the write, not composed from the request.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Atomicity.&lt;/strong&gt; A multi-step action completes fully or leaves no trace.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Structured refusal.&lt;/strong&gt; A denial returns a machine-readable code, not a sentence the model can reinterpret.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Evidence.&lt;/strong&gt; An append-only record of before and after values, independent of the record's own chatter.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you are evaluating any AI-for-ERP tool, including mine, those four are a better test than any demo. Ask the vendor to show you the row, re-read from the database, after the write. Not the log of what it tried.&lt;/p&gt;

&lt;h2&gt;
  
  
  Disclosure, and my own score
&lt;/h2&gt;

&lt;p&gt;I help build one of these (nanti.ai). So take the above as claims to verify rather than neutral reporting, and here is my own honest scoring against the same bar.&lt;/p&gt;

&lt;p&gt;We do the four. Every write is re-read on an independent connection after commit and compared field by field against what the caller intended, proven by deliberately corrupting the stored value behind a write in the test suite to confirm the product refuses the happy echo. Refusals are committed on a separate transaction so the record of the denial survives the rollback of the action it denied.&lt;/p&gt;

&lt;p&gt;Where we fall short, since a bar you only apply to other people is worth nothing:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;We have &lt;strong&gt;not&lt;/strong&gt; commissioned a third-party penetration test, and will not claim one.&lt;/li&gt;
&lt;li&gt;Several of those properties are &lt;strong&gt;paid-tier only&lt;/strong&gt;. A free-tier install genuinely has less.&lt;/li&gt;
&lt;li&gt;Until this week our own core advertised an idempotency key it did not honour on the free tier, so a client retry could create a second record. That was a real defect in shipped code. It is fixed, with a test that holds it closed.&lt;/li&gt;
&lt;li&gt;We claim &lt;strong&gt;tamper-evident&lt;/strong&gt;, never tamper-proof. A database superuser who disables the trigger can still alter rows. The hash chain makes that evident rather than impossible.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;None of this makes Odoo's engineering bad. It is a deliberate, bounded design and their documentation describes it accurately. It does mean that if you are about to let an agent write into your ERP, the question is not how capable it is.&lt;/p&gt;

&lt;p&gt;It is: after it writes, where does the confirmation come from?&lt;/p&gt;

&lt;p&gt;If the answer is that the model said so, you do not have a record. You have a rumour.&lt;/p&gt;

&lt;p&gt;Odoo is a trademark of Odoo S.A. This is independent work, not affiliated with or endorsed by Odoo S.A.&lt;/p&gt;

&lt;p&gt;If you have pointed an agent at your Odoo, I would like to know what it wrote that it should not have.&lt;/p&gt;

</description>
      <category>odoo</category>
      <category>ai</category>
      <category>erp</category>
      <category>python</category>
    </item>
    <item>
      <title>Your AI can query Odoo. It still gets revenue wrong.</title>
      <dc:creator>Chandan Sharma</dc:creator>
      <pubDate>Mon, 07 Sep 2026 19:20:30 +0000</pubDate>
      <link>https://dev.to/chandsharma/your-ai-can-query-odoo-it-still-gets-revenue-wrong-223d</link>
      <guid>https://dev.to/chandsharma/your-ai-can-query-odoo-it-still-gets-revenue-wrong-223d</guid>
      <description>&lt;p&gt;Connecting a large language model to Odoo used to be the hard part. It is not any more. There are MCP servers, XML-RPC wrappers, and JSON-RPC clients that hand an agent the ability to read and write Odoo records in an afternoon. Reaching the data is solved.&lt;/p&gt;

&lt;p&gt;Understanding it is not. An agent that can run any query will still answer "what was our revenue last month" with a number that is confidently, quietly wrong. The failure is not in the connection. It is that the model was never told what the records mean.&lt;/p&gt;

&lt;p&gt;Here are three traps I hit repeatedly, each one real, each one something a naive agent walks straight into.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 1: "invoices" is not a table
&lt;/h2&gt;

&lt;p&gt;Ask an agent to "add up last month's invoices" and it will look for something invoice-shaped. In Odoo it finds &lt;code&gt;account.move&lt;/code&gt;, sees invoice-like columns, and sums them.&lt;/p&gt;

&lt;p&gt;But &lt;code&gt;account.move&lt;/code&gt; is not the invoices table. It is the journal-entries table. Customer invoices, vendor bills, customer credit notes, and vendor refunds all live in the same model, separated only by a discriminator column, &lt;code&gt;move_type&lt;/code&gt;:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;out_invoice&lt;/code&gt; is a customer invoice&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;in_invoice&lt;/code&gt; is a vendor bill&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;out_refund&lt;/code&gt; is a customer credit note&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;in_refund&lt;/code&gt; is a vendor refund&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;So the honest domain for "customer invoices" is:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight python"&gt;&lt;code&gt;&lt;span class="p"&gt;[(&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;move_type&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;=&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="s"&gt;out_invoice&lt;/span&gt;&lt;span class="sh"&gt;"&lt;/span&gt;&lt;span class="p"&gt;)]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;An agent that skips that filter sums your sales and your supplier bills into one figure. It will not error. It will return a plausible, wrong total. You can see the discriminator in Odoo's own source, in &lt;code&gt;addons/account/models/account_move.py&lt;/code&gt;, where &lt;code&gt;move_type&lt;/code&gt; is defined and used to tell these document types apart.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 2: "revenue" has no single answer
&lt;/h2&gt;

&lt;p&gt;Say the filter is right and the agent is looking only at customer invoices. Now: what is revenue?&lt;/p&gt;

&lt;p&gt;There is general-ledger revenue, recognised through income accounts. There is invoiced sales, the total of issued customer invoices. They are different numbers, on different date bases (invoice date, accounting date, or delivery period), and a business means a specific one when it asks. No column in Odoo is named &lt;code&gt;revenue&lt;/code&gt;. A field like &lt;code&gt;amount_total&lt;/code&gt; looks close, but it is a document total that mixes tax and, across the wrong population of moves, mixes document types too.&lt;/p&gt;

&lt;p&gt;The correct behaviour when a human asks "what is revenue" is not to answer. It is to ask which definition and which date basis apply, then compute that one. An agent optimised to be helpful will instead pick the first plausible field and commit.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trap 3: field labels lie
&lt;/h2&gt;

&lt;p&gt;&lt;code&gt;account.move&lt;/code&gt; has a field &lt;code&gt;invoice_user_id&lt;/code&gt;. From the name you might read "the user who created the invoice". It is not. It is the salesperson associated with the invoice, which is a business fact about attribution, not authorship. If your agent maps it to "created by" and builds a per-rep sales report on it, the report is subtly wrong in a way nobody catches until commission season.&lt;/p&gt;

&lt;p&gt;Field names are shorthand written for developers, not contracts about meaning. Guessing from the label is how an agent produces answers that survive review because they look reasonable.&lt;/p&gt;

&lt;h2&gt;
  
  
  The missing layer
&lt;/h2&gt;

&lt;p&gt;The pattern under all three traps is the same. A connector tells an agent &lt;em&gt;how to reach&lt;/em&gt; a record. Nothing tells it &lt;em&gt;what the record means&lt;/em&gt;, which tempting reading is wrong, or what it must clarify before it computes. That second thing is a separate layer:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;question -&amp;gt; context -&amp;gt; AI reasoning + existing connector -&amp;gt; Odoo
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The context step carries three kinds of knowledge the connector never will: what a business noun maps to, what a field actually means, and the negative knowledge of which plausible interpretation to refuse. I have been building an open format for exactly this layer, OCL, and the reference implementation is small enough to run in a minute, so the rest of this is a walk-through you can reproduce.&lt;/p&gt;

&lt;h2&gt;
  
  
  Trying it
&lt;/h2&gt;

&lt;p&gt;The repository ships an example resolver over ten public Odoo 19 example entries. Clone it and ask the revenue question:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;git clone https://github.com/Nantiai/ocl-standard.git
&lt;span class="nb"&gt;cd &lt;/span&gt;ocl-standard
python reference/python/ocl_examples.py get-context &lt;span class="s2"&gt;"What is revenue?"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The interesting part of the response, trimmed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"facts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"entry_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ocl.example.odoo19.account.revenue_ambiguity"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rendered"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"... Clarify: Which business definition and date basis apply?"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"warnings"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"entry_id"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"ocl.example.odoo19.account.amount_total_warning"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"rendered"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"... Correction: Resolve the metric definition and record population first."&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"severity"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"warning"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;],&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"unknowns"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"code"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"clarify_revenue"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"message"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"Which business definition and date basis apply?"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"required_action"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"clarify_before_execution"&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The context pack does not answer the question. It returns a &lt;code&gt;warning&lt;/code&gt; against treating one total field as universal revenue, and an &lt;code&gt;unknown&lt;/code&gt; that says clarify before execution. Handed to a model as tool output, that is the difference between an agent that guesses and one that asks the one question it should have asked.&lt;/p&gt;

&lt;p&gt;The other two traps are covered by two more calls. Resolving a noun to its real domain:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python reference/python/ocl_examples.py resolve-noun &lt;span class="s2"&gt;"customer invoice"&lt;/span&gt;
&lt;span class="c"&gt;# -&amp;gt; model account.move, domain [["move_type", "=", "out_invoice"]]&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;And explaining a field instead of trusting its name:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;python reference/python/ocl_examples.py explain-field account.move invoice_user_id
&lt;span class="c"&gt;# -&amp;gt; meaning: "Salesperson associated with the invoice."&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Putting it in an agent's tool loop
&lt;/h2&gt;

&lt;p&gt;The same resolver runs as an MCP server, so an agent can call &lt;code&gt;get_context&lt;/code&gt;, &lt;code&gt;resolve_noun&lt;/code&gt;, and &lt;code&gt;explain_field&lt;/code&gt; mid-reasoning:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"mcpServers"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"ocl-public-examples"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"command"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"python"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="nl"&gt;"args"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"/absolute/path/to/ocl-standard/reference/python/ocl_examples.py"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
        &lt;/span&gt;&lt;span class="s2"&gt;"serve-mcp"&lt;/span&gt;&lt;span class="w"&gt;
      &lt;/span&gt;&lt;span class="p"&gt;]&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now, before the agent writes a query, it can ask what "revenue" means, get told to clarify, and clarify. Before it reports on a rep, it can check what &lt;code&gt;invoice_user_id&lt;/code&gt; is. The knowledge lives outside the prompt, so it is auditable and it does not drift.&lt;/p&gt;

&lt;h2&gt;
  
  
  Honest boundaries
&lt;/h2&gt;

&lt;p&gt;This is worth saying plainly, because the failure mode of posts like this is overclaiming.&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;The format is an experimental technical alpha, &lt;code&gt;0.1.0&lt;/code&gt;. It can change while independent implementations test it.&lt;/li&gt;
&lt;li&gt;The ten examples are &lt;strong&gt;candidates that demonstrate the shape&lt;/strong&gt;, not a certified registry. Each one carries a low confidence score on purpose. A conforming document describes evidence and assertions; deciding a fact is verified is a separate, deliberate step, not "an LLM wrote plausible JSON".&lt;/li&gt;
&lt;li&gt;The example server serves only those public examples. It is not a commercial runtime, and cloning it does not give you coverage of a real Odoo database.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;What is real and reproducible today is the pattern: separate &lt;em&gt;reaching&lt;/em&gt; records from &lt;em&gt;meaning&lt;/em&gt;, encode the meaning as facts, warnings, and unknowns, and give an agent a way to ask before it computes. You can apply that idea with or without this repo.&lt;/p&gt;

&lt;h2&gt;
  
  
  Who I am
&lt;/h2&gt;

&lt;p&gt;I help build nanti.ai, and OCL is our open take on this context layer for Odoo. The repository is Apache-2.0: &lt;a href="https://github.com/Nantiai/ocl-standard" rel="noopener noreferrer"&gt;github.com/Nantiai/ocl-standard&lt;/a&gt;. If you would rather see one grounded decision run without cloning anything, there is a no-login live demo of the revenue case at &lt;a href="https://api.context.nanti.ai/demo/revenue" rel="noopener noreferrer"&gt;api.context.nanti.ai/demo/revenue&lt;/a&gt;. The reference server is also published on the official MCP registry as &lt;code&gt;io.github.Nantiai/ocl-standard&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;Odoo is a trademark of Odoo S.A. This project is independently developed and is not affiliated with or endorsed by Odoo S.A.&lt;/p&gt;

&lt;p&gt;If you have connected an agent to Odoo, I would like to know which field it got wrong first. Mine was &lt;code&gt;invoice_user_id&lt;/code&gt;.&lt;/p&gt;

</description>
      <category>odoo</category>
      <category>ai</category>
      <category>mcp</category>
      <category>python</category>
    </item>
  </channel>
</rss>
