All posts
Integrator7 min read

Odoo email templates and automated actions: triggers, dynamic expressions, and delivery gotchas

Odoo email templates use QWeb expressions evaluated at send time. Automated actions bind those templates to record lifecycle events. Together they handle most transactional email needs - if you avoid the five common pitfalls that silently drop messages.

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:

VariableWhat it is
objectThe record the template is sent for
userThe user triggering the send (or the cron user for automated sends)
ctxThe context dictionary
format_date(date, format)Utility to format date fields

Example body snippet:

html
<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#

TriggerWhen it fires
On record creationAfter create() completes
On record updateAfter write() on specified fields
On record deletionBefore unlink()
Based on a time conditionCron-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:

  1. Set Action To Do to "Send an Email."
  2. Pick the Email Template from the dropdown.
  3. 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:

sql
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:

python
# In automated action filter domain
[('partner_id.email', '!=', False)]

Testing before going live#

  1. Use a catch-all mailbox (e.g., Mailpit or Mailtrap) as the outgoing server during development.
  2. Trigger the template manually: open a record → Action → Send Email → select the template → preview.
  3. Check the rendered body for expression errors before sending to real recipients.
  4. Verify the mail queue clears: Settings → Technical → Email → Emails, check for "Sent" status.

Try ERPeek on your own Odoo module - ask questions, scaffold tests, and explore your codebase in plain language.

Get started free