Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Manage Odoo (contacts, any business objects, and metadata) via the official External XML-RPC API. Supports generic CRUD operations on any model using execute_kw, with ready-made flows for res.partner and model introspection. Features dynamic instance and database switching with context-aware URL, database, and credential resolution.
.claude/skills/leoyeai-odoo-manager/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-03 | ✗→✓ | ▲ Improved | 153% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 483% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 252% | 0% |
| case-17 | ✗→✓ | ▲ Improved | 181% | 0% |
| case-18 | ✗→✓ | ▲ Improved | 194% | 0% |
Odoo server URL precedence (highest to lowest):
temporary_url — one-time URL for a specific operationuser_url — user-defined URL for the current sessionODOO_URL — environment default URLThis allows you to:
Examples (conceptual):
text// Default: uses ODOO_URL from environment {{resolved_url}}/xmlrpc/2/common // Override for one operation: temporary_url = "https://staging.mycompany.odoo.com" {{resolved_url}}/xmlrpc/2/common // Override for session: user_url = "https://client-xyz.odoo.com" {{resolved_url}}/xmlrpc/2/common
Database name (db) precedence:
temporary_dbuser_dbODOO_DBUse this to:
Username precedence:
temporary_usernameuser_usernameODOO_USERNAMESecret (password or API key) precedence:
temporary_api_key or temporary_passworduser_api_key or user_passwordODOO_API_KEY (if set) or ODOO_PASSWORDImportant:
Environment variables are handled via standard OpenClaw metadata: requires.env declares required variables (ODOO_URL, ODOO_DB, ODOO_USERNAME, ODOO_PASSWORD). ODOO_API_KEY is an optional environment variable used instead of the password when present; it is not listed in metadata and should simply be set in the environment when needed.
At runtime the skill always works with:
{{resolved_url}} — final URL{{resolved_db}} — final database name{{resolved_username}} — final login{{resolved_secret}} — password or API key actually used to authenticateThese are computed using the precedence rules above.
> The temporary_* and user_* names are runtime context variables used by the skill logic, not OpenClaw metadata fields. OpenClaw does not have an optional.context metadata key; context is resolved dynamically at runtime as described below.
User examples:
odoo_demo juste pour cette opération"Behavior:
temporary_* (url, db, username, api_key/password)This is ideal for:
User examples:
clientx_prod pour cette session"Behavior:
user_* (url, db, username, api_key/password)temporary_* or by clearing user_*User examples:
Action:
user_url, user_db, user_username, user_password, user_api_keyODOO_URL, ODOO_DB, ODOO_USERNAME, ODOO_PASSWORD / ODOO_API_KEY)User examples:
Response should show (never full secrets):
textCurrent Odoo Context: - URL: https://client-xyz.odoo.com (user_url) - DB: clientxyz_prod (user_db) - Username: api_integration (user_username) - Secret: using API key (user_api_key) - Fallback URL: https://default.odoo.com (ODOO_URL) - Fallback DB: default_db (ODOO_DB)
Odoo exposes part of its server framework over XML-RPC (not REST). The External API is documented here: https://www.odoo.com/documentation/18.0/fr/developer/reference/external_api.html
Two main endpoints:
{{resolved_url}}/xmlrpc/2/common — authentication and meta calls{{resolved_url}}/xmlrpc/2/object — model methods via execute_kwCall version() on the common endpoint to verify URL and connectivity:
pythoncommon = xmlrpc.client.ServerProxy(f"{resolved_url}/xmlrpc/2/common") version_info = common.version()
Example result:
json{ "server_version": "18.0", "server_version_info": [18, 0, 0, "final", 0], "server_serie": "18.0", "protocol_version": 1 }
Use authenticate(db, username, password_or_api_key, {}) on the common endpoint:
pythonuid = common.authenticate(resolved_db, resolved_username, resolved_secret, {})
uid is an integer user ID and will be used in all subsequent calls.
If authentication fails, uid is False / 0 — the skill should:
ODOO_URL, ODOO_DB, username, and secretBuild an XML-RPC client for the object endpoint:
pythonmodels = xmlrpc.client.ServerProxy(f"{resolved_url}/xmlrpc/2/object")
Then use execute_kw with the following signature:
pythonmodels.execute_kw( resolved_db, uid, resolved_secret, "model.name", # e.g. "res.partner" "method_name", # e.g. "search_read" [positional_args], {keyword_args} )
All ORM operations in this skill are expressed in terms of execute_kw.
Domains are lists of conditions:
pythondomain = [["field_name", "operator", value], ...]
Examples:
[['is_company', '=', True]][['country_id', '=', france_id]][['probability', '>', 50]]Common operators:
"=", "!=", ">", ">=", "<", "<=""like", "ilike" (case-insensitive)"in", "not in""child_of" (hierarchical relations)YYYY-MM-DD or ISO 8601 format.int) when writing; reads often return [id, display_name].Each subsection below shows typical user queries and the corresponding execute_kw usage. They are applicable to any model (not only res.partner).
User queries:
Action (generic):
pythonids = models.execute_kw( resolved_db, uid, resolved_secret, "model.name", "search", [domain], {"offset": 0, "limit": 80} )
Notes:
domain is a list (can be empty [] to match all records).offset and limit for pagination.User queries:
Action:
pythoncount = models.execute_kw( resolved_db, uid, resolved_secret, "model.name", "search_count", [domain] )
User queries:
Action:
pythonrecords = models.execute_kw( resolved_db, uid, resolved_secret, "model.name", "read", [ids], {"fields": ["name", "country_id", "comment"]} )
If fields is omitted, Odoo returns all readable fields (often a lot).
Shortcut for search() + read() in a single call.
User queries:
Action:
pythonrecords = models.execute_kw( resolved_db, uid, resolved_secret, "model.name", "search_read", [domain], { "fields": ["name", "country_id", "comment"], "limit": 5, "offset": 0, # Optional: "order": "name asc" } )
User queries:
Action:
pythonnew_id = models.execute_kw( resolved_db, uid, resolved_secret, "model.name", "create", [{ "name": "New Partner" # other fields... }] )
Returns the newly created record ID.
User queries:
Action:
pythonsuccess = models.execute_kw( resolved_db, uid, resolved_secret, "model.name", "write", [ids, {"field": "new value", "other_field": 123}] )
Notes:
ids is a list of record IDs.ids receive the same values.User queries:
Action:
pythonsuccess = models.execute_kw( resolved_db, uid, resolved_secret, "model.name", "unlink", [ids] )
Useful for quick lookup on models with a display name (e.g. partners, products).
User queries:
Action:
pythonresults = models.execute_kw( resolved_db, uid, resolved_secret, "res.partner", "name_search", ["Agrolait"], {"limit": 10} )
Result is a list of [id, display_name].
res.partner is the core model for contacts, companies, and many business relations in Odoo.
User queries:
Action:
pythoncompanies = models.execute_kw( resolved_db, uid, resolved_secret, "res.partner", "search_read", [[["is_company", "=", True]]], {"fields": ["name", "country_id", "comment"], "limit": 80} )
User queries:
Action:
python[partner] = models.execute_kw( resolved_db, uid, resolved_secret, "res.partner", "read", [[7]], {"fields": ["name", "country_id", "comment"]} )
User queries:
Minimal body:
pythonpartner_id = models.execute_kw( resolved_db, uid, resolved_secret, "res.partner", "create", [{ "name": "New Partner", "is_company": True }] )
Additional fields examples:
street, zip, city, country_idemail, phone, mobilecompany_type ("person" or "company")User queries:
Action:
pythonmodels.execute_kw( resolved_db, uid, resolved_secret, "res.partner", "write", [[7], { "street": "New street 1", "phone": "+33 1 23 45 67 89" }] )
User queries:
Action:
pythonmodels.execute_kw( resolved_db, uid, resolved_secret, "res.partner", "unlink", [[999]] )
User queries:
Action:
pythonfields = models.execute_kw( resolved_db, uid, resolved_secret, "res.partner", "fields_get", [], {"attributes": ["string", "help", "type"]} )
The result is a mapping from field name to metadata:
json{ "name": {"type": "char", "string": "Name", "help": ""}, "country_id": {"type": "many2one", "string": "Country", "help": ""}, "is_company": {"type": "boolean", "string": "Is a Company", "help": ""} }
User queries:
Action:
pythonmodels_list = models.execute_kw( resolved_db, uid, resolved_secret, "ir.model", "search_read", [[]], {"fields": ["model", "name", "state"], "limit": 200} )
state indicates whether a model is defined in code ("base") or created dynamically ("manual").
User queries:
Action (simplified):
pythonpartner_model_ids = models.execute_kw( resolved_db, uid, resolved_secret, "ir.model", "search", [[["model", "=", "res.partner"]]] ) fields_meta = models.execute_kw( resolved_db, uid, resolved_secret, "ir.model.fields", "search_read", [[["model_id", "in", partner_model_ids]]], {"fields": ["name", "field_description", "ttype", "required", "readonly"], "limit": 500} )
authenticate returns False or later calls fail.xmlrpc/2/common or xmlrpc/2/object.The skill should:
limit / offset on search and search_read to handle large datasets.limit to a reasonable value (e.g. 80).fields list for read / search_read when possible.=, in) for large datasets.{{resolved_url}}, {{resolved_db}}, {{resolved_username}}, {{resolved_secret}}version() on {{resolved_url}}/xmlrpc/2/commonuidexecute_kw on res.partner with search_read and domain [['is_company', '=', True]]common.authenticatecreate a new res.partner with {"name": "New Partner", "is_company": True}read that ID with fields ["name", "is_company", "country_id"]temporary_url and/or temporary_db to point to another Odoo environment.This skill can:
search, search_count, read, search_read, create, write, unlink) on any Odoo model via execute_kw.res.partner (contacts / companies).fields_get, ir.model, and ir.model.fields.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-22 | pass→pass | 16,989 | 8,450 | -50% | 1 | 1 | 0% | 2,942 | 6,425 | +118% | 0 | 0 | — |
case-01 | pass→pass | 11,237 | 6,405 | -43% | 1 | 1 | 0% | 1,309 | 6,083 | +365% | 0 | 0 | — |
case-02 | pass→pass | 5,786 | 4,354 | -25% | 1 | 1 | 0% | 1,089 | 5,692 | +423% | 0 | 0 | — |
case-03 | fail→pass | 12,191 | 10,331 | -15% | 1 | 1 | 0% | 2,367 | 5,997 | +153% | 0 | 0 | — |
case-04 | pass→pass | 4,596 | 2,808 | -39% | 1 | 1 | 0% | 825 | 5,421 | +557% | 0 | 0 | — |
case-05 | pass→pass | 4,450 | 2,562 | -42% | 1 | 1 | 0% | 790 | 5,208 | +559% | 0 | 0 | — |
case-06 | pass→pass | 6,605 | 3,774 | -43% | 1 | 1 | 0% | 1,093 | 5,187 | +375% | 0 | 0 | — |
case-07 | pass→pass | 5,699 | 3,004 | -47% | 1 | 1 | 0% | 996 | 5,387 | +441% | 0 | 0 | — |
case-08 | pass→pass | 7,535 | 3,554 | -53% | 1 | 1 | 0% | 1,506 | 5,180 | +244% | 0 | 0 | — |
case-09 | fail→pass | 5,318 | 2,468 | -54% | 1 | 1 | 0% | 891 | 5,194 | +483% | 0 | 0 | — |
case-10 | pass→pass | 5,628 | 4,720 | -16% | 1 | 1 | 0% | 1,100 | 5,658 | +414% | 0 | 0 | — |
case-11 | fail→pass | 9,141 | 3,341 | -63% | 1 | 1 | 0% | 1,546 | 5,444 | +252% | 0 | 0 | — |
case-12 | pass→pass | 2,148 | 1,807 | -16% | 1 | 1 | 0% | 371 | 5,147 | +1287% | 0 | 0 | — |
case-13 | pass→pass | 4,709 | 2,465 | -48% | 1 | 1 | 0% | 769 | 5,271 | +585% | 0 | 0 | — |
case-14 | pass→pass | 6,128 | 2,832 | -54% | 1 | 1 | 0% | 1,089 | 5,324 | +389% | 0 | 0 | — |
case-15 | pass→pass | 3,536 | 1,916 | -46% | 1 | 1 | 0% | 546 | 5,217 | +855% | 0 | 0 | — |
case-21 | fail→fail | 12,890 | 13,352 | +4% | 1 | 1 | 0% | 2,330 | 7,375 | +217% | 0 | 0 | — |
case-16 | pass→pass | 7,291 | 4,464 | -39% | 1 | 1 | 0% | 1,308 | 5,706 | +336% | 0 | 0 | — |
case-17 | fail→pass | 12,190 | 3,340 | -73% | 1 | 1 | 0% | 1,956 | 5,500 | +181% | 0 | 0 | — |
case-18 | fail→pass | 10,772 | 4,223 | -61% | 1 | 1 | 0% | 1,876 | 5,517 | +194% | 0 | 0 | — |
case-19 | fail→fail | 7,498 | 6,286 | -16% | 1 | 1 | 0% | 1,232 | 5,979 | +385% | 0 | 0 | — |
case-20 | fail→fail | 19,694 | 25,635 | +30% | 1 | 1 | 0% | 4,607 | 11,051 | +140% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 22 cases were attempted. The headline lift of +23 percentage points is the difference between those two pass rates over the 22 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.