Odoo Development · intermediate · 10 min read
Scheduled Actions and Cron Jobs
Designing reliable Odoo cron jobs: ir.cron configuration, idempotency, batching, failure handling, and alternatives to infinite server actions.
Last reviewed 2026-07-30 · Estimated effort: 2–6 weeks typical implementation slice
Table of contents
Scheduled actions (ir.cron) call a model method on an interval — minutes, hours, or days — using a specified user and optional priority. They run inside Odoo workers alongside HTTP and longpolling.
Cron is for modest batch work tied to Odoo data: mail queues, session vacuum, incremental sync markers. Heavy ETL or minute-level external API polling often belongs in external Python with Odoo RPC.
Reliable crons are idempotent, batched, logged, and bounded by time or record count. They respect company context and avoid holding locks across thousands of rows in one transaction.
Automated actions triggered from the UI are fine for demos; production schedules belong in versioned XML with code review and staging validation.
This article covers defining crons in modules, choosing intervals, implementing safe job methods, and operating them in multi-worker deployments.
XML record sets name, model_id, state=code, code=model.method(), interval_number, interval_type, numbercall (-1 for infinite), active, user_id, and priority. Method must exist on model and be callable without UI context.
Load cron data after models and ACL — the job user needs rights to touch target records. Document expected runtime so ops can spot stuck jobs.
- ▸doall=False skips missed runs after downtime — usually preferred
- ▸nextcall in data is optional; server recalculates on activate
- ▸priority lower number runs first when queue backs up
Process in chunks: search with limit, mark state, commit or sub-savepoint per batch. Use a watermark field (last_sync_at, processed_until_id) instead of rescanning entire tables.
Guard re-entry with ir.config_parameter lock or SELECT FOR UPDATE SKIP LOCKED pattern on a queue model when multiple workers could overlap.
Cron runs as user_id on the cron record — record rules apply. For multi-company, iterate companies with with_company(company) or sudo().with_company(company) when rules require elevation.
Email templates and translated messages need lang in context — set on env when generating customer-facing content from cron.
Log start/end counts at info level; log failures with stack traces. For critical jobs, write ir.logging rows or a custom job.log model visible to support.
Deactivate runaway crons in emergencies via UI or active=False in data with hotfix module. Prefer feature flags in config parameters over commenting code in production.
Sub-minute scheduling, GPU work, large file transforms, or retry-heavy external APIs — use queue_job (OCA), Celery, or cloud scheduler hitting Odoo API.
Odoo.sh and on-prem multi-worker setups still share one cron thread per worker behavior depending on version — verify docs for your hosting.
- ✓Make every cron safe to run twice — upsert by external id, not blind create.
- ✓Cap batch size and loop until no work remains instead of one massive transaction.
- ✓Set realistic intervals — hourly sync beats every minute hammering partner API.
- ✓Include cron user in security review and integration runbooks.
- ✓Test cron methods via shell and TransactionCase before relying on schedule.
- ✓Document how to manually trigger method from shell for support playbooks.
- !Cron method raises on first bad row — entire batch rolls back silently.
- !Running as superuser and forgetting record rules in production data mix.
- !Unbounded search([]) on mail.message or stock.move history tables.
- !Duplicating same cron via Studio and module — double execution.
- !Long sleep() inside cron blocking worker thread pool.
- !Assuming missed runs catch up with doall=True on heavy jobs — backlog storm.
Cron jobs are production code — batch them, log them, and assign a real user. Treat intervals and locks as part of the design, not ops afterthought.
When schedules outgrow Odoo workers, move orchestration outward and keep Odoo as system of record via APIs.
Request
HTTP / RPC enters Odoo.
Auth & ACL
Users, groups, record rules.
ORM work
Reads/writes with business logic.
Response
UI, API payload, or job enqueue.
Configure and train
Odoo covers ≥80% of the process
Custom module
Competitive advantage depends on it
Integrate
Another system owns the data
Defer / pilot
Scope is speculative
Frequently asked questions
How do I run a cron manually?
Settings → Technical → Automation → Scheduled Actions → Run Manually, or call method from odoo shell with same user context.
Can one cron call another model?
Code field executes as Python on the model — use env['other.model'].method() inside. Keep indirection readable.
Why did my cron stop after upgrade?
Check active flag, model xml id rename, method rename, or missing ACL for cron user after security refactor.
Scheduled action vs automated action?
Scheduled = time-based ir.cron in code/XML. Automated action = ir.cron or triggers on create/write defined in UI — prefer XML/code for version control.
Related articles
How the Odoo ORM Works
Environments, recordsets, transactions, prefetch, and the create/write/unlink lifecycle — practical ORM behavior for Odoo 16/17 developers.
View →REST APIs and External Integrations
Integrating Odoo with external systems: JSON-RPC/XML-RPC, REST controllers, webhooks, API keys, idempotent sync, and when to build middleware outside Odoo.
View →Deployment Best Practices
Shipping Odoo modules safely: staging parity, -u vs -i, backups, zero-downtime limits, workers, logging, and rollback playbooks for on-prem and Odoo.sh.
View →Testing Odoo Modules
Automated testing for Odoo addons: TransactionCase, SavepointCase, test tags, mocking RPC, view loading checks, and CI patterns for Odoo 16/17.
View →Unreliable nightly jobs or duplicate sync runs?
We redesign cron and queue patterns with idempotent batches and operator-visible job status.
