Ad – 728×90
🔧 Backend Development

Odoo Server Actions – ir.actions.server Automation

Server actions (ir.actions.server) are configurable actions that run Python code, create records, or update fields on a set of records — all without writing a Python method directly on a model. Combined with automated actions (ir.base.automation), they enable powerful event-driven workflows: automatically setting state when a record changes, sending emails on creation, or chaining multiple actions together. This page covers both mechanisms in Odoo 19.

⏱️ 22 min 🎯 Intermediate 📅 Updated 2026

📋 What You Will Learn

  • The different server action types (state values)
  • Defining a server action in XML that runs Python code
  • The variables available inside server action code (env, record, records)
  • Binding a server action to a model's Action menu
  • Creating automated actions with ir.base.automation triggers
  • Chaining multiple server actions with state='multi'

What are Server Actions?

An ir.actions.server record encapsulates a piece of logic that can be triggered in several ways: manually from the Actions button in a list/form view, by an automated action (event-driven), by a scheduled action, or called from Python code. They are the "no-code" or "low-code" counterpart to writing full Python methods.

The key field on a server action is state, which determines what the action does:

state valueWhat it does
codeRuns a snippet of Python code
object_writeWrites specific field values on the record(s)
object_createCreates a new record of a specified model
multiRuns multiple other server actions in sequence
followersAdds followers to the record
next_activitySchedules an activity on the record

Server Action Types

The state='code' type is the most flexible — it lets you write arbitrary Python logic. The other types are more constrained but easier to configure without programming knowledge, making them useful for allowing administrators to create automations from the UI.

XML
<odoo>
    <!-- Type 1: code — runs Python snippet -->
    <record id="action_server_mark_overdue" model="ir.actions.server">
        <field name="name">Mark as Overdue</field>
        <field name="model_id" ref="model_library_loan"/>
        <field name="binding_model_id" ref="model_library_loan"/>
        <field name="state">code</field>
        <field name="code">
records.filtered(lambda l: l.state == 'active').write({'state': 'overdue'})
        </field>
    </record>

    <!-- Type 2: object_write — set specific fields -->
    <record id="action_server_reset_stock" model="ir.actions.server">
        <field name="name">Reset Stock to Zero</field>
        <field name="model_id" ref="model_library_book"/>
        <field name="binding_model_id" ref="model_library_book"/>
        <field name="state">object_write</field>
        <field name="fields_lines" eval="[(0, 0, {
            'col1': ref('library_management.field_library_book__stock_quantity'),
            'value': '0',
            'evaluation_type': 'value',
        })]"/>
    </record>
</odoo>

Creating in XML

A complete server action definition with state='code' includes the model, the code snippet, and optionally a binding to make it appear in the Actions menu:

XML
<odoo>
    <record id="action_server_send_overdue_notice" model="ir.actions.server">
        <field name="name">Send Overdue Notice Email</field>

        <!-- The model this action operates on -->
        <field name="model_id" ref="model_library_loan"/>

        <!-- Binding: show this in the Actions dropdown on library.loan -->
        <field name="binding_model_id" ref="model_library_loan"/>
        <!-- Show in both list and form views -->
        <field name="binding_view_types">list,form</field>

        <field name="state">code</field>
        <field name="code">
template = env.ref('library_management.email_template_overdue_notice', raise_if_not_found=False)
if template:
    for loan in records:
        if loan.state == 'overdue' and loan.borrower_id.email:
            template.send_mail(loan.id, force_send=True)
        </field>
    </record>
</odoo>
Ad – 336×280

Execute Python Code

Inside a state='code' server action, Odoo provides a set of pre-defined variables in the execution context:

VariableTypeDescription
envodoo.api.EnvironmentFull ORM environment — access any model with env['model.name']
modelRecordset (empty)The model class — for calling @api.model methods
recordSingle recordThe single record this action was triggered on (if applicable)
recordsRecordsetAll records selected when the action was triggered
time, datetime, dateutilModulesStandard library modules for date/time operations
WarningClassRaise Warning('message') to show a warning to the user
Python (server action code field)
# Example: complex code inside a server action's code field
# Variables available: env, record, records, model, time, datetime

# Check permissions before acting
if not env.user.has_group('library_management.group_library_librarian'):
    raise Warning("Only librarians can use this action.")

# Process selected records
returned = 0
today = datetime.date.today()
for loan in records:
    if loan.state in ('active', 'overdue'):
        loan.write({
            'state': 'returned',
            'return_date': today,
        })
        returned += 1

# Send a confirmation to the current user
if returned:
    env.user.notify_info(f"Marked {returned} loans as returned.")

Trigger on Record Create/Update

Automated actions (ir.base.automation) connect server actions to model events — they fire automatically when records are created, written, deleted, or when a date condition is met:

