Odoo's transactional email system handles order confirmations, invoice reminders, purchase approvals, and any other event-driven message your business needs. It has two moving parts: email templates (the message content and recipients) and automated actions (the triggers that fire them). Both are configured through the UI without code. Both have edge cases that swallow messages silently.
Email templates#
A template (Settings → Technical → Email → Templates) stores:
- From address - defaults to the company outgoing mail server address.
- To (Email To) - a Jinja2 expression evaluated at send time:
{{ object.partner_id.email }} - Subject - another Jinja2 expression:
Invoice {{ object.name }} is due on {{ object.invoice_date_due }} - Body - full HTML with QWeb/Jinja2 expressions.
- Attachments - static files or dynamically-generated reports (e.g., invoice PDF).
Jinja2 expressions in templates#
The template engine exposes:
| Variable | What it is |
|---|---|
object | The record the template is sent for |
user | The user triggering the send (or the cron user for automated sends) |
ctx | The context dictionary |
format_date(date, format) | Utility to format date fields |
Example body snippet:
<p>Dear {{ object.partner_id.name }},</p>
<p>Your order <strong>{{ object.name }}</strong> placed on
{{ format_date(object.date_order, 'MMMM d, yyyy') }}
is confirmed.</p>
<p>Total: {{ object.currency_id.symbol }}{{ '%.2f' % object.amount_total }}</p>Dynamic recipient resolution#
The To field supports multiple expressions separated by commas:
{{ object.partner_id.email }},{{ object.user_id.email }}Use {{ ', '.join([f.email for f in object.follower_ids if f.email]) }} to CC all followers.
For Many2many partner fields: {{ object.message_partner_ids.mapped('email') | join(',') }}
Attaching reports#
In the template's Reports tab, add any Odoo report (e.g., "Sale Order / Quotation"). When the template sends, Odoo renders the report for the specific record and attaches the PDF. No code required.
Automated actions#
Automated actions (Settings → Technical → Automation → Automated Actions) bind a trigger to a server action, which can include sending an email template.
Trigger types#
| Trigger | When it fires |
|---|---|
| On record creation | After create() completes |
| On record update | After write() on specified fields |
| On record deletion | Before unlink() |
| Based on a time condition | Cron-driven, relative to a date field |
| On user action (button) | Explicitly called from a button or menu |
Time-based triggers#
Time conditions fire via the base.automation.check scheduled action (default: hourly). The condition is expressed as a delay relative to a date field:
- "1 day before
date_deadline" → sends a reminder the day before the deadline. - "3 hours after
date_invoice" → sends a payment receipt 3 hours after invoicing.
The precision is limited by the cron frequency. If the cron runs hourly and the trigger is "1 hour before deadline," records with deadlines in the past hour will be caught. Records created after the last cron run and whose deadline falls before the next run will be missed.
Configuring the server action#
In the automated action:
- Set Action To Do to "Send an Email."
- Pick the Email Template from the dropdown.
- Optionally add a Before Update Filter (for update triggers) to check what changed.
Filters and before-update domains#
For update triggers, you often want the action to fire only when a specific field reaches a specific value:
- Before Update Filter (domain):
[('state', '=', 'draft')]- the record matched this domain BEFORE the write. - Filter (domain):
[('state', '=', 'sale')]- the record matches this domain AFTER the write.
Combined: fires only on the draft→sale transition. Without the before-update filter, the action fires every time a confirmed order is updated (e.g., adding a note), which causes duplicate sends.
Five delivery pitfalls#
1. No outgoing mail server configured
Templates queue messages in mail.mail. Without an outgoing server (Settings → Technical → Email → Outgoing Mail Servers), nothing leaves. Check the mail queue: Settings → Technical → Email → Emails.
2. SPF/DKIM not set up for the From address
Odoo sends from your configured address, but if your DNS SPF record does not include the sending server's IP, receiving servers silently discard the message. Test with a tool like MXToolbox.
3. Jinja2 expressions raising exceptions
If the template expression fails (e.g., object.partner_id is False), Odoo logs the exception and skips the send. No error surfaces to the user. Check /var/log/odoo/odoo-server.log for mail.template errors.
Guard against empty fields:
{{ object.partner_id.email or 'no-reply@example.com' }}4. Time-based triggers not catching records
If you create a record after the cron ran and its trigger date falls before the next cron run, the record is missed. The cron processes records by evaluating:
WHERE date_field BETWEEN (NOW() - interval) AND NOW()Missing the window means the message never sends. Set the cron to run more frequently for tight time windows.
5. Follower / partner email field empty
Template expressions that rely on partner_id.email silently skip the send if the field is blank. Add a validation on the model or filter the automated action to only fire when the email is populated:
# In automated action filter domain
[('partner_id.email', '!=', False)]Testing before going live#
- Use a catch-all mailbox (e.g., Mailpit or Mailtrap) as the outgoing server during development.
- Trigger the template manually: open a record → Action → Send Email → select the template → preview.
- Check the rendered body for expression errors before sending to real recipients.
- Verify the mail queue clears: Settings → Technical → Email → Emails, check for "Sent" status.

