All posts
Developer7 min read

Odoo server actions: when to use them instead of computed fields or OWL components

Server actions execute Python code in response to a button click, a scheduled cron, or a record lifecycle event. They fill the gap between simple field computations and full custom Python modules - when used correctly.

Server actions are Odoo's mechanism for executing arbitrary Python code from a UI interaction, a cron job, or an automation rule - without writing a full module method. They sit between "this is simple enough for a computed field" and "this needs a proper Python method in a model class." Used correctly, they handle one-off business logic cleanly. Misused, they scatter business logic into database records that are impossible to test and hard to version-control.

What server actions are#

A server action is a record in ir.actions.server. It stores Python code (as a string) and a model binding. When triggered, the ORM evaluates the code in a sandboxed environment with access to:

  • env - the Odoo ORM environment.
  • model - the bound model.
  • records - the recordset the action was triggered on.
  • record - the first record (shorthand for records[0]).

Create in Settings → Technical → Actions → Server Actions.

Types of server actions#

TypeWhat it does
Python CodeExecutes arbitrary Python. Most flexible.
Create RecordCreates a new record with field values.
Update RecordSets field values on the current record.
Multi ActionsExecutes a list of other server actions in sequence.
Send EmailSends an email template.
Add FollowersSubscribes partners to the record's chatter.

For most business logic needs, "Python Code" is the right choice. The declarative types (Create Record, Update Record) are useful only when building integrations from the UI without coding.

When to use server actions#

Use server actions for:

  • One-off data migration scripts run via a manual button.
  • Business logic triggered by a button that does not belong in the model's core API.
  • Automation rules (triggered by record lifecycle events).
  • Scheduled cron jobs with simple logic.
  • Sending a contextual notification (email, SMS) in response to a specific state change.

Do not use server actions for:

  • Logic that is always applied when a field changes (use @api.onchange or _compute_).
  • Reusable business logic called from multiple places (write a model method).
  • Complex multi-step workflows (use proper Python code in the module).
  • Logic that needs unit tests (code in server action records is not testable with pytest/Jest).

Triggering from a button#

Add a button to a form view that triggers a server action:

xml
<!-- mymodule/views/sale_order_views.xml -->
<record id="view_sale_order_form_custom" model="ir.ui.view">
  <field name="model">sale.order</field>
  <field name="inherit_id" ref="sale.view_order_form"/>
  <field name="arch" type="xml">
    <xpath expr="//button[@name='action_confirm']" position="after">
      <button name="%(mymodule.action_send_internal_alert)d"
              string="Send Internal Alert"
              type="action"
              class="oe_highlight"
              attrs="{'invisible': [('state', '!=', 'sale')]}"/>
    </xpath>
  </field>
</record>

The type="action" attribute tells Odoo this button triggers a server action (not a Python method). The name is the XML ID of the action using the %(xmlid)d syntax.

Define the server action:

xml
<record id="action_send_internal_alert" model="ir.actions.server">
  <field name="name">Send Internal Alert</field>
  <field name="model_id" ref="sale.model_sale_order"/>
  <field name="binding_model_id" ref="sale.model_sale_order"/>
  <field name="state">code</field>
  <field name="code">
    for record in records:
        record.message_post(
            body=f"Order {record.name} flagged for internal review.",
            message_type='comment',
            subtype_xmlid='mail.mt_note',
        )
        record.write({'x_internal_review': True})
  </field>
</record>

Using server actions in automated actions#

Automated actions (Settings → Technical → Automation → Automated Actions) call a server action when a trigger fires. The code block in the automated action IS a server action (the fields are shared).

Example: send a Slack-style internal note when a sale order reaches a specific amount threshold:

python
# Automated action code
if record.amount_total > 50000:
    record.activity_schedule(
        'mail.mail_activity_data_todo',
        user_id=record.company_id.x_high_value_owner_id.id,
        note=f'High-value order {record.name}: {record.amount_total} {record.currency_id.symbol}',
    )

Cron jobs via server actions#

Scheduled actions (Settings → Technical → Automation → Scheduled Actions) execute server action code on a timer.

The cron server action does not receive a records recordset. Instead, query what you need:

python
# Scheduled action code  -  no 'records' variable
SaleOrder = env['sale.order']
overdue = SaleOrder.search([
    ('state', '=', 'sale'),
    ('x_due_date', '<', fields.Date.today()),
    ('x_overdue_notified', '=', False),
])
for order in overdue:
    order.message_post(
        body='This order is overdue.',
        partner_ids=order.partner_id.ids,
    )
    order.x_overdue_notified = True

Limitations and gotchas#

No transaction rollback on error: If the server action raises an exception mid-execution, Odoo may partially commit changes depending on whether the action is called within a with env.cr.savepoint() block. Wrap destructive operations:

python
with env.cr.savepoint():
    for record in records:
        record.action_risky_operation()

No version control: Server action code lives in the database, not in git. Developers edit it in the UI. This makes change tracking, code review, and rollback painful. For anything non-trivial, write a proper model method and call it from a minimal server action:

python
# In server action  -  just a dispatcher
records._my_module_process_alert()
python
# In mymodule/models/sale_order.py  -  the real logic
def _my_module_process_alert(self):
    for record in self:
        # Testable business logic here
        ...

Performance: Server actions called on large recordsets process all records in a single transaction. For bulk operations on thousands of records, use a BullMQ-style background job or a cron with batching logic.

Try ERPeek on your own Odoo module - ask questions, scaffold tests, and explore your codebase in plain language.

Get started free