Skip to content

Odoo 6 min read Updated

Odoo Studio vs a custom module: which should you use?

When Odoo Studio is enough, when you need a real addon, and how Studio's data-based customizations affect version control, upgrades and mixing the two.

Written by the erpfly team, people who build on Frappe and Odoo for a living.

Use Odoo Studio for fields, form layout, simple automations and report tweaks that a functional admin should be able to change without a developer. Use a custom module when there’s real business logic: validation that must never be skipped, changes to how standard documents compute or post, integrations, or anything you need to test and review before it reaches production. Studio is an Enterprise feature and stores everything as database records, not code, so it’s quick to use and awkward to version, diff or promote from staging to production. Plenty of good Odoo setups use both, with a clear rule about which goes where.

What Studio actually is

Studio is the web_studio app, available on Odoo Enterprise only. If you’re on Community, the question is already answered. On Odoo Online it isn’t part of every plan either; at the time of writing it sits in the Custom plan rather than Standard, so check your subscription before planning around it.

The important thing to understand is where Studio puts your work. It doesn’t generate Python files. Everything it creates is a row in the database:

  • New fields are ir.model.fields records with names starting x_studio_.
  • New models are ir.model records with names starting x_.
  • Form and list changes are inherited ir.ui.view records containing XPath against the standard views.
  • Automations are base.automation records, often with a server action behind them.
  • Report edits are QWeb views attached to ir.actions.report.

Studio groups these under a module name, studio_customization, and it can export them as a zip containing that module so you can import them into another database. That export is XML data files, not a module you’d want to maintain by hand.

Where Studio is genuinely fine

We’re not Studio skeptics. For a lot of changes it’s the right tool, and building a module for them would be overkill.

  • Adding fields people fill in by hand. A “Site contact phone” on the sale order, a “Batch notes” field on a manufacturing order. No logic, just storage and display.
  • Rearranging forms. Moving fields to a tab, hiding what your team never uses, making a field required for one form.
  • Simple automation. Assign a salesperson when a lead reaches a stage, send an email when a ticket’s been idle, set a tag on create.
  • Small report edits. Your logo placement, an extra field on the invoice layout, a different footer.
  • Prototyping. Letting a department lead build the screen they think they want before a developer writes it properly.

The common thread: if the change broke tomorrow, someone would notice quickly and nothing financial would be wrong.

Where Studio runs out

Studio’s limits aren’t about what you can click. Automations can run Python code in a server action, so technically quite a lot is possible. The limits are about what you can do safely.

No proper overrides. You can’t cleanly extend action_confirm or change what a standard method computes. Automations fire on record events (created, updated, a field changing), so a rule like “an order with a forklift delivery can’t be confirmed without a street address” becomes an automation watching the state field that raises a UserError. It works. It also lives in a text field where a developer reviewing the sales flow won’t think to look.

Server action code is sandboxed. No imports, no helper modules, no shared functions. Each action is its own island of code in a text field, and the moment you have ten of them, copy-paste becomes the architecture.

No tests. There’s nowhere to put them. Every change is verified by clicking, in whatever database you happened to make it in.

No front-end code. Custom OWL components, new widgets, controllers for a portal page or a webhook: all of that needs a module.

XPath you didn’t write. Studio’s view inheritance targets the standard view’s structure at the moment you made the change. It’s usually sensible. When it isn’t, you’re debugging generated XPath in a database record.

Version control and promotion: the real cost

This is the part people underestimate. A custom module lives in Git. You branch, review a pull request, run tests on a staging build, and merge. On Odoo.sh the whole platform is designed around that flow.

Studio changes live in whichever database they were made in. If you make them in production, you have no staging review. If you make them in staging, a rebuild from production can wipe them, and they don’t follow your Git branch to production. You’re left with export and import, or making the same change twice by hand.

There’s no diff either. When something on the sale order form changes behaviour, you can’t git log your way to who changed it and why. You go digging through view records and automation histories.

For a single admin making occasional field changes, that’s tolerable. For a team of three people all customising the same models, it becomes a real problem within a few months.

Upgrades cut both ways