XML
<odoo>
    <!-- Server action that sets state to overdue -->
    <record id="action_server_auto_overdue" model="ir.actions.server">
        <field name="name">Auto: Set Loan Overdue</field>
        <field name="model_id" ref="model_library_loan"/>
        <field name="state">code</field>
        <field name="code">
records.write({'state': 'overdue'})
        </field>
    </record>

    <!-- Automated action: trigger the server action when a condition is met -->
    <record id="automation_loan_overdue" model="ir.base.automation">
        <field name="name">Library Loan: Auto Mark Overdue</field>
        <field name="model_id" ref="model_library_loan"/>

        <!-- Trigger options: on_create, on_write, on_create_or_write,
             on_unlink, on_change, based_on_timed_condition -->
        <field name="trigger">based_on_timed_condition</field>

        <!-- For timed triggers: which date field + offset -->
        <field name="date_field_name">due_date</field>
        <field name="trg_date_range">0</field>
        <field name="trg_date_range_type">day</field>

        <!-- Only trigger on loans that are currently 'active' -->
        <field name="filter_domain">[('state','=','active')]</field>

        <field name="action_server_ids" eval="[(4, ref('action_server_auto_overdue'))]"/>
        <field name="active">True</field>
    </record>

    <!-- Second example: trigger on write — when state changes to 'returned' -->
    <record id="automation_loan_returned_notify" model="ir.base.automation">
        <field name="name">Library Loan: Notify on Return</field>
        <field name="model_id" ref="model_library_loan"/>
        <field name="trigger">on_write</field>
        <field name="filter_pre_domain">[('state','!=','returned')]</field>
        <field name="filter_domain">[('state','=','returned')]</field>
        <field name="action_server_ids" eval="[(4, ref('action_server_send_overdue_notice'))]"/>
        <field name="active">True</field>
    </record>
</odoo>
TriggerWhen it fires
on_createAfter a new record is created
on_writeAfter an existing record is updated
on_create_or_writeAfter create or update
on_unlinkBefore a record is deleted
based_on_timed_conditionAt a specific date/time relative to a date field on the record

Chaining Server Actions

Use state='multi' to create a server action that runs multiple child actions in sequence. This lets you compose complex automation from simple reusable building blocks:

XML
<odoo>
    <record id="action_server_full_overdue_workflow" model="ir.actions.server">
        <field name="name">Full Overdue Workflow</field>
        <field name="model_id" ref="model_library_loan"/>
        <field name="state">multi</field>
        <!-- child_ids: the list of actions to run in sequence -->
        <field name="child_ids" eval="[
            (4, ref('action_server_mark_overdue')),
            (4, ref('action_server_send_overdue_notice')),
        ]"/>
    </record>
</odoo>

Summary

📋 Key Points

  • Server actions (ir.actions.server) encapsulate logic that can be triggered from the UI, by automations, or from code
  • state='code' runs Python with env, record, and records pre-defined
  • Bind to a model with binding_model_id to appear in the Actions dropdown
  • Automated actions (ir.base.automation) connect server actions to record lifecycle events
  • Use filter_pre_domain + filter_domain in automated actions to detect field value transitions
  • state='multi' lets you chain multiple server actions into a single callable sequence

FAQ

What is the difference between a server action and a scheduled action? +

A server action (ir.actions.server) is a piece of logic triggered either manually by a user or by an automated action event (create/write/delete/time). A scheduled action (ir.cron) is triggered automatically by the Odoo scheduler on a time interval regardless of any record event. Server actions operate on specific records; cron jobs typically search for records themselves.

Can automated actions (ir.base.automation) run without binding to a model event? +

Yes — the based_on_timed_condition trigger type doesn't require a record change event. It fires when a specified date/time field on existing records meets a configured condition (e.g. "on the due_date" or "7 days before the due_date"). Odoo's scheduler checks these conditions periodically, similar to cron jobs but scoped to individual records.

How do filter_pre_domain and filter_domain work together in on_write triggers? +

filter_pre_domain is evaluated against the record's state BEFORE the write operation (using old values). filter_domain is evaluated AFTER the write (new values). Together they detect transitions: a loan that was NOT returned before (filter_pre_domain=[('state','!=','returned')]) and IS returned after (filter_domain=[('state','=','returned')]) triggers exactly once at the moment of state change.

Is it better to use ir.base.automation or override write() in Python? +

Both approaches work. Use ir.base.automation for simple field updates, email sends, or activity creation — it keeps the logic in XML data and lets administrators see and modify it from the UI. Override write() in Python when the logic is complex, involves multiple steps, or needs to be unit-tested. For business-critical flows, Python overrides are more maintainable and debuggable.