OWL in Odoo: The Modern Web Client
OWL is Odoo's own React-like JavaScript framework, and since Odoo 15+ it powers the entire web client. When you need custom interactivity beyond what XML views offer, you write OWL components.
import { Component, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
class MyWidget extends Component {
static template = "my_module.MyWidget"; // an XML QWeb template
setup() {
this.state = useState({ count: 0 });
this.orm = useService("orm"); // talk to the server
}
}
registry.category("actions").add("my_widget", MyWidget); // make it usable
| Piece | Role |
|---|---|
| Component | class + QWeb template |
setup() | state, hooks, services |
services (orm, notification) | shared functionality |
| registry | where components/widgets are registered |
Where OWL fits: most Odoo screens are declared in XML (form/list/kanban views) and need no JavaScript. You reach for OWL when you need custom interactivity — a bespoke dashboard, a special field widget, a client action with its own UI. The registry is central to Odoo's OWL: you don't just render components anywhere — you register them (as fields, actions, or components) so Odoo's web client knows how to use them, e.g. a custom field widget you can then reference in a view with widget="my_widget". Services connect OWL to Odoo: useService("orm") lets a component read/write models, notification shows toasts, action triggers window actions — the bridge between your frontend and the Python backend. Templates are QWeb XML (the same templating used for reports), referenced by name. Prerequisite: comfort with modern JavaScript and component thinking (state, props, one-way data flow) — OWL concepts mirror React/Vue, so that knowledge transfers directly.
🏋️ Practical Exercise
- Note that OWL is Odoo’s frontend framework.
- Identify a component
.jsfile. - Register a component.
- Note the assets bundle.
- Reload assets after a change.
🔥 Challenge Exercise
Explain OWL’s role as Odoo’s modern JavaScript framework (replacing the old widget system): how components are registered and loaded via asset bundles, and how it compares conceptually to React/Vue (components, reactive state, templates).
- Where OWL is used in Odoo (backend, POS, website editor)
- How asset bundles work and how to add your files
- The
@odoo-moduledirective and ES module system - The service and registry pattern
- How the Odoo web client boots
Where OWL is Used in Odoo
| Area | OWL usage | Main asset bundle |
|---|---|---|
| Backend (/web) | All views, dialogs, widgets, action manager | web.assets_backend |
| Point of Sale | Full POS app — screens, components, popups | point_of_sale.assets |
| Website editor | Editor toolbar, snippet options, overlay | website.assets_wysiwyg |
| Frontend (public pages) | Minimal — some widgets (chat, form) | web.assets_frontend |
Backend and POS customizations go into different bundles. Never put backend code in the POS bundle or vice versa — they boot separately and have different registries.
Asset Bundles
Asset bundles are named collections of JS, CSS, and XML files. Declare files in your module's __manifest__.py:
'assets': {
# Backend customizations
'web.assets_backend': [
'my_module/static/src/js/my_widget.js',
'my_module/static/src/xml/my_widget.xml',
'my_module/static/src/scss/my_widget.scss',
],
# POS customizations
'point_of_sale.assets': [
'my_module/static/src/pos/my_pos_component.js',
'my_module/static/src/pos/my_pos_component.xml',
],
# Public website
'web.assets_frontend': [
'my_module/static/src/js/website_widget.js',
],
}
Odoo builds bundles at startup (or from cache). In development mode (?debug=assets), bundles are rebuilt on every request.
@odoo-module Directive
Every JS file in an Odoo asset bundle must start with /** @odoo-module **/. This marks the file as an ES module in Odoo's module system and enables imports from other Odoo modules:
/** @odoo-module **/
// Import from Odoo core
import { Component, useState, onMounted } from '@odoo/owl';
import { registry } from '@web/core/registry';
import { useService } from '@web/core/utils/hooks';
// Import from another module in the same bundle
import { MyHelperClass } from 'my_module/static/src/js/helpers';
export class MyComponent extends Component {
static template = 'my_module.MyComponent';
setup() {
this.orm = useService('orm');
this.state = useState({ count: 0 });
}
}
The path in imports like '@web/core/registry' resolves to the module exported from web/static/src/core/registry.js in the Odoo source.
The Registry System
Registries are the primary mechanism for extending Odoo's UI without patching. A registry is a key→value store where you register components, services, or views under a string key:
/** @odoo-module **/
import { registry } from '@web/core/registry';
import { MyFieldWidget } from './my_field_widget';
// Register a custom field widget
registry.category('fields').add('my_widget', {
component: MyFieldWidget,
displayName: 'My Custom Widget',
supportedTypes: ['char', 'text'],
});
// Use it in a view XML: widget="my_widget"
Common registry categories:
| Category | Purpose |
|---|---|
| fields | Field widgets for form/list views |
| views | View types (form, list, kanban, etc.) |
| services | Singleton services injected into components |
| actions | Client-side action handlers |
| systray | Items in the top-right system tray |
Services
Services are singletons that provide shared functionality to all components. Access them via useService():
/** @odoo-module **/
import { Component } from '@odoo/owl';
import { useService } from '@web/core/utils/hooks';
export class MyComponent extends Component {
static template = 'my_module.MyComponent';
setup() {
this.orm = useService('orm'); // ORM calls
this.notification = useService('notification'); // Toast messages
this.dialog = useService('dialog'); // Dialog popups
this.router = useService('router'); // URL routing
this.user = useService('user'); // Current user info
}
async loadData() {
const records = await this.orm.searchRead(
'res.partner',
[['is_company', '=', true]],
['name', 'email'],
{ limit: 10 },
);
this.notification.add('Loaded!', { type: 'success' });
}
}
- Backend, POS, and website editor use separate OWL asset bundles — add files to the correct bundle
- Every JS file needs
/** @odoo-module **/at the top - Use registries to add field widgets, views, services, and systray items without patching
- Access shared functionality (ORM, notifications, dialogs) through services via
useService()
Interview Questions
- What is OWL?
- Why did Odoo build OWL?
- How does OWL compare to React or Vue?
- How are OWL components loaded in Odoo?
- What is an assets bundle?
Related Topics
FAQ
A registry registers a new entry under a key — it adds to the system. A patch modifies an existing component or class in-place. Use registries for adding new field widgets, services, and actions. Use patches for extending existing POS components or altering behavior of a specific existing component.
Open DevTools and use the OWL Devtools browser extension (available for Chrome/Firefox). It shows the component tree, component state, props, and environment. In development mode (?debug=1), Odoo also adds extra error messages and disables caching.
Not directly via npm — Odoo's asset system doesn't use npm. You can include a pre-bundled JavaScript library by adding the file to your module's static/lib/ folder and registering it in the manifest. Alternatively, load it from a CDN via a <script> tag in a QWeb template, though this adds a network dependency.