Because Studio customizations are data, they travel with your database through Odoo’s upgrade process, which is a genuine advantage. Custom modules, by contrast, have to be ported to each new version by whoever maintains them. Model and view changes between Odoo 17, 18 and 19 mean a module written for one won’t necessarily install on the next.

But data-based doesn’t mean upgrade-proof. A Studio view that inherits a standard form can fail to apply when that form’s structure changes, and a server action that references a renamed field fails when it runs, not when the upgrade finishes. The difference is that a module’s breakage shows up in a test run on a staging build. Studio’s breakage shows up when a user hits it.

What the module version looks like

For comparison, here’s the forklift rule as module code, for Odoo 17 to 19. The constraint runs on every write, including imports and API calls.

from odoo import api, fields, models
from odoo.exceptions import ValidationError


class SaleOrder(models.Model):
    _inherit = "sale.order"

    delivery_window = fields.Selection(
        [("am", "Morning"), ("pm", "Afternoon")],
        string="Delivery window",
    )
    requires_forklift = fields.Boolean(string="Requires forklift")

    @api.constrains("requires_forklift", "partner_shipping_id")
    def _check_forklift_address(self):
        for order in self:
            if order.requires_forklift and not order.partner_shipping_id.street:
                raise ValidationError("Forklift deliveries need a full street address.")

And the view, using the inline invisible expression that replaced attrs in Odoo 17:

<odoo>
    <record id="view_order_form_delivery_window" model="ir.ui.view">
        <field name="name">sale.order.form.delivery.window</field>
        <field name="model">sale.order</field>
        <field name="inherit_id" ref="sale.view_order_form"/>
        <field name="arch" type="xml">
            <xpath expr="//field[@name='payment_term_id']" position="after">
                <field name="delivery_window"/>
                <field name="requires_forklift" invisible="not delivery_window"/>
            </xpath>
        </field>
    </record>
</odoo>

It’s more work than dragging two fields onto a form. It’s also reviewable, testable and identical on every database you install it on. If you haven’t built one before, our guide to creating a custom module in Odoo walks through the full addon structure, and _inherit vs _inherits explains the extension pattern used above.

Our opinion: give Studio a boundary, not a ban

Banning Studio outright usually fails. Admins need to add a field on a Friday afternoon, and they’ll find a way. What works is a written boundary:

  • Studio may add fields, change layouts and create notification-style automations.
  • Anything that blocks a save, changes amounts, touches accounting, stock moves or payroll, or integrates with another system goes in a module.
  • Module code never depends on x_studio_ fields. If logic needs a field, the module defines it.

That last rule matters most. When a module reads a Studio field, anyone with Studio access can rename or delete it and silently break code they can’t see.

Moving a Studio field into a module

When a Studio field graduates into real logic, define a proper field in your module and copy the data across. If it’s the module’s first install, migration scripts won’t run, so use a post_init_hook (Odoo 17 and later pass it env):

# my_sales/__init__.py
from . import models


def post_init_hook(env):
    env.cr.execute("""
        UPDATE sale_order
           SET delivery_window = x_studio_delivery_window
         WHERE x_studio_delivery_window IS NOT NULL
    """)

Register it in the manifest with "post_init_hook": "post_init_hook". If the Studio field was a selection, check its keys match yours before copying, then remove the Studio field once the module version is live and verified. For an existing module, the same SQL goes in a migrations/<version>/post-migrate.py script instead.

Getting the module side built

When your Studio setup has outgrown its boundary, erpfly can generate the addon: models, views, access rules and tests, delivered to your repository. See Odoo module development, or Odoo customization if you’re mostly adjusting standard apps. And if you’re finding Odoo itself is the constraint, our Odoo vs custom ERP comparison is worth a read.

Sources

The official documentation and source code this page was checked against.

  1. 01 Odoo editions comparison, Odoo S.A. odoo.com
  2. 02 Odoo pricing, Odoo S.A. odoo.com
  3. 03 Models, modules and apps, Odoo 19 Studio documentation odoo.com
  4. 04 Automation rules, Odoo 19 Studio documentation odoo.com
  5. 05 Upgrade, Odoo 19 documentation odoo.com

Try it on your own module

Describe what you need. You'll see the generated code before anything touches your site.

Odoo 19

Keep reading

Create your account

Free to start. No card needed.

By signing up you agree to our terms and privacy policy.