Ad – 728×90
🦉 Odoo Integration

Odoo OWL Services – Complete Reference

Odoo's service system is a registry of singleton objects — each providing a focused capability — that any OWL component can access via useService("name"). They are the backbone of every Odoo frontend interaction: the orm service reads and writes database records, notification shows toast messages, action navigates between views, and dialog opens modals programmatically. This lesson is a practical deep-dive into each major service with real Odoo code examples, plus a guide to writing your own custom service.

⏱️ 24 min read 🎯 Intermediate 📅 Updated 2026 👁️ Lesson 2 of 3

orm Service

The most used service. Wraps Odoo's JSON-RPC ORM calls with a clean async API.

JavaScript – orm service methods
/** @odoo-module **/
import { useService } from "@web/core/utils/hooks";

setup() {
  this.orm = useService("orm");
}

// ── searchRead: search + read in one call ─────────────────────────
const partners = await this.orm.searchRead(
  "res.partner",                              // model
  [["is_company", "=", true], ["active", "=", true]],  // domain
  ["id", "name", "email", "phone"],           // fields to fetch
  { limit: 20, offset: 0, order: "name asc" } // options
);

// ── read: fetch specific records by IDs ──────────────────────────
const [order] = await this.orm.read(
  "sale.order",
  [this.props.orderId],
  ["name", "partner_id", "amount_total", "state"]
);

// ── write: update records ─────────────────────────────────────────
await this.orm.write(
  "res.partner",
  [partnerId],
  { name: "New Name", phone: "+1234567890" }
);

// ── create: create a record ───────────────────────────────────────
const newId = await this.orm.create(
  "sale.order",
  { partner_id: 5, order_line: [[0, 0, { product_id: 12, product_uom_qty: 2 }]] }
);

// ── unlink: delete records ────────────────────────────────────────
await this.orm.unlink("res.partner", [id1, id2]);

// ── call: custom Python method on model ──────────────────────────
const result = await this.orm.call(
  "sale.order",           // model
  "action_confirm",       // method
  [[orderId]],            // positional args (first arg is always ids list)
  { raise_if_nothing: true }  // keyword args
);

notification Service

JavaScript – notification service
setup() {
  this.notification = useService("notification");
}

// Simple message
this.notification.add("Record saved successfully!");

// With type
this.notification.add("Invalid email address", { type: "warning" });
this.notification.add("Could not connect to server", { type: "danger" });
this.notification.add("Payment received", { type: "success" });

// Sticky — stays until manually closed
this.notification.add("Long background task started…", {
  type: "info",
  sticky: true,
});

// With action buttons
this.notification.add("New sale order created", {
  type: "success",
  buttons: [
    {
      name:  "View Order",
      primary: true,
      onClick: () => this.action.doAction({
        type: "ir.actions.act_window",
        res_model: "sale.order",
        res_id: newOrderId,
        views: [[false, "form"]],
      }),
    },
  ],
});
Ad – 336×280

action Service

JavaScript – action service
setup() {
  this.action = useService("action");
}

// Open a form view for a specific record
this.action.doAction({
  type:      "ir.actions.act_window",
  res_model: "res.partner",
  res_id:    partnerId,
  views:     [[false, "form"]],
  target:    "current",          // "current" | "new" (dialog) | "fullscreen"
});

// Open a list view with a domain
this.action.doAction({
  type:      "ir.actions.act_window",
  res_model: "sale.order",
  views:     [[false, "list"], [false, "form"]],
  domain:    [["state", "in", ["draft", "sent"]]],
  name:      "Draft Orders",
});

// Trigger by XML ID
this.action.doAction("sale.action_quotations_with_onboarding");

// Client action
this.action.doAction({ type: "ir.actions.client", tag: "my_addon.dashboard" });

// Switch current view type
this.action.switchView("kanban");

dialog Service

JavaScript – dialog service
import { ConfirmationDialog } from "@web/core/confirmation_dialog/confirmation_dialog";

