Advertisement
🦉 Odoo Integration

OWL JS in the Odoo Backend – Module Structure & Registration

Building OWL components in isolation is one thing — integrating them into Odoo modules is another. Odoo has its own module system, asset pipeline, and component registries that OWL-based code must plug into. This lesson covers everything you need to write production OWL code inside an Odoo module: the @odoo-module pragma, folder structure conventions, the component registry, field widget development, and the assets manifest. This is the bridge between pure OWL knowledge and real Odoo development.

⏱️ 22 min read 🎯 Intermediate 📅 Updated 2026 👁️ Lesson 1 of 3

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.

JavaScript – @odoo-module at top of every file
/** @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 {
  // ...
}
⚠️
Without @odoo-module, imports will fail

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

Text – Odoo addon JS 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

Python – __manifest__.py assets
{
    '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.

JavaScript – registering a field widget
/** @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;
XML – using the widget in a view
<!-- In the form view XML -->
<field name="priority" widget="priority_stars"/>
Advertisement

Key Registry Categories

Registry categoryPurposeExample
fieldsField widgets for form/list/kanban viewsCustom input, star rating, color picker
viewsCustom view typesMap view, Gantt view, Calendar view
actionsClient-side actions triggered from menus/buttonsImport wizard, custom dashboard
main_componentsRoot-level components always present in the UIGlobal banner, chatbot widget
systrayTop-right system tray iconsNotification bell, custom indicator
servicesSingleton services available via useService()Custom data service, analytics tracker

Client Actions

JavaScript – client action component
/** @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);
Python – ir.actions.client record
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-specificStandalone equivalent
register in registrycall mount()
useService("orm")your own fetch/API code
manifest assetsyour 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:

  1. Create the addon skeleton: __manifest__.py, __init__.py, static/src/js/.
  2. Build a ColorPickerWidget that renders 5 color swatches (red, orange, green, blue, purple). Clicking one sets the field value to that color name.
  3. Show the currently selected color's swatch highlighted with a border.
  4. Register it in the fields registry for supportedTypes: ["char"].
  5. Add a Char field named label_color to res.partner and 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__.py under assets → 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.
  • standardFieldProps provides the standard props contract for field widgets (record, name, readonly, etc.).
  • Odoo services (orm, notification, action) are available via useService("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?

FAQ

Can I use OWL outside of the Odoo backend (website, portal)? +

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.

What is the difference between static/src/xml and defining templates with xml``? +

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.