📋 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.automationtriggers - 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 value | What it does |
|---|---|
code | Runs a snippet of Python code |
object_write | Writes specific field values on the record(s) |
object_create | Creates a new record of a specified model |
multi | Runs multiple other server actions in sequence |
followers | Adds followers to the record |
next_activity | Schedules 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.
<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:
<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>
Execute Python Code
Inside a state='code' server action, Odoo provides a set of pre-defined variables in the execution context:
| Variable | Type | Description |
|---|---|---|
env | odoo.api.Environment | Full ORM environment — access any model with env['model.name'] |
model | Recordset (empty) | The model class — for calling @api.model methods |
record | Single record | The single record this action was triggered on (if applicable) |
records | Recordset | All records selected when the action was triggered |
time, datetime, dateutil | Modules | Standard library modules for date/time operations |
Warning | Class | Raise Warning('message') to show a warning to the user |
# 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:
<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>
| Trigger | When it fires |
|---|---|
on_create | After a new record is created |
on_write | After an existing record is updated |
on_create_or_write | After create or update |
on_unlink | Before a record is deleted |
based_on_timed_condition | At 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:
<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 withenv,record, andrecordspre-defined- Bind to a model with
binding_model_idto appear in the Actions dropdown - Automated actions (
ir.base.automation) connect server actions to record lifecycle events - Use
filter_pre_domain+filter_domainin automated actions to detect field value transitions state='multi'lets you chain multiple server actions into a single callable sequence
FAQ
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.
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.
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.
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.