What Are Automated Actions?
Automated actions (base.automation) trigger Python code when records are created, updated, deleted, or on a time condition. They combine the power of server actions with event-based triggers — no custom module needed. Configure them at Settings > Technical > Automated Actions.
Trigger Types
| Trigger | When It Fires |
|---|---|
| On Creation | When a record is created |
| On Update | When specified fields change |
| On Creation & Update | Both events |
| On Deletion | When a record is deleted |
| Based on Time Condition | Scheduled, relative to a date field |
Recipe 1: Auto-Assign Leads by Country
Trigger: On Creation | Model: crm.lead
country = record.country_id.code
sales_team_map = {
'US': 'North America',
'CA': 'North America',
'GB': 'Europe',
'DE': 'Europe',
'FR': 'Europe',
}
team_name = sales_team_map.get(country, 'General')
team = env['crm.team'].search([('name', '=', team_name)], limit=1)
if team:
record.write({'team_id': team.id})Recipe 2: Auto-Archive Stale Quotations
Trigger: Based on Time Condition (60 days after validity_date) | Model: sale.order | Filter: state = draft
record.write({'active': False})
record.message_post(body='Quotation auto-archived: expired over 60 days ago.')Recipe 3: Escalate Overdue Tasks
Trigger: Based on Time Condition (1 day after date_deadline) | Model: project.task | Filter: stage is not Done
manager = record.project_id.user_id
if manager:
record.activity_schedule(
'mail.mail_activity_data_todo',
user_id=manager.id,
date_deadline=datetime.date.today(),
summary=f'OVERDUE: {record.name}',
note=f'Task is overdue. Original deadline: {record.date_deadline}',
)Recipe 4: Auto-Subscribe Project Manager
Trigger: On Creation | Model: project.task
if record.project_id.user_id:
record.message_subscribe(
partner_ids=[record.project_id.user_id.partner_id.id]
)Recipe 5: Notify Sales Manager on Large Orders
Trigger: On Update (amount_total field) | Model: sale.order
if record.amount_total > 50000:
managers = env['res.users'].search([
('groups_id', 'in', env.ref('sales_team.group_sale_manager').id)
])
record.message_post(
body=f'High-value order: {record.name} = {record.amount_total:,.2f}',
partner_ids=managers.mapped('partner_id').ids,
subtype_xmlid='mail.mt_comment',
)Recipe 6: Auto-Set Priority on VIP Customers
Trigger: On Creation | Model: sale.order
vip_tag = env['res.partner.category'].search([('name', '=', 'VIP')], limit=1)
if vip_tag and vip_tag in record.partner_id.category_id:
record.write({'priority': '3'})Recipe 7: Block Negative Invoice Lines
Trigger: On Creation & Update | Model: account.move.line
if record.move_id.move_type == 'out_invoice' and record.price_unit < 0:
raise UserError('Negative prices are not allowed on customer invoices. Use a credit note instead.')Recipe 8: Auto-Confirm Purchase Orders Under Threshold
Trigger: On Creation | Model: purchase.order
if record.amount_total < 500:
record.button_confirm()
record.message_post(body='Auto-confirmed: order under 500 threshold.')Recipe 9: Sync Customer Phone to Delivery Address
Trigger: On Update (phone field) | Model: res.partner
if record.phone:
children = env['res.partner'].search([
('parent_id', '=', record.id),
('type', '=', 'delivery')
])
children.write({'phone': record.phone})Recipe 10: Welcome Email on Customer Creation
Trigger: On Creation | Model: res.partner | Filter: customer_rank > 0
template = env.ref('your_module.welcome_email_template', raise_if_not_found=False)
if template and record.email:
template.send_mail(record.id, force_send=True)
log(f'Welcome email sent to {record.email}')Recipe 11: Auto-Tag Products by Price Range
Trigger: On Creation & Update (list_price field) | Model: product.template
premium_tag = env['product.tag'].search([('name', '=', 'Premium')], limit=1)
budget_tag = env['product.tag'].search([('name', '=', 'Budget')], limit=1)
if record.list_price > 1000 and premium_tag:
record.write({'product_tag_ids': [(4, premium_tag.id)]})
elif record.list_price < 50 and budget_tag:
record.write({'product_tag_ids': [(4, budget_tag.id)]})Recipe 12: Notify Warehouse on Low Stock
Trigger: On Update (quantity field) | Model: stock.quant
if record.quantity < record.product_id.reordering_min_qty:
warehouse_users = env['res.users'].search([
('groups_id', 'in', env.ref('stock.group_stock_manager').id)
])
record.product_id.message_post(
body=f'Low stock alert: {record.product_id.name} has {record.quantity} units at {record.location_id.name}.',
partner_ids=warehouse_users.mapped('partner_id').ids,
)Recipe 13: Auto-Close Old Helpdesk Tickets
Trigger: Based on Time Condition (30 days after write_date) | Model: helpdesk.ticket | Filter: stage is Waiting for Customer
closed_stage = env['helpdesk.stage'].search([('name', '=', 'Closed')], limit=1)
if closed_stage:
record.write({'stage_id': closed_stage.id})
record.message_post(body='Ticket auto-closed after 30 days without customer response.')Recipe 14: Validate Email Format
Trigger: On Creation & Update (email field) | Model: res.partner
import re
if record.email:
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if not re.match(pattern, record.email.strip()):
raise UserError(f'Invalid email format: {record.email}')Recipe 15: Auto-Create Calendar Event for Meetings
Trigger: On Creation | Model: crm.lead | Filter: type = opportunity
if record.partner_id and record.user_id:
env['calendar.event'].create({
'name': f'Intro call: {record.partner_id.name}',
'start': datetime.datetime.now() + datetime.timedelta(days=2),
'stop': datetime.datetime.now() + datetime.timedelta(days=2, hours=1),
'partner_ids': [(4, record.partner_id.id), (4, record.user_id.partner_id.id)],
'res_model': 'crm.lead',
'res_id': record.id,
})Tips for Reliable Automated Actions
- Always test with a single record first
- Use domain filters to narrow which records trigger the action
- Add
log()statements for debugging - Time-based actions run via cron — check cron is active
- Avoid infinite loops: do not update a field that triggers the same action
- Use
raise UserError()to block invalid operations