odoo-agent
v1.0.1
Published
TypeScript library for agents to interact with Odoo: explore models, query data, and optionally write records.
Maintainers
Readme
odoo-agent
TypeScript library for agents and developers to interact with Odoo via its JSON-RPC API.
- Readonly by default — safe for AI agents browsing production data
- Explicit write opt-in — mutations only when you ask for them
- Zero runtime dependencies — uses native
fetch(Node 18+) - Fully typed — comprehensive TypeScript types for all Odoo constructs
Installation
npm install odoo-agent
# or
npm install @unergy/odoo-agentQuick start
import { OdooClient } from "odoo-agent";
const client = new OdooClient({
host: "https://mycompany.odoo.com",
database: "mycompany-prod",
username: "[email protected]",
password: process.env.ODOO_PASSWORD!,
});
// Or from environment variables:
const client = OdooClient.fromEnv();Environment variables: ODOO_HOST, ODOO_DATABASE, ODOO_USERNAME, ODOO_PASSWORD, ODOO_TIMEOUT_MS (optional).
Explorer — discover models and fields
client.explorer is always readonly and safe to call on production instances.
// Find models by keyword
const models = await client.explorer.searchModels("payroll");
// → [{ name: "Payslip", model: "hr.payslip" }, ...]
// All fields on a model
const fields = await client.explorer.modelFields("hr.payslip");
// → { state: { type: "selection", selection: [["draft","Draft"],...] }, ... }
// Filter fields by type
const dates = await client.explorer.modelFields("hr.payslip", "date");
const relations = await client.explorer.modelRelations("hr.payslip");
// → { employee_id: { type: "many2one", relation: "hr.employee" }, ... }
// Metadata for a single field
const meta = await client.explorer.fieldInfo("hr.payslip", "state");
// Count records (optionally filtered)
const total = await client.explorer.countRecords("hr.employee");
const active = await client.explorer.countRecords("hr.employee", [["active", "=", true]]);
// Peek at real data
const samples = await client.explorer.sample("hr.employee", {
limit: 3,
fields: ["name", "job_title", "department_id"],
domain: [["active", "=", true]],
});ORM — read data
// Search multiple records
const employees = await client.orm.filter("hr.employee", {
domain: [["department_id.name", "=", "Engineering"], ["active", "=", true]],
fields: ["name", "job_title", "department_id"],
limit: 20,
order: "name asc",
offset: 0,
});
// Get single record (throws OdooRecordNotFoundError if missing)
const employee = await client.orm.get("hr.employee", {
domain: [["identification_id", "=", "1234567890"]],
});
// Read by numeric ID(s)
const [partner] = await client.orm.readById("res.partner", 42, ["name", "email"]);
const many = await client.orm.readById("res.partner", [1, 2, 3]);
// Count
const drafts = await client.orm.count("hr.payslip", [["state", "=", "draft"]]);
// IDs only
const ids = await client.orm.search("hr.payslip", [["state", "=", "verify"]], 100);
// Single field value
const email = await client.orm.getValue("res.partner", "email", [["name", "=", "ACME"]]);
// List of values across records
const names = await client.orm.getValues("res.partner", "name", {
domain: [["is_company", "=", true]],
});ORM — write data (explicit opt-in)
OdooClient is readonly by default. Write operations throw OdooReadonlyError unless you opt in.
// Get a writable client (shares the existing authenticated connection)
const rw = client.withWriteAccess();
// Create → returns new record ID
const id = await rw.orm.create("res.partner", {
name: "ACME Corp",
email: "[email protected]",
is_company: true,
});
// Update (single ID or array)
await rw.orm.update("res.partner", id, { phone: "+57 300 000 0000" });
await rw.orm.update("hr.payslip", [101, 102], { state: "verify" });
// Delete (irreversible)
await rw.orm.delete("res.partner", id);
// Call business-logic methods
await rw.orm.callMethod("account.move", "action_post", [[invoiceId]]);
await rw.orm.callRecord("purchase.order", 55, "button_confirm");Or construct a writable client directly:
const rw = new OdooClient(config, { readonly: false });Domain (filter) syntax
Domains are arrays of [field, operator, value] conditions.
// Simple equality
[["state", "=", "open"]]
// Case-insensitive string search
[["name", "ilike", "john"]]
// Date range
[["date_from", ">=", "2026-01-01"], ["date_to", "<=", "2026-03-31"]]
// Set membership
[["state", "in", ["draft", "verify"]]]
// AND (implicit — all conditions in the array)
[["active", "=", true], ["department_id.name", "=", "Finance"]]
// OR (explicit "|" prefix)
["|", ["name", "ilike", "John"], ["name", "ilike", "Jane"]]
// NOT
["!", ["state", "=", "cancel"]]Valid operators: = != < <= > >= like ilike not like not ilike in not in child_of parent_of
Common Odoo models
| Concept | Model | Key fields |
|---|---|---|
| Partner / Contact | res.partner | name, email, phone, is_company, vat |
| Employee | hr.employee | name, identification_id, department_id, job_title, active |
| Payslip | hr.payslip | name, employee_id, state, date_from, date_to |
| Payslip batch | hr.payslip.run | name, state, date_start, date_end |
| Invoice | account.move | name, partner_id, state, move_type, amount_total, invoice_date |
| Invoice line | account.move.line | name, account_id, debit, credit, move_id |
| Leave / Absence | hr.leave | employee_id, holiday_status_id, state, date_from, date_to |
| Product | product.product | name, default_code, list_price, type |
| Analytic account | account.analytic.account | name, code, plan_id |
| Payroll attachment | hr.salary.attachment | employee_id, name, monthly_amount, state, date_start |
Use client.explorer.searchModels(keyword) when unsure of the exact model name.
Error handling
import {
OdooAuthError,
OdooRecordNotFoundError,
OdooReadonlyError,
OdooServerError,
OdooTimeoutError,
} from "odoo-agent";
try {
const emp = await client.orm.get("hr.employee", {
domain: [["identification_id", "=", "9999"]],
});
} catch (e) {
if (e instanceof OdooRecordNotFoundError) {
// No employee found with that ID
} else if (e instanceof OdooReadonlyError) {
// Tried to write on a readonly client
} else if (e instanceof OdooServerError) {
console.error(e.message, e.code, e.debug);
} else if (e instanceof OdooTimeoutError) {
// Server didn't respond in time
} else if (e instanceof OdooAuthError) {
// Wrong credentials or inactive user
}
}API reference
OdooClient
| Method / Property | Description |
|---|---|
| new OdooClient(config, options?) | Create client. options.readonly defaults to true. |
| OdooClient.fromEnv(options?) | Create from environment variables. |
| client.connect() | Authenticate eagerly. Returns uid. |
| client.withWriteAccess() | Return new writable client sharing same connection. |
| client.isReadonly | true if writes are blocked. |
| client.userId | Authenticated UID, or null. |
| client.explorer | OdooExplorer instance. |
| client.orm | OdooORM instance. |
OdooExplorer
| Method | Description |
|---|---|
| searchModels(keyword) | Find models by name/technical name. |
| countRecords(model, domain?) | Count records, optionally filtered. |
| modelFields(model, fieldType?) | All fields with metadata; optionally filter by type. |
| modelRelations(model) | Only relational fields (many2one / one2many / many2many). |
| fieldInfo(model, field) | Metadata for a single field. |
| sample(model, options?) | Fetch a few real records to inspect data shape. |
OdooORM
| Method | Description |
|---|---|
| filter(model, options?) | Search records. Returns array. |
| get(model, options?) | First matching record. Throws if none. |
| readById(model, ids, fields?) | Read by numeric ID(s). |
| count(model, domain?) | Count matching records. |
| search(model, domain?, limit?) | IDs only. |
| getValue(model, field, domain?) | Single field value from first match. |
| getValues(model, field, options?) | List of field values across records. |
| create(model, data) ⚠️ | Create record. Returns new ID. |
| update(model, ids, data) ⚠️ | Update records. |
| delete(model, ids) ⚠️ | Delete records permanently. |
| callMethod(model, method, args, kwargs?) | Call arbitrary model method. |
| callRecord(model, id, method, kwargs?) | Call method on a specific record. |
⚠️ Requires withWriteAccess() or { readonly: false }.
Requirements
- Node.js ≥ 18 (uses native
fetch) - Odoo 14+ (JSON-RPC endpoint)
