Scheduled Actions (ir.cron) execute server-side Python code at certain intervals of time. Any errors raised by scheduled actions are managed within Odoo 19 by a failure tracking mechanism in the ir_cron.py file. Learning about how the system works can help you make your cron jobs more reliable.
Cron Job Failure Handling Mechanism in Odoo 19
However, before we explain the methods, here’s what Odoo 19 does behind the scenes by default:
- Every cron action has a failure_count and first_failure_date field on ir.cron
- On each exception, the _callback() function does a rollback on the transaction and raises the exception again. The cron action status is updated to "failed".
- If there are five consecutive failures over a span of seven days, then the cron action will be deactivated automatically, and the _notify_admin() function will be called to notify the administrator.
- Any successful execution will reset the failure_count value to 0.
- With this, it should be easy for you to handle cron actions with greater confidence.
Method 1: Try/Except in the Cron Code Body
The easiest way is to put your code in a try/except block. This allows you to catch certain exceptions and deal with them appropriately (log, notify, skip), all while preventing the job from failing completely.
from odoo import models, api
import logging
_logger = logging.getLogger(__name__)
class SaleOrder(models.Model):
_inherit = 'sale.order'
@api.model
def action_sync_orders(self):
orders = self.search([('state', '=', 'draft')])
for order in orders:
try:
order._sync_to_external_system()
except ConnectionError as e:
_logger.warning(
"Sync failed for order %s: %s. Skipping.",
order.name, e
)
continue
except Exception as e:
_logger.error(
"Unexpected error for order %s: %s",
order.name, e, exc_info=True
)
raise # re-raise to mark the job as failed
When to use this:
To ignore individual failures but avoid failing the whole task execution process.
To be able to discern recoverable from unrecoverable exceptions. Raising the exception again ensures the failure counter is updated by Odoo’s failure counting system.
Method 2: Override _notify_admin for Custom Alerts
Odoo 19 calls the method _notify_admin() of ir.cron when a task fails several times and gets deactivated. The default version only issues a log entry. Overriding this method will enable sending an email notification, posting a chat message, or invoking an external API URL endpoint.
from odoo import models
import logging
_logger = logging.getLogger(__name__)
class IrCron(models.Model):
_inherit = 'ir.cron'
def _notify_admin(self, message):
mail_template = self.env.ref(
'your_module.cron_failure_email_template',
raise_if_not_found=False
)
if mail_template:
# Use sudo() and pass no record_id (send a generic notification)
# OR find the cron by matching its active=False state
cron = self.env['ir.cron'].sudo().search(
[('active', '=', False)], order='write_date desc', limit=1
)
if cron:
mail_template.send_mail(cron.id, force_send=True) # ? valid id
# Always keep the default log
super()._notify_admin(message)
Xml
<record id="cron_failure_email_template" model="mail.template">
<field name="name">Cron Job Failure Alert</field>
<field name="model_id" ref="base.model_ir_cron"/>
<field name="subject">Scheduled Action Failed: {{ object.cron_name }}</field>
<field name="body_html">
<p>The scheduled action <strong>{{ object.cron_name }}</strong>
has been deactivated after repeated failures.
Please check the server logs for details.</p>
</field>
<field name="email_to">admin@yourcompany.com</field>
</record>
When to use this:
You need proactive alerts before someone notices the cron is off.
Your production system must notify a DevOps team, Slack channel, or support desk automatically.
Method 3: Check failure_count and first_failure_date for Conditional Logic
The Odoo 19 version contains the failure data within the field of the ir.cron model. These fields can be used within your cron function to adapt depending on the number of failures.
from odoo import models, api, fields
import logging
_logger = logging.getLogger(__name__)
class StockPicking(models.Model):
_inherit = 'stock.picking'
@api.model
def action_retry_failed_pickings(self):
cron = self.env['ir.cron'].sudo().search(
[('code', 'like', 'action_retry_failed_pickings')], limit=1
)
if cron and cron.failure_count >= 3:
_logger.warning(
"Cron '%s' has failed %s times since %s. "
"Switching to safe mode (read-only check only).",
cron.cron_name,
cron.failure_count,
cron.first_failure_date,
)
# Safe mode: only report the problem, don't attempt writes
failed = self.search([('state', '=', 'confirmed'), ('scheduled_date', '<', fields.Datetime.now())])
_logger.warning("Found %s overdue pickings during safe mode run.", len(failed))
return
# Normal mode: attempt processing
failed_pickings = self.search([
('state', '=', 'confirmed'),
('scheduled_date', '<', fields.Datetime.now()),
])
for picking in failed_pickings:
try:
picking.action_assign()
except Exception as e:
_logger.error("Error assigning picking %s: %s", picking.name, e, exc_info=True)
raise
When to use this:
Your task has negative consequences (writing, emailing, API requests) that you would like to avoid during a series of failures. You want a "safe mode" where diagnostics are collected without further damage. You need a custom escalation depending on how long your task has failed.
Odoo 19 comes with built-in failure logging for tasks, recovery logging when your tasks fail to recover, and deactivating tasks that keep failing. Using custom error handling, notifications to administrators, and failure-aware programming, you can turn scheduled actions into production-ready ones.
To read more about How to Configure Scheduled Actions in Odoo 19, refer to our blog How to Configure Scheduled Actions in Odoo 19.