setup() {
  this.dialog = useService("dialog");
}

// Built-in confirmation dialog
confirmDelete() {
  this.dialog.add(ConfirmationDialog, {
    title: "Delete Record",
    body:  "This action cannot be undone. Are you sure?",
    confirm: async () => {
      await this.orm.unlink("sale.order", [this.props.orderId]);
      this.notification.add("Deleted successfully", { type: "success" });
    },
    cancel: () => {},
  });
}

// Custom dialog component
openCustomDialog() {
  this.dialog.add(MyCustomDialog, {
    // Props passed to the dialog component
    orderId:  this.state.selectedId,
    onSave:   (data) => this.handleSave(data),
  });
}

user Service

JavaScript – user service
setup() {
  this.user = useService("user");
}

// Properties
const uid      = this.user.userId;        // res.users ID (number)
const name     = this.user.name;          // "Administrator"
const lang     = this.user.lang;          // "en_US"
const tz       = this.user.tz;            // "Europe/London"
const isAdmin  = this.user.isAdmin;       // true / false
const companies = this.user.allowedCompanies;

// Check group membership
const isSalesManager = await this.user.hasGroup("sales_team.group_sale_manager");
const isAccountant   = await this.user.hasGroup("account.group_account_user");

Writing a Custom Service

JavaScript – custom service registration
/** @odoo-module **/
import { registry } from "@web/core/registry";

// Services receive { env } and optionally dependencies from other services
const myAnalyticsService = {
  dependencies: ["orm", "user"],

  start(env, { orm, user }) {
    // Runs once when the service is initialized
    const sessionId = crypto.randomUUID();

    return {
      // Public API of the service
      track(eventName, payload = {}) {
        console.log(`[Analytics] ${eventName}`, {
          userId:    user.userId,
          sessionId,
          timestamp: Date.now(),
          ...payload,
        });
        // In production: POST to /web/dataset/call_kw or external API
      },
      setPage(pageName) {
        this.track("page_view", { page: pageName });
      },
    };
  },
};

// Register in the services category
registry.category("services").add("my_analytics", myAnalyticsService);

// Usage in any component:
// this.analytics = useService("my_analytics");
// this.analytics.track("button_click", { button: "confirm_order" });

📋 Summary

  • orm: searchRead, read, write, create, unlink, call — all return Promises.
  • notification: add(message, { type, sticky, buttons }) — types: info, success, warning, danger.
  • action: doAction(action | xmlid), switchView(type) — navigate to any Odoo view or client action.
  • dialog: add(DialogComponent, props) — opens any component as a modal; use built-in ConfirmationDialog for confirmations.
  • user: userId, name, lang, isAdmin, hasGroup(xmlid).
  • Custom services: register under registry.category("services") with a start(env, deps) method that returns the public API.

🏋️ Exercise

Build a QuickActionsPanel component using multiple services together:

  1. Use user service to show the logged-in user's name and hide admin-only buttons when !user.isAdmin.
  2. A "Confirm All Quotes" button: uses orm.call to call a Python method, then shows a success notification.
  3. A "View Pipeline" button: uses action.doAction to open the CRM pipeline kanban view.
  4. A "Delete Selected" button: uses dialog.add(ConfirmationDialog) before calling orm.unlink.
  5. Add a custom service audit_log that logs user actions to the console. Call it from each button click.

FAQ

Should I use orm service or rpc service directly? +

Use orm service for standard model operations (read, write, create, unlink, searchRead, call). Use rpc service only for non-ORM endpoints: custom controllers, JSON-RPC calls to routes you wrote, or third-party API calls through Odoo. The orm service handles authentication, session cookies, and error formatting automatically — the rpc service is lower-level.

Can services depend on other services? +

Yes — declare dependencies in the dependencies array of your service definition. Odoo resolves them and passes them as the second argument to start(env, { dep1, dep2 }). Circular dependencies will throw at startup. This is how orm internally uses the rpc service, and how the action service uses router.