Skip to content

15 Odoo Automated Action Recipes for Business Automation

DeployMonkey Team · March 23, 2026 13 min read

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

TriggerWhen It Fires
On CreationWhen a record is created
On UpdateWhen specified fields change
On Creation & UpdateBoth events
On DeletionWhen a record is deleted
Based on Time ConditionScheduled, 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