What You Will Build
By the end of this guide, you will have a Python script that connects Claude to your Odoo instance. Claude will be able to search records, read data, create entries, and generate reports — all through Odoo's XML-RPC API. This is the foundation for building any custom AI agent for Odoo.
Prerequisites
- Python 3.10+
- An Odoo instance (deploy one free on DeployMonkey)
- Anthropic API key (console.anthropic.com)
- Admin credentials for your Odoo instance
Step 1: Connect to Odoo
import xmlrpc.client
ODOO_URL = 'https://your-odoo.deploymonkey.com' # Your Odoo URL
DB = 'your_database'
USER = 'admin'
PASSWORD = 'your_password'
# Authenticate
common = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/common')
uid = common.authenticate(DB, USER, PASSWORD, {})
print(f'Authenticated: uid={uid}')
# Get models proxy
models = xmlrpc.client.ServerProxy(f'{ODOO_URL}/xmlrpc/2/object')
def odoo_call(model, method, args=None, kwargs=None):
"""Helper to call Odoo methods."""
return models.execute_kw(
DB, uid, PASSWORD, model, method,
args or [], kwargs or {}
)Step 2: Define Odoo Tools for Claude
import anthropic
import json
client = anthropic.Anthropic()
# Define tools that Claude can use
tools = [
{
"name": "odoo_search",
"description": "Search for records in an Odoo model. Returns record IDs matching the domain filter.",
"input_schema": {
"type": "object",
"properties": {
"model": {"type": "string", "description": "Odoo model name, e.g. 'sale.order'"},
"domain": {"type": "array", "description": "Search domain, e.g. [['state','=','sale']]"},
"limit": {"type": "integer", "description": "Max results", "default": 10}
},
"required": ["model", "domain"]
}
},
{
"name": "odoo_read",
"description": "Read fields from Odoo records by ID.",
"input_schema": {
"type": "object",
"properties": {
"model": {"type": "string"},
"ids": {"type": "array", "items": {"type": "integer"}},
"fields": {"type": "array", "items": {"type": "string"}}
},
"required": ["model", "ids", "fields"]
}
},
{
"name": "odoo_read_group",
"description": "Aggregate data from an Odoo model (like SQL GROUP BY).",
"input_schema": {
"type": "object",
"properties": {
"model": {"type": "string"},
"domain": {"type": "array"},
"fields": {"type": "array", "items": {"type": "string"}},
"groupby": {"type": "array", "items": {"type": "string"}}
},
"required": ["model", "domain", "fields", "groupby"]
}
},
{
"name": "odoo_count",
"description": "Count records matching a domain.",
"input_schema": {
"type": "object",
"properties": {
"model": {"type": "string"},
"domain": {"type": "array"}
},
"required": ["model", "domain"]
}
}
]Step 3: Handle Tool Calls
def execute_tool(tool_name, tool_input):
"""Execute an Odoo tool call and return the result."""
if tool_name == "odoo_search":
ids = odoo_call(tool_input['model'], 'search',
[tool_input['domain']], {'limit': tool_input.get('limit', 10)})
return json.dumps(ids)
elif tool_name == "odoo_read":
records = odoo_call(tool_input['model'], 'read',
[tool_input['ids'], tool_input['fields']])
return json.dumps(records, default=str)
elif tool_name == "odoo_read_group":
result = odoo_call(tool_input['model'], 'read_group',
[tool_input['domain'], tool_input['fields'], tool_input['groupby']])
return json.dumps(result, default=str)
elif tool_name == "odoo_count":
count = odoo_call(tool_input['model'], 'search_count',
[tool_input['domain']])
return json.dumps(count)
return json.dumps({"error": f"Unknown tool: {tool_name}"})Step 4: Chat Loop with Tool Use
def chat_with_odoo(question: str) -> str:
"""Ask Claude a question about your Odoo data."""
messages = [{"role": "user", "content": question}]
while True:
response = client.messages.create(
model="claude-sonnet-4-20250514",
max_tokens=4096,
system="You are an Odoo data analyst. Use the available tools to "
"query the Odoo database and answer business questions. "
"Always use read_group for aggregations instead of reading "
"individual records.",
tools=tools,
messages=messages
)
# Check if Claude wants to use a tool
if response.stop_reason == "tool_use":
# Process each tool call
tool_results = []
for block in response.content:
if block.type == "tool_use":
result = execute_tool(block.name, block.input)
tool_results.append({
"type": "tool_result",
"tool_use_id": block.id,
"content": result
})
messages.append({"role": "assistant", "content": response.content})
messages.append({"role": "user", "content": tool_results})
else:
# Claude has the final answer
return response.content[0].text
# Example usage
print(chat_with_odoo("What were our top 5 customers by revenue this quarter?"))
print(chat_with_odoo("How many open support tickets do we have?"))
print(chat_with_odoo("What is our inventory value by warehouse?"))Security Best Practices
- Use a dedicated read-only Odoo user — Do not use admin credentials for analytics queries
- Limit accessible models — Define which models the tools can access
- Set query timeouts — Prevent long-running queries from blocking the server
- Log all queries — Audit trail for compliance and debugging
- Rate limit tool calls — Prevent runaway agents from overwhelming the API
Going Further
- Add write tools (create, write) for automation agents — with appropriate guardrails
- Add SSH tools for server monitoring agents
- Add file tools for coding agents
- Deploy as a web service for team access
- Connect to DeployMonkey's built-in AI agent for a managed experience