Ad – 728×90
🦉 OWL in Odoo

OWL Services in Odoo – orm, notification, dialog, and Custom Services

Services are singletons in Odoo's OWL environment that provide shared functionality to all components. You inject them with useService(). Odoo ships dozens of built-in services; you can also write your own to encapsulate shared logic for your module.

⏱️ 20 min 🎯 Intermediate 📅 Updated 2026
What you'll learn:
  • 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():

JavaScript
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:

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:

JavaScript
// 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
});
Ad – 728×90

dialog Service

Show confirmation dialogs or custom dialog components:

JavaScript
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:

JavaScript
/** @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').

Key takeaways:
  • Services are singletons — inject once in setup() via useService()
  • orm service covers all ORM operations: searchRead, create, write, unlink, call
  • notification.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

Frequently Asked Questions

What's the difference between orm.call and orm.searchRead?

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.

Can services hold reactive state?

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.

How do I call a Python @api.model method from JavaScript?

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.