Odoo's scheduled actions — its internal cron system — run tasks automatically on a schedule: sending payment reminders, updating stock forecasts, generating reports, syncing data with external systems. Getting them right means your Odoo keeps itself maintained without manual intervention. Getting them wrong means silent failures and stale data.
How Odoo's Scheduler Works
Odoo does not use the operating system's cron daemon. It has its own scheduler implemented in Python, running inside dedicated worker processes (configured by max_cron_threads in odoo.conf).
The scheduler wakes up periodically (by default every minute) and checks the database for scheduled actions that are due to run. It then executes them in dedicated cron worker processes, separate from the HTTP workers that handle web requests.
This architecture means:
- Cron jobs can run even when no users are active
- Cron jobs don't compete with HTTP workers for resources (unless you set
max_cron_threads = 0) - All cron job configuration is in the database, not on the filesystem
Viewing Built-In Scheduled Actions
To see all scheduled actions, you need Developer Mode enabled. Go to:
Settings → Technical → Automation → Scheduled Actions
Common built-in actions include:
- Send payment reminders — overdue invoice follow-ups
- Fetch emails — IMAP/POP3 polling Update reorder rules — inventory replenishment
- Generate recurring entries — accounting automation
- Clean old sessions — maintenance
Configuring max_cron_threads
In your odoo.conf:
[options]
workers = 4 ; HTTP workers
max_cron_threads = 1 ; dedicated cron workers
Key rules:
- Set
max_cron_threads = 0to disable the scheduler entirely (rarely appropriate) - For most deployments,
max_cron_threads = 1is sufficient - Increase to
2if you have many heavy cron jobs that need to run concurrently - Cron workers count against your total memory budget — each uses the same memory limits as HTTP workers
With workers = 0 (single-threaded mode), cron jobs still run, handled by a background thread in the main process. This is not suitable for production.
Creating a Custom Scheduled Action
Step 1: Enable Developer Mode
Scheduled actions require Developer Mode. See our guide on enabling Odoo Developer Mode.
Step 2: Create the Action
- Go to Settings → Technical → Automation → Scheduled Actions
- Click New
- Fill in the form:
Name: Sync External Inventory
Model Name: product.product (the model this action operates on)
Execute Every: 1 Hour
Next Execution Date: [set to when you want first run]
Number of Calls: -1 (unlimited, runs forever)
Code: (see below)
Step 3: Write the Action Code
The "Code" field contains Python code that runs with env available as the Odoo environment:
# Simple example: log all expired subscriptions
records = env['product.product'].search([
('active', '=', True),
('type', '=', 'service'),
])
for product in records:
if product.list_price == 0:
product.message_post(body="Zero-price service product detected")
For complex logic, define a method on the model and call it from the scheduled action:
# In your custom module's model:
class ProductProduct(models.Model):
_inherit = 'product.product'
def _cron_sync_inventory(self):
products = self.search([('active', '=', True)])
for product in products:
# your sync logic here
pass
# In the scheduled action's Code field:
env['product.product']._cron_sync_inventory()
This is the recommended pattern — keeps the business logic testable and in the module where it belongs.
Interval Settings Explained
| Field | Description | Example |
|---|---|---|
| Execute Every | How often the action runs | 1 Hour, 30 Minutes, 1 Day |
| Next Execution Date | When the next run is scheduled | Auto-updated after each run |
| Number of Calls | -1 = unlimited, N = run N more times | Set to 1 for one-time actions |
| Priority | Lower number = higher priority when multiple actions are due | Default: 5 |
Odoo does not guarantee exact timing — if a cron worker is busy when the scheduled time arrives, the action runs as soon as a worker is available. For time-sensitive operations, reduce max_cron_threads latency by increasing the worker count or reducing the interval.
Debugging Failed Cron Jobs
Check the Scheduled Action Log
After each run, Odoo records whether it succeeded or failed. On the scheduled action form, look for a log field or check the action's last execution status. In older Odoo versions, errors are logged to the Odoo log file.
# Filter Odoo logs for cron errors
grep -i "cron\|scheduler\|ir.cron" /var/log/odoo/odoo.log | grep -i "error\|exception"
# Docker
docker compose logs odoo | grep -i "ir.cron" | grep -i "error"
Common Cron Problems and Fixes
| Problem | Cause | Fix |
|---|---|---|
| Action never runs | max_cron_threads = 0, or workers = 0 with threading disabled | Set max_cron_threads = 1 in odoo.conf, restart |
| Action runs but does nothing | Domain filter returns no records | Test the search domain in the Odoo shell |
| Action fails with AccessError | Cron runs as low-privilege user | Use env(user=SUPERUSER_ID) in the action code |
| Action takes too long and times out | limit_time_real_cron too low | Increase limit_time_real_cron in odoo.conf |
| Action runs too infrequently | Interval set too long | Edit the scheduled action interval setting |
Manually Triggering a Cron Job
To test a scheduled action without waiting for the interval:
- Open the scheduled action in Settings → Technical → Automation → Scheduled Actions
- Click Run Manually (the button may be labeled "Execute Manually" in some versions)
- Check logs immediately after
# Or run it from the Odoo shell:
docker compose exec odoo odoo shell -d your_db
# Then in the shell:
env['ir.cron'].browse(ACTION_ID).method_direct_trigger()
How DeployMonkey Monitors Cron Jobs
DeployMonkey monitors cron worker health as part of instance monitoring. If cron workers stop processing (a common symptom of memory exhaustion or deadlocks), our monitoring detects the stall and alerts you — and can automatically restart the affected workers.
Cron job logs are available directly in the DeployMonkey control panel, so you don't need SSH access to debug a failing scheduled action.
Start a free instance with built-in cron monitoring included.
Frequently Asked Questions
Can I run Odoo cron jobs on a schedule defined outside Odoo (like system cron)?
Yes, using the Odoo shell: odoo shell -d mydb --no-http then call the model method. You can wrap this in a bash script and trigger it from system cron. This gives you more precise scheduling control but bypasses Odoo's built-in scheduler management.
Why do my cron jobs stop running after a few hours?
Usually caused by a cron worker crash due to memory exhaustion. The worker process dies but the main Odoo process does not restart it automatically. Check your memory limits and consider reducing max_cron_threads if your cron jobs are memory-intensive.
Can cron jobs run concurrently?
Yes, if max_cron_threads is greater than 1. Multiple cron workers can each run a different scheduled action simultaneously. However, a single scheduled action only runs in one worker at a time — Odoo prevents the same action from running concurrently with itself.
How do I pass parameters to a scheduled action?
Scheduled actions don't natively support parameters. The standard pattern is to store configuration in ir.config_parameter (system parameters), read them in your cron method, or use model-level settings. If you need parameterized scheduling, create multiple scheduled actions calling the same method with different hardcoded arguments.