Quick Answer
Stop manual daily tasks. Create scheduled actions (cron jobs) to auto-cancel unpaid orders after 48 hours, sync inventory hourly, send birthday emails at 9 AM, generate monthly reports. Manual way: someone logs in daily, filters records, clicks actions. Result: missed deadlines, human error. Automated way: Odoo runs tasks in background. Result: 100% consistency, zero manual effort. Efficient companies run 20-50 cron jobs.
The Cron Job Problem
Your D2C brand needs to:
❏ Cancel unpaid orders automatically after 48 hours
❏ Sync inventory with your 3PL every hour
❏ Send "Happy Birthday" emails every morning at 9 AM
❏ Generate monthly sales reports on the 1st of every month
The "Manual" Way
Someone logs in every day, filters records, clicks "Action," runs a script, and hopes they didn't forget anything.
Result: Missed deadlines, human error, and wasted salary.
The "Automated" Way (Cron Jobs)
You configure Odoo to run these tasks silently in the background.
Result: 100% consistency. Zero manual effort. The system works for you, not the other way around.
We've implemented 150+ Odoo systems. The most efficient companies have 20-50 cron jobs running everything from maintenance to marketing. The inefficient ones rely on humans to be robots.
What is a Scheduled Action (Cron)?
A Scheduled Action (model ir.cron) is a background task runner. It executes a specific Python method on a specific model at a defined interval.
Key Components
| Component | Description |
|---|---|
| Name | What shows in the log |
| Model | Which object logic to execute (e.g., sale.order) |
| Code | The Python code to run (e.g., model.action_cancel_expired()) |
| Interval | How often (Every 1 Day, Every 30 Minutes) |
| Next Execution Date | When to run next (in UTC!) |
Method 1: Creating via XML (Best Practice)
For custom modules, always define cron jobs in XML. This ensures they are installed automatically with your module and trackable in version control.
Scenario: Auto-cancel unpaid sales orders older than 48 hours.
Step 1: The Python Logic
from odoo import models, fields, api
from datetime import datetime, timedelta
import logging
_logger = logging.getLogger(__name__)
class SaleOrder(models.Model):
_inherit = 'sale.order'
@api.model
def action_cancel_expired_orders(self):
"""Cron Job: Cancels draft orders older than 48 hours."""
expiration_time = datetime.now() - timedelta(hours=48)
# Find orders: Draft state AND created before 48 hours ago
expired_orders = self.search([
('state', '=', 'draft'),
('create_date', '<', expiration_time)
])
if not expired_orders:
_logger.info("Cron: No expired orders found.")
return
_logger.info(f"Cron: Cancelling {len(expired_orders)} expired orders.")
for order in expired_orders:
try:
order.action_cancel()
# Log to chatter so we know WHY it was cancelled
order.message_post(body="Auto-cancelled by system (Expired).")
except Exception as e:
_logger.error(f"Failed to cancel order {order.name}: {e}")
Step 2: The XML Definition
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data noupdate="1">
<record id="ir_cron_cancel_expired_sales" model="ir.cron">
<!-- Name visible in Settings -->
<field name="name">Sales: Auto-Cancel Expired Orders</field>
<!-- Model to execute on -->
<field name="model_id" ref="sale.model_sale_order"/>
<!-- User to run as (usually OdooBot or Admin) -->
<field name="user_id" ref="base.user_root"/>
<!-- Configuration -->
<field name="state">code</field>
<field name="code">model.action_cancel_expired_orders()</field>
<field name="interval_number">1</field>
<field name="interval_type">days</field>
<field name="numbercall">-1</field> <!-- -1 means run forever -->
<field name="doall" eval="False"/>
<field name="active" eval="True"/>
</record>
</data>
</odoo>
Important Attributes
✓ noupdate="1": CRITICAL. Prevents Odoo from resetting "Next Execution Date" or re-enabling the cron if you manually disabled it every time you upgrade the module.
✓ user_id: Typically base.user_root (System) or base.user_admin. This determines access rights. If the user doesn't have permission to cancel orders, the cron will fail.
✓ doall=False (Recommended): If the server was down and missed 5 executions, run only once when it comes back up.
⚠️ doall=True: Run all 5 missed executions back-to-back (dangerous for heavy tasks).
Method 2: Creating via UI (Quick Fixes)
Sometimes you need a temporary job or don't have developer access.
1. Enable Developer Mode
2. Go to Settings → Technical → Automation → Scheduled Actions
3. Click Create
4. Model: Select Sale Order
5. Execute Python Code: Write Python directly
6. Set Interval and Save
# You can write python directly here!
env['sale.order'].action_cancel_expired_orders()
Handling Timezones (The #1 "Gotcha")
Odoo stores all datetimes in UTC.
⚠️ Timezone Issue:
If you set the "Next Execution Date" to 2023-12-20 09:00:00 in the UI, and your user is in EST (UTC-5), Odoo interprets that as 09:00 YOUR TIME. However, in the database, it stores 14:00:00 UTC.
✓ Best Practice: When setting a specific time (e.g., "Run at 2 AM"), always check the "Next Execution Date" field in the UI. It usually displays in your local user timezone.
Troubleshooting & Debugging
Problem: "My Cron didn't run!"
❏ Check active: Is the checkbox ticked?
❏ Check Next Execution Date: Is it in the future? If it's in the past, the scheduler might be stuck.
❏ Check the Logs: Odoo logs cron execution. Look for odoo.addons.base.models.ir_cron
❏ Trigger Manually: Click the "Run Manually" button in the Scheduled Action form. If it errors there, it's a code issue.
Problem: "Transaction RollbackError"
If your cron processes 1,000 records and record #999 fails, Odoo rolls back everything.
Fix: Use explicit commits (sparingly) or handle exceptions per record.
def action_process_queue(self):
for item in self.search([]):
try:
with self.env.cr.savepoint():
item.do_processing()
except Exception as e:
_logger.error(f"Failed item {item.id}: {e}")
# The loop continues! Other items are saved.
Performance: Don't Kill Your Server
Scenario: You have a cron running every 1 minute to sync inventory. It takes 45 seconds to run.
⚠️ Risk:
If the data volume spikes and it takes 65 seconds, the next cron triggers before the first one finishes. You now have two overlapping jobs fighting for database locks.
Result: CPU spike, database lock waits, system slow-down.
Protection Strategies
✓ Avoid Overlap: Odoo crons generally lock the row in ir_cron so the same job shouldn't overlap itself, but be careful with shared resources.
✓ Batching: Don't process all records. Process a batch. If you have more work, the next run will pick them up.
# Process only 100 at a time
records = self.search([('state', '=', 'pending')], limit=100)
# If you have more work, the next run will pick them up.
Cron Interval Options
| Interval Type | Example | Use Case |
|---|---|---|
| minutes | Every 30 minutes | High-frequency inventory sync |
| hours | Every 1 hour | Sync with 3PL systems |
| days | Every 1 day (daily) | Cancel expired orders, send birthday emails |
| weeks | Every 1 week (weekly) | Weekly cleanup tasks |
| months | Every 1 month (monthly) | Generate monthly sales reports |
Action Items: Implement Cron Jobs
Audit Your Routine Tasks
❏ List every manual "check" or "cleanup" task you do weekly
❏ Identify which can be automated with logic
Implement
❏ Write the Python method (with logging!)
❏ Create the XML record with noupdate="1"
❏ Set a sensible interval (don't run every minute if daily is fine)
Test
❏ Set the interval to 1 minute temporarily
❏ Watch the logs
❏ Verify the data changed correctly
❏ Reset interval to production schedule
Frequently Asked Questions
Should I create cron jobs via XML or the UI?
Use XML for custom modules (best practice, version control, automatic installation). Use UI for temporary jobs or when you don't have developer access. Always use noupdate="1" in XML to prevent Odoo from resetting settings on module upgrades.
Why isn't my cron job running?
Check: (1) active checkbox is ticked, (2) Next Execution Date is in the future, (3) Logs for errors (odoo.addons.base.models.ir_cron), (4) Click "Run Manually" to test for code issues, (5) User permissions (user_id must have access to execute the method).
How do I handle timezone issues in Odoo cron jobs?
Odoo stores all datetimes in UTC. When setting "Next Execution Date" in the UI, it displays in your local timezone but stores in UTC. Always check the field value to verify the actual execution time. For "Run at 2 AM" scenarios, verify the UTC conversion.
What's the difference between doall=True and doall=False?
doall=False (Recommended): If server was down and missed 5 executions, run only once when it comes back up. doall=True: Run all 5 missed executions back-to-back (dangerous for heavy tasks, can cause server overload).
Free Automation Audit
Stop running your business manually. We'll identify the top 5 manual processes draining your team's time, architect the cron logic to automate them, review your existing crons for performance risks (overlap, locking), and show you how to monitor job health. Automation is the compounding interest of productivity.
