Most of the broken automation I get called in to fix is not really an automation problem. The Odoo 19 automation rules fire exactly as designed. The wrong action type was asked to do the work, so a developer writes forty lines of Python for something an Update Record action handles in one field. Odoo 19 ships a small, well defined set of action models, and knowing which one owns which job is the difference between automation you can hand over and automation you babysit.
It is the most common review finding in my Odoo performance optimization work for Australian clients. Teams reach for an Execute Code server action by reflex, then discover two upgrades later that nobody remembers what the code does. Every rule you write lands on the same ir.actions.server model, so here is what the server action state field offers and when each type earns its place.
The Six Action Models Odoo 19 Actually Ships
Every action in Odoo is a record the web client receives as a dictionary and reacts to based on its type. Six models turn up in practice, and only one is really about automation.
- ir.actions.act_window opens a view on a model, with res_model, view_mode, domain and context doing the real work behind almost every menu item.
- ir.actions.act_url sends the browser to a URL, useful for portal redirects and handoffs.
- ir.actions.report renders a document through a QWeb report template.
- ir.actions.client mounts a registered client side component, which is how dashboards and custom OWL screens appear.
- ir.actions.server executes server side logic. This is the automation workhorse.
- ir.cron is the scheduled action, and in Odoo 19 it delegates to a server action rather than carrying its own code.
The first four types describe what the user should see next, while server and scheduled actions describe what the system should do. When a requirement starts with “and then show the user”, you probably need a window action returned from your logic, not more logic.
Server Action Types You Configure Every Day
The state field on ir.actions.server decides what kind of server action you get. Odoo 19 core offers Update Record, Create Record, Duplicate Record, Execute Code, Send Webhook Notification and Multi Actions. Discuss extends that server action state field with Send Email, Add Followers, Remove Followers and Create Activity, and the SMS and WhatsApp apps add messaging states on top.
Update Record, Create Record, and Duplicate Record
The Update Record action is more capable than most developers assume. Its update_path field accepts a dotted path such as partner_id.name, so you can write to a related record without a single line of Python. The evaluation_type field then decides how the value is produced: a static value, the next number from a sequence, or a computed Python expression. That last option covers most of the small calculations people write full code actions for.
Create Record builds a new record on any model, which makes it the clean way to spawn a follow up task when a deal closes. Duplicate Record copies a record and suits workflows needing a snapshot rather than a reference. Both are declarative, both are visible in the interface, and both survive a handover to a functional consultant.
Execute Code, Multi Actions, and Send Webhook Notification
Execute Code is the escape hatch and deserves to be treated as one. Your snippet runs with env, model, record and records already in scope, so you work directly against the ORM.
If you are going to live in this action type, it pays to understand how the Odoo 19 ORM works behind the scenes, because a careless write inside a loop shows up as a slow database long before it shows up as an error.
Multi Actions chains several server actions through child_ids and runs them in sequence order, which keeps each action small and readable instead of one monolithic code block. Send Webhook Notification posts a JSON payload to an external URL, with the record id and model always included as _id and _model plus any extra fields you select in webhook_field_ids . For integrations that only need a nudge, it removes a custom controller entirely.
Wiring Server Actions Into Automation Rules
A server action does nothing until something calls it. The base.automation model provides the trigger, and Odoo 19 groups triggers into families: value based ones such as Stage is set to and Tag is added, lifecycle ones including on_create , on_create_or_write , on_unlink and on_change for interface edits before a save, timing ones tied to a date field or to record creation, message triggers for incoming and outgoing mail, and on_webhook for external systems.
Two habits save real debugging time here. First, always set trigger_field_ids when you use on_create_or_write , so the rule only wakes up for the fields you care about instead of every write on the model. Second, be deliberate about on_change , because a UI change trigger runs against an in memory record that has not been saved, so nothing there can assume a database identity.
Declaring Actions in XML
Configuring automation through the interface is fine for a one off, but anything that matters belongs in a module so it travels through your environments with the code. In Odoo 19 the automation rule owns the trigger and the server action points back at it through base_automation_id .
<record id="automation_flag_high_value_order" model="base.automation">
<field name="name">Flag high value sales orders</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="trigger">on_create_or_write</field>
<field name="trigger_field_ids"
eval="[(4, ref('sale.field_sale_order__amount_total'))]"/>
</record>
<record id="action_flag_high_value_order" model="ir.actions.server">
<field name="name">Flag High Value Order</field>
<field name="model_id" ref="sale.model_sale_order"/>
<field name="base_automation_id" ref="automation_flag_high_value_order"/>
<field name="state">code</field>
<field name="code">
for order in records:
if order.amount_total > 50000 and order.priority != '1':
order.priority = '1'
order.message_post(body="Flagged for finance review.")
</field>
</record>Notice the guard on priority inside the loop. Without it, a rule triggered by a write re-enters itself when the action writes back to the same record. Notice too that the rule watches amount_total specifically, so edits to a delivery address never wake it up.
Mistakes That Cost Me Debugging Hours
- Reaching for Execute Code first. If the requirement is “set this field to that value”, an Update Record action with an
update_pathis faster to write, read and hand over. - Forgetting the binding fields. Setting
binding_model_idandbinding_typeputs a server action in the cog menu, which is often all a user needed instead of a new button and view. - Ignoring recursion. A rule that writes to the model it watches will call itself unless you narrow the trigger fields or guard the write with a condition.
- Treating scheduled actions as separate. An
ir.crondelegates to a server action, so the same Multi Actions and Execute Code patterns and tests apply. - Skipping the payload check. Read the sample payload Odoo generates for a webhook action and confirm the receiving system expects those keys.
If you are staring at a stack of automation rules nobody wants to touch, or want the action types settled before the first line of Python, that is the kind of review I do. Book a Consultation and we can work out what should be declarative, what genuinely needs code, and how to leave your team an automation layer they can maintain.
Conclusion
Action types in Odoo 19 are the contract between your business logic and the framework, and choosing well is what keeps automation understandable a year later. Let the window, URL, report and client actions handle what the user sees. Let server actions carry the work, and inside those use the declarative types until the requirement outgrows them. Wire them to automation rules with narrow triggers, declare them in XML, and reserve Execute Code for the problems that have earned it.
Frequently Asked Questions
What is the difference between a server action and an automation rule in Odoo 19?
The server action is the work and the automation rule is the trigger. An ir.actions.server record defines what happens, a base.automation record defines when. They stay separate because a server action can also be run manually from a contextual action or by a scheduled action.
When should I use Execute Code instead of Update Record?
Use Update Record whenever the requirement is setting field values, including related fields through update_path and computed values through the equation evaluation_type. Move to an Execute Code server action only for branching logic, multi model work, or method calls the declarative types cannot reach.
How do I stop an automation rule from triggering itself?
Narrow the rule with trigger_field_ids so it only reacts to the fields that matter, and guard the write so it does nothing when the value is already correct. Those two habits prevent almost every recursion loop I see on the on_create_or_write trigger.
Can a server action send data to an external system without custom code?
Yes. The Send Webhook Notification state posts a JSON payload to the URL you configure, always including the record id and model plus whatever fields you select. For one way notifications this replaces a custom controller, though two way integrations still need proper API work.
Do scheduled actions use the same action types?
They do. In Odoo 19 the ir.cron record delegates to a server action, so a scheduled job can use Execute Code, Multi Actions or any other state. The benefit is that you can test the logic by running the server action manually before trusting it to a schedule.