The @odoo-module Pragma
Every JavaScript file in an Odoo module that uses ES modules must start with /** @odoo-module **/. This tells Odoo's bundler (Rollup/Webpack) to process the file as an ES module and enables the import / export syntax.
/** @odoo-module **/
import { Component, xml, useState, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
// Your component code here
class MyWidget extends Component {
// ...
}
Odoo's asset bundler uses the pragma to switch files into ES module mode. Without it, import statements are not processed and you get "Unexpected token 'import'" errors. Put /** @odoo-module **/ as the very first line of every JS file in your Odoo addon that uses imports.
Standard Folder Structure
my_addon/
├── __manifest__.py
├── models/
│ └── sale_order.py
├── views/
│ └── sale_order_views.xml
└── static/
├── description/
│ └── icon.png
└── src/
├── js/
│ ├── components/
│ │ ├── my_widget/
│ │ │ ├── my_widget.js
│ │ │ ├── my_widget.xml ← separate template file (optional)
│ │ │ └── my_widget.scss
│ │ └── dashboard/
│ │ └── dashboard.js
│ ├── hooks/
│ │ └── use_my_data.js
│ └── services/
│ └── my_service.js
└── xml/
└── my_templates.xml
Registering Assets in __manifest__.py
{
'name': 'My Addon',
'version': '17.0.1.0.0',
'depends': ['web', 'sale'],
'assets': {
# Add to the main web client bundle
'web.assets_backend': [
'my_addon/static/src/js/components/**/*.js',
'my_addon/static/src/js/components/**/*.xml',
'my_addon/static/src/js/components/**/*.scss',
'my_addon/static/src/js/hooks/*.js',
'my_addon/static/src/js/services/*.js',
],
# Frontend assets (website, portal)
'web.assets_frontend': [
'my_addon/static/src/js/portal_widget.js',
],
},
}
The Component Registry
In Odoo, components are not mounted directly — they are registered in the Odoo component registry and the framework instantiates them when needed. Different registry categories serve different purposes.
/** @odoo-module **/
import { Component, xml } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { standardFieldProps } from "@web/views/fields/standard_field_props";
class PriorityStars extends Component {
static template = xml`
<div class="o_priority">
<span
t-foreach="[1,2,3]"
t-as="n"
t-key="n"
t-att-class="{ 'fa fa-star': true, 'o_priority_star': true, filled: props.value >= n }"
t-on-click="() => setValue(n)"
role="img"
t-att-aria-label="'Priority ' + n"
/>
</div>
`;
static props = {
...standardFieldProps, // spreads id, name, record, readonly, etc.
};
setValue(n) {
if (!this.props.readonly) {
this.props.record.update({ [this.props.name]: n });
}
}
}
// Register as a field widget named "priority_stars"
registry.category("fields").add("priority_stars", {
component: PriorityStars,
displayName: "Priority Stars",
supportedTypes: ["integer"],
});
export default PriorityStars;
<!-- In the form view XML -->
<field name="priority" widget="priority_stars"/>
Key Registry Categories
| Registry category | Purpose | Example |
|---|---|---|
fields | Field widgets for form/list/kanban views | Custom input, star rating, color picker |
views | Custom view types | Map view, Gantt view, Calendar view |
actions | Client-side actions triggered from menus/buttons | Import wizard, custom dashboard |
main_components | Root-level components always present in the UI | Global banner, chatbot widget |
systray | Top-right system tray icons | Notification bell, custom indicator |
services | Singleton services available via useService() | Custom data service, analytics tracker |
Client Actions
/** @odoo-module **/
import { Component, xml, useState, onWillStart } from "@odoo/owl";
import { useService } from "@web/core/utils/hooks";
import { registry } from "@web/core/registry";
class SalesDashboard extends Component {
static template = xml`
<div class="o_action o_sales_dashboard">
<div class="o_dashboard_header">
<h1>Sales Dashboard</h1>
</div>
<div class="o_dashboard_metrics">
<div t-foreach="state.metrics" t-as="metric" t-key="metric.label" class="o_metric_card">
<span class="o_metric_value" t-esc="metric.value"/>
<span class="o_metric_label" t-esc="metric.label"/>
</div>
</div>
</div>
`;
state = useState({ metrics: [] });
setup() {
this.orm = useService("orm");
onWillStart(() => this.loadMetrics());
}
async loadMetrics() {
const [result] = await this.orm.call(
"sale.order",
"get_dashboard_metrics",
[]
);
this.state.metrics = result;
}
}
// Register as a client action
registry.category("actions").add("my_addon.sales_dashboard", SalesDashboard);
self.env['ir.actions.client'].create({
'name': 'Sales Dashboard',
'tag': 'my_addon.sales_dashboard',
'target': 'current',
})
Using OWL Within an Odoo Module
Writing OWL in Odoo differs from standalone OWL in three practical ways: you don't mount components yourself (the registry does), you reach the server through services, and your assets must be declared in the manifest.
import { Component, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
class Dashboard extends Component {
static template = "my_module.Dashboard";
setup() {
this.orm = useService("orm"); // server access
this.state = useState({ rows: [] });
onWillStart(async () => {
this.state.rows = await this.orm.searchRead("sale.order", [], ["name"]);
});
}
}
registry.category("actions").add("my_dashboard", Dashboard); // register, don't mount
| Odoo-specific | Standalone equivalent |
|---|---|
register in registry | call mount() |
useService("orm") | your own fetch/API code |
manifest assets | your bundler config |
You register, you don't mount: in Odoo you add your component to a registry category (actions for a client action, fields for a field widget, view_widgets, etc.), and Odoo instantiates it when appropriate — e.g. a client action registered under a name can be opened by a menu/action record. This is the biggest shift from standalone OWL. Services are your bridge to the backend: useService("orm") lets you searchRead/write/call models; useService("notification") shows toasts; useService("action") triggers window actions. Fetch initial data in onWillStart so the first render has it. Templates live in XML referenced by dotted name, and both JS and XML must be declared in the manifest's assets bundle (usually web.assets_backend) or nothing loads — the classic beginner trip-up. Extending vs creating: to modify existing Odoo components use patch(); to add new UI, create and register your own. Reminder: after changing JS/XML, reload assets (dev mode) and hard-refresh the browser — and after manifest changes, upgrade the module. Everything else is normal OWL: components, state, props, hooks.
🏋️ Practical Exercise
Create a minimal Odoo addon with a custom OWL field widget:
- Create the addon skeleton:
__manifest__.py,__init__.py,static/src/js/. - Build a
ColorPickerWidgetthat renders 5 color swatches (red, orange, green, blue, purple). Clicking one sets the field value to that color name. - Show the currently selected color's swatch highlighted with a border.
- Register it in the
fieldsregistry forsupportedTypes: ["char"]. - Add a
Charfield namedlabel_colortores.partnerand add the widget to the form view.
🔥 Challenge Exercise
Add a custom OWL component into Odoo: define it, register it in the appropriate registry (e.g. systray or a view), add it to an assets bundle, and load it. Explain how Odoo discovers OWL components via registries and asset bundles.
📋 Summary
- Every Odoo JS file using ES modules must start with
/** @odoo-module **/. - Register JS files in
__manifest__.pyunderassets → web.assets_backend. - Use
registry.category("fields").add("widget_name", { component })to register custom field widgets. - Use
registry.category("actions").add("module.tag", Component)for client actions. standardFieldPropsprovides the standard props contract for field widgets (record, name, readonly, etc.).- Odoo services (
orm,notification,action) are available viauseService("name")in any registered component.
Interview Questions
- How does Odoo use OWL?
- What is a registry in Odoo’s OWL setup?
- How are OWL components loaded in Odoo?
- How do you extend an existing Odoo component?
- What is an assets bundle?
Related Topics
FAQ
Yes — add your JS files to web.assets_frontend in the manifest instead of web.assets_backend. Most Odoo OWL services are available in the frontend too. However, the full ORM service is not available on the public portal (unauthenticated). For portal/website OWL widgets, use useService("rpc") with public controller routes instead.
Both work. xml`` (tagged template literal) defines the template inline in the JS file — convenient for small templates. Separate .xml template files allow you to use Odoo's QWeb engine with template inheritance (t-inherit) and translation (t-translation). For large templates or templates that need inheritance/translation, use separate XML files. For small widget templates, inline xml`` is fine.

