All posts
Integrator11 min read

Odoo automated actions and server actions: configuring workflow automation without code

Automated actions let you trigger business logic on record events - creation, field change, time condition - without writing a custom module. Server actions let you execute Python, send emails, update fields, or chain flows. This guide covers both, including the gotchas that break them silently.

Two Tools, One Goal#

Odoo provides two configuration-based automation tools:

  • Automated Actions (Settings → Technical → Automation → Automated Actions): trigger a server action when a record event occurs - creation, update, deletion, or a time-based condition.
  • Server Actions (Settings → Technical → Actions → Server Actions): the action that runs - update fields, execute Python, send an email, create records, or chain other actions.

They work together: an Automated Action fires a Server Action. You can also call Server Actions directly from buttons in views.


Automated Actions: Triggers and Conditions#

Trigger Types#

TriggerWhen it fires
On creationA new record is created
On updateA record is written (any field)
On creation & updateEither create or write
On deletionA record is unlinked
Based on a timed conditionCron-like: runs on records where a date field condition is met

Filtering with a Domain#

Every automated action has a Before Update Filter and an optional Filter (domain) that restricts which records qualify. If the filter is empty, the action fires for all records matching the trigger.

Domains use Odoo's standard domain notation:

  • [('state', '=', 'sale')] - only confirmed sale orders
  • [('amount_total', '>', 10000)] - only orders above 10,000
  • [('partner_id.country_id.code', '=', 'CA')] - Canadian partners

Before Update Filter#

For "On update" triggers, the Before Update Filter specifies what the record looked like before the write. This lets you detect transitions:

  • Before: [('state', '=', 'draft')]
  • After filter: [('state', '=', 'sale')]

Together these mean "fire when a record moves from draft to confirmed". Without a before-update filter, the action fires on every write where the record ends up in the confirmed state - including re-saves of already-confirmed orders.

Timed Condition#

For time-based triggers:

  • Select a Date Field (e.g., date_deadline)
  • Set an Offset (e.g., -3 days = 3 days before)
  • The cron picks up records where the condition is now met

Important: The timed cron only runs records that have not been processed yet. Once a record is processed, it is not re-processed on the next cron run unless the date field changes.


Server Actions: Action Types#

When you create a Server Action, choose one of these types:

Update a Record#

Sets field values on the target records. No Python required. Use this for:

  • Setting a field to a fixed value ("set state to done")
  • Setting a field from another field on the same record ("copy name to display_name")
  • Clearing a field

Execute Python Code#

Full Python execution with access to env, model, records, time, datetime, dateutil, timezone, Warning.

python
for record in records:
    if record.amount_total > 10000:
        record.message_post(
            body="Large order flagged for review.",
            subtype_xmlid="mail.mt_note",
        )
        record.partner_id.write({'category_id': [(4, env.ref('sale.res_partner_category_0').id)]})

Send an Email#

Choose a QWeb email template. The template is rendered for each matching record and sent to the recipients configured in the template.

Create a New Activity#

Schedule an activity (e.g., a phone call reminder) on the matching record or a related record.

Create Records#

Create one or more records of a target model, with field values populated from the triggering record's data.

Execute several actions#

Chain multiple server actions in sequence. Use this to compose complex flows from reusable building blocks.


Common Integration Patterns#

Pattern 1: Auto-assign territory on customer creation#

Trigger: On creation, model res.partner

Filter: [('customer_rank', '>', 0)]

Action type: Execute Python Code

python
for partner in records:
    if partner.country_id.code == 'CA':
        partner.user_id = env.ref('base.user_canada_rep')
    elif partner.country_id.code == 'US':
        partner.user_id = env.ref('base.user_us_rep')

Pattern 2: Notify on overdue invoices#

Trigger: Timed condition

Date field: invoice_date_due

Offset: 0 (on the due date itself)

Filter: [('state', '=', 'posted'), ('payment_state', '!=', 'paid')]

Action: Send an email using the overdue invoice template

Pattern 3: Lock a record after approval#

Trigger: On update

Before update filter: [('state', '!=', 'approved')]

After filter: [('state', '=', 'approved')]

Action type: Update a record → set is_locked = True


Gotchas That Break Automated Actions Silently#

Gotcha 1: The domain uses stored fields only#

Domains filter against the database. If you write a domain on a non-stored computed field, the filter silently fails - it returns no records (because non-stored fields don't exist as columns).

Fix: Ensure all domain fields are either native columns or stored computed fields (store=True).

Gotcha 2: Before update filter is ignored for new records#

If the trigger is "On creation", the before-update filter has no meaning and is ignored. Odoo does not raise an error - the filter is just discarded.

Gotcha 3: Python code runs as the triggering user#

The Python code block runs in the context of the user who triggered the write. If that user is a portal user or external API user, their access rights apply. Add sudo() when you need to write to records the triggering user cannot see.

Gotcha 4: Timed actions can pile up without a "processed" flag#

If the timed condition triggers an email but the record's date field never changes, each cron run will re-trigger the action. Add a boolean field (notification_sent) and include it in the filter: [('notification_sent', '=', False)]. Set it in the server action after sending.

Gotcha 5: Errors in Python code are swallowed in production#

If the Python code raises an exception, the automated action logs the error but does NOT roll back the triggering write. The record write succeeds; the automation silently fails. Check Settings → Technical → Automation → Automated Actions → Logs to see failure records.


Testing Automated Actions#

  1. Enable developer mode (Settings → Activate the developer mode).
  2. Open the automated action and click Run Manually (available in developer mode).
  3. Enter a domain to pick which records to test on.
  4. Check the action ran correctly, then check the Logs tab.

For timed actions, set the date field on a test record to today's date and use Run Manually to trigger it without waiting for the next cron run.


When Automated Actions Are Not Enough#

Automated actions are powerful but have limits:

  • No cross-model transactions - one action operates on records of one model at a time
  • No async execution - everything runs synchronously in the triggering transaction
  • No conditional branching within a single Python block based on runtime state (you can simulate this with if/else in Python, but there is no flowchart-style conditional in the UI)
  • No error handling - a Python error in the automation does not retry

When you need retry logic, async processing, cross-model orchestration, or complex branching, the right tool is a custom module with BullMQ-style queue workers or Odoo's scheduled actions (ir.cron).


ERPeek can answer "which automated actions are defined on this model?" or "what does this server action's Python code actually do?" across any installed Odoo version and module set. See the contact page for a demo.

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

Get started free