OWL Services: Shared Tools Any Component Can Use
Services are long-lived singletons that provide functionality across the whole web client — talking to the server, showing notifications, opening dialogs. A component grabs one with the useService hook instead of re-implementing it.
import { useService } from "@web/core/utils/hooks";
class OrderButton extends Component {
setup() {
this.orm = useService("orm"); // server calls
this.notification = useService("notification");
}
async confirm() {
await this.orm.write("sale.order", [this.props.id], { state: "done" });
this.notification.add("Order confirmed", { type: "success" });
}
}
| Service | Provides |
|---|---|
orm | read/write/search Odoo models |
notification | toast messages |
dialog | modal dialogs |
rpc | low-level server calls |
action | trigger window actions |
Why services and not imports: a service is a single shared instance registered in the environment, so every component talks to the same notification queue, the same ORM session. useService must be called in setup() (it's a hook). You can register your own services for app-wide shared state or logic.
🏋️ Practical Exercise
- Inject a service with
useService. - Use the
ormservice to call a model. - Use the
notificationservice. - Use the
rpcservice. - Register a custom service.
🔥 Challenge Exercise
Use the orm service inside an OWL component to read records and the notification service to show feedback. Explain what services are in OWL/Odoo, how useService provides them, and why services centralize cross-cutting concerns.
- How services work and the
useService()hook - The orm service: searchRead, create, write, unlink
- The notification service: success, warning, danger toasts
- The dialog service: confirmation dialogs and custom dialogs
- Writing and registering a custom service
How Services Work
A service is a plain JavaScript object registered in the services registry. It is instantiated once when the Odoo web client boots, and the same instance is injected into every component that requests it. Services can depend on other services.
In a component, inject a service in setup():
import { useService } from '@web/core/utils/hooks';
setup() {
this.orm = useService('orm');
this.notification = useService('notification');
this.dialog = useService('dialog');
this.user = useService('user');
this.router = useService('router');
this.action = useService('action');
}
orm Service
The orm service is the primary way to call Odoo model methods from JavaScript:
// search + read combined
const partners = await this.orm.searchRead(
'res.partner', // model
[['is_company', '=', true]], // domain
['name', 'email', 'phone'], // fields
{ limit: 20, order: 'name asc' }, // options
);
// search only (returns IDs)
const ids = await this.orm.search('res.partner', [['name', 'like', 'Al']]);
// read specific records
const records = await this.orm.read('res.partner', [1, 2, 3], ['name', 'email']);
// searchCount
const total = await this.orm.searchCount('res.partner', []);
// create
const newId = await this.orm.create('library.book', {
name: 'Clean Code', author: 'Robert C. Martin',
});
// write
await this.orm.write('library.book', [newId], { available: true });
// unlink
await this.orm.unlink('library.book', [newId]);
// call arbitrary model method
const result = await this.orm.call(
'library.book', 'check_availability', [[bookId]], {}
);
notification Service
Show non-blocking toast messages:
// Success (green)
this.notification.add('Record saved successfully!', { type: 'success' });
// Warning (yellow)
this.notification.add('Low stock warning.', { type: 'warning' });
// Danger (red)
this.notification.add('Operation failed.', { type: 'danger' });
// Info (blue, default)
this.notification.add('Processing your request…');
// With auto-dismiss duration (ms) and sticky option
this.notification.add('Changes saved.', {
type: 'success',
sticky: false, // auto-dismiss (default)
});
this.notification.add('Important notice.', {
type: 'warning',
sticky: true, // stays until user closes
});
dialog Service
Show confirmation dialogs or custom dialog components:
import { ConfirmationDialog } from '@web/core/confirmation_dialog/confirmation_dialog';
// Simple confirmation
this.dialog.add(ConfirmationDialog, {
title: 'Delete Book',
body: 'Are you sure you want to delete this book?',
confirm: async () => {
await this.orm.unlink('library.book', [this.bookId]);
this.notification.add('Book deleted.', { type: 'success' });
},
cancel: () => {/* optional cancel handler */},
});
// Custom dialog component
import { MyCustomDialog } from './my_custom_dialog';
this.dialog.add(MyCustomDialog, {
title: 'Custom Dialog',
bookId: this.bookId,
onSave: (result) => {
this.state.savedResult = result;
},
});
Writing a Custom Service
Encapsulate shared logic (e.g., a book availability cache) in a custom service:
/** @odoo-module **/
import { registry } from '@web/core/registry';
const bookService = {
dependencies: ['orm', 'notification'],
async start(env, { orm, notification }) {
const cache = new Map();
return {
async checkAvailability(bookId) {
if (cache.has(bookId)) {
return cache.get(bookId);
}
const [book] = await orm.read(
'library.book', [bookId], ['available_copies']
);
const available = book.available_copies > 0;
cache.set(bookId, available);
return available;
},
clearCache() {
cache.clear();
},
};
},
};
registry.category('services').add('book', bookService);
Inject it in any component with this.book = useService('book').
- Services are singletons — inject once in
setup()viauseService() ormservice covers all ORM operations: searchRead, create, write, unlink, callnotification.add(msg, {type})for toasts;dialog.add(Component, props)for dialogs- Custom services are registered in
registry.category('services')and can depend on other services
Interview Questions
- What are services in OWL?
- What does
useServicedo? - What is the
ormservice used for? - How do you show a notification from a component?
- How do you register a custom service?
Related Topics
FAQ
orm.searchRead(model, domain, fields, opts) is a convenience wrapper for the common search+read pattern. orm.call(model, method, args, kwargs) calls any Python model method by name — use it for custom model methods, workflow actions, or any method not covered by the ORM convenience wrappers.
Yes — use reactive() from @odoo/owl inside the service's start() method to create reactive data that components can observe. Components will re-render when the reactive object changes, even though the data lives in the service rather than the component's local state.
Use orm.call(modelName, methodName, positionalArgs, keywordArgs). For example: await this.orm.call('library.book', 'get_bestsellers', [], {limit: 5}). The method must be decorated with @api.model on the Python side. Record-based methods (decorated with @api.multi or @api.one) pass record IDs as the first positional argument.

