orm Service
The most used service. Wraps Odoo's JSON-RPC ORM calls with a clean async API.
/** @odoo-module **/
import { useService } from "@web/core/utils/hooks";
setup() {
this.orm = useService("orm");
}
// ── searchRead: search + read in one call ─────────────────────────
const partners = await this.orm.searchRead(
"res.partner", // model
[["is_company", "=", true], ["active", "=", true]], // domain
["id", "name", "email", "phone"], // fields to fetch
{ limit: 20, offset: 0, order: "name asc" } // options
);
// ── read: fetch specific records by IDs ──────────────────────────
const [order] = await this.orm.read(
"sale.order",
[this.props.orderId],
["name", "partner_id", "amount_total", "state"]
);
// ── write: update records ─────────────────────────────────────────
await this.orm.write(
"res.partner",
[partnerId],
{ name: "New Name", phone: "+1234567890" }
);
// ── create: create a record ───────────────────────────────────────
const newId = await this.orm.create(
"sale.order",
{ partner_id: 5, order_line: [[0, 0, { product_id: 12, product_uom_qty: 2 }]] }
);
// ── unlink: delete records ────────────────────────────────────────
await this.orm.unlink("res.partner", [id1, id2]);
// ── call: custom Python method on model ──────────────────────────
const result = await this.orm.call(
"sale.order", // model
"action_confirm", // method
[[orderId]], // positional args (first arg is always ids list)
{ raise_if_nothing: true } // keyword args
);
notification Service
setup() {
this.notification = useService("notification");
}
// Simple message
this.notification.add("Record saved successfully!");
// With type
this.notification.add("Invalid email address", { type: "warning" });
this.notification.add("Could not connect to server", { type: "danger" });
this.notification.add("Payment received", { type: "success" });
// Sticky — stays until manually closed
this.notification.add("Long background task started…", {
type: "info",
sticky: true,
});
// With action buttons
this.notification.add("New sale order created", {
type: "success",
buttons: [
{
name: "View Order",
primary: true,
onClick: () => this.action.doAction({
type: "ir.actions.act_window",
res_model: "sale.order",
res_id: newOrderId,
views: [[false, "form"]],
}),
},
],
});
action Service
setup() {
this.action = useService("action");
}
// Open a form view for a specific record
this.action.doAction({
type: "ir.actions.act_window",
res_model: "res.partner",
res_id: partnerId,
views: [[false, "form"]],
target: "current", // "current" | "new" (dialog) | "fullscreen"
});
// Open a list view with a domain
this.action.doAction({
type: "ir.actions.act_window",
res_model: "sale.order",
views: [[false, "list"], [false, "form"]],
domain: [["state", "in", ["draft", "sent"]]],
name: "Draft Orders",
});
// Trigger by XML ID
this.action.doAction("sale.action_quotations_with_onboarding");
// Client action
this.action.doAction({ type: "ir.actions.client", tag: "my_addon.dashboard" });
// Switch current view type
this.action.switchView("kanban");
dialog Service
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";
setup() {
this.dialog = useService("dialog");
}
// Built-in confirmation dialog
confirmDelete() {
this.dialog.add(ConfirmationDialog, {
title: "Delete Record",
body: "This action cannot be undone. Are you sure?",
confirm: async () => {
await this.orm.unlink("sale.order", [this.props.orderId]);
this.notification.add("Deleted successfully", { type: "success" });
},
cancel: () => {},
});
}
// Custom dialog component
openCustomDialog() {
this.dialog.add(MyCustomDialog, {
// Props passed to the dialog component
orderId: this.state.selectedId,
onSave: (data) => this.handleSave(data),
});
}
user Service
setup() {
this.user = useService("user");
}
// Properties
const uid = this.user.userId; // res.users ID (number)
const name = this.user.name; // "Administrator"
const lang = this.user.lang; // "en_US"
const tz = this.user.tz; // "Europe/London"
const isAdmin = this.user.isAdmin; // true / false
const companies = this.user.allowedCompanies;
// Check group membership
const isSalesManager = await this.user.hasGroup("sales_team.group_sale_manager");
const isAccountant = await this.user.hasGroup("account.group_account_user");
Writing a Custom Service
/** @odoo-module **/
import { registry } from "@web/core/registry";
// Services receive { env } and optionally dependencies from other services
const myAnalyticsService = {
dependencies: ["orm", "user"],
start(env, { orm, user }) {
// Runs once when the service is initialized
const sessionId = crypto.randomUUID();
return {
// Public API of the service
track(eventName, payload = {}) {
console.log(`[Analytics] ${eventName}`, {
userId: user.userId,
sessionId,
timestamp: Date.now(),
...payload,
});
// In production: POST to /web/dataset/call_kw or external API
},
setPage(pageName) {
this.track("page_view", { page: pageName });
},
};
},
};
// Register in the services category
registry.category("services").add("my_analytics", myAnalyticsService);
// Usage in any component:
// this.analytics = useService("my_analytics");
// this.analytics.track("button_click", { button: "confirm_order" });
Odoo Services: The Bridge From OWL to the Backend
Services are shared singletons that give OWL components access to cross-cutting functionality — talking to the server, showing notifications, opening dialogs. A component grabs one with useService instead of reimplementing it.
| Service | Provides |
|---|---|
orm | read/write/search models (the main one) |
notification | toast messages |
dialog | modal dialogs/confirmations |
action | trigger window actions/navigation |
rpc | low-level server calls |
setup() {
this.orm = useService("orm");
this.notification = useService("notification");
}
async save() {
await this.orm.write("sale.order", [this.props.id], { state: "done" });
this.notification.add("Saved", { type: "success" });
}
Why services exist: functionality like "talk to the database" or "show a notification" is needed everywhere and should be a single shared instance — not re-created per component. A service is registered once on the environment; every component that calls useService("notification") talks to the same notification queue, the same ORM session. The ORM service is the workhorse: searchRead, read, write, create, unlink, and call (for any model method) — prefer it over the low-level rpc service, which you only need for non-standard endpoints, because orm handles Odoo's protocol for you. The rule: useService is a hook, so call it inside setup() (never in an event handler or after an await). Async everywhere: service calls return promises — await them and wrap in try/catch to surface server errors to the user via the notification service. You can write your own service for app-wide shared state or logic, registering it so any component can inject it. Think of services as OWL's dependency-injection system: the clean, shared channel between your UI and Odoo's backend and infrastructure.
🏋️ Practical Exercise
Build a QuickActionsPanel component using multiple services together:
- Use
userservice to show the logged-in user's name and hide admin-only buttons when!user.isAdmin. - A "Confirm All Quotes" button: uses
orm.callto call a Python method, then shows a successnotification. - A "View Pipeline" button: uses
action.doActionto open the CRM pipeline kanban view. - A "Delete Selected" button: uses
dialog.add(ConfirmationDialog)before callingorm.unlink. - Add a custom service
audit_logthat logs user actions to the console. Call it from each button click.
🔥 Challenge Exercise
Inside an Odoo OWL component, use useService('orm') to read records and useService('notification') to show feedback, then register a small custom service. Explain what services are, how useService accesses them, and why services centralize shared logic.
📋 Summary
- orm:
searchRead,read,write,create,unlink,call— all return Promises. - notification:
add(message, { type, sticky, buttons })— types: info, success, warning, danger. - action:
doAction(action | xmlid),switchView(type)— navigate to any Odoo view or client action. - dialog:
add(DialogComponent, props)— opens any component as a modal; use built-inConfirmationDialogfor confirmations. - user:
userId,name,lang,isAdmin,hasGroup(xmlid). - Custom services: register under
registry.category("services")with astart(env, deps)method that returns the public API.
Interview Questions
- What are services in Odoo’s OWL?
- What does
useServicedo? - What is the
ormservice? - How do you register a custom service?
- Name three built-in Odoo services.
Related Topics
FAQ
Use orm service for standard model operations (read, write, create, unlink, searchRead, call). Use rpc service only for non-ORM endpoints: custom controllers, JSON-RPC calls to routes you wrote, or third-party API calls through Odoo. The orm service handles authentication, session cookies, and error formatting automatically — the rpc service is lower-level.
Yes — declare dependencies in the dependencies array of your service definition. Odoo resolves them and passes them as the second argument to start(env, { dep1, dep2 }). Circular dependencies will throw at startup. This is how orm internally uses the rpc service, and how the action service uses router.

