Skip to content

Odoo mail.thread & Field Tracking Guide: Chatter, Followers, and Notifications

DeployMonkey Team · March 24, 2026 11 min read

What is mail.thread?

The mail.thread mixin adds Odoo's signature chatter functionality to any model. It provides message logging, field change tracking, follower management, email notifications, and activity scheduling. Nearly every important Odoo model inherits from mail.thread — sales orders, invoices, leads, tasks, and helpdesk tickets all use it.

Basic Setup

class ServiceRequest(models.Model):
    _name = 'service.request'
    _inherit = ['mail.thread', 'mail.activity.mixin']
    _description = 'Service Request'

    name = fields.Char(required=True, tracking=True)
    state = fields.Selection([
        ('new', 'New'),
        ('in_progress', 'In Progress'),
        ('done', 'Done'),
    ], default='new', tracking=True)
    partner_id = fields.Many2one('res.partner', tracking=True)
    priority = fields.Selection([
        ('0', 'Normal'),
        ('1', 'High'),
        ('2', 'Urgent'),
    ], tracking=True)

Field Tracking

How tracking=True Works

When you set tracking=True on a field, Odoo automatically logs a message in the chatter whenever that field's value changes. The message shows the old value, new value, and who made the change.

Tracking Priority

You can also use tracking=N (integer) to control the order fields appear in tracking messages:

name = fields.Char(tracking=1)   # Appears first
state = fields.Selection([...], tracking=2)  # Second
partner_id = fields.Many2one('res.partner', tracking=3)  # Third

Lower numbers appear first. tracking=True is equivalent to tracking=100.

Custom Tracking Values

Override _track_set_log_message() for custom tracking behavior, or override _tracking_value_display() to customize how values are displayed.

Message Posting

Post a Note (Internal)

record.message_post(
    body='Internal note about this request.',
    message_type='comment',
    subtype_xmlid='mail.mt_note',
)

Post a Comment (Notifies Followers)

record.message_post(
    body='This request has been approved.',
    message_type='comment',
    subtype_xmlid='mail.mt_comment',
)

Post with Attachments

attachment = self.env['ir.attachment'].create({
    'name': 'report.pdf',
    'datas': base64_data,
    'res_model': record._name,
    'res_id': record.id,
})
record.message_post(
    body='Report attached.',
    attachment_ids=[attachment.id],
)

Follower Management

Subscribe Followers

# Subscribe specific partners
record.message_subscribe(partner_ids=[partner.id])

# Subscribe with specific subtypes
record.message_subscribe(
    partner_ids=[partner.id],
    subtype_ids=[subtype.id],
)

Unsubscribe

record.message_unsubscribe(partner_ids=[partner.id])

Auto-Subscribe on Create

Odoo automatically subscribes the creating user. Override _message_auto_subscribe_followers() to subscribe additional partners based on field values.

Message Subtypes

Subtypes control what followers are notified about:

<record id="mt_request_new" model="mail.message.subtype">
    <field name="name">New Request</field>
    <field name="res_model">service.request</field>
    <field name="default" eval="True"/>
    <field name="description">New service request created</field>
</record>

<record id="mt_request_done" model="mail.message.subtype">
    <field name="name">Request Completed</field>
    <field name="res_model">service.request</field>
    <field name="default" eval="True"/>
    <field name="description">Service request completed</field>
</record>

Triggering Subtypes on State Change

def _track_subtype(self, init_values):
    self.ensure_one()
    if 'state' in init_values:
        if self.state == 'done':
            return self.env.ref('my_module.mt_request_done')
    return super()._track_subtype(init_values)

Email Notifications

Control email content with templates:

record.message_post_with_source(
    'my_module.email_template_request_assigned',
    subtype_xmlid='mail.mt_comment',
    render_values={'request': record},
)

Common Patterns

Log State Changes with Custom Messages

def action_confirm(self):
    self.write({'state': 'confirmed'})
    for record in self:
        record.message_post(
            body="Request confirmed by %s." % self.env.user.name,
            message_type='notification',
        )

Notify Without Logging

record.message_notify(
    partner_ids=[partner.id],
    body='You have been assigned to this request.',
    subject='Assignment Notification',
)

Common Pitfalls

  • Tracking on computed fieldstracking=True only works on stored fields. Non-stored computed fields cannot be tracked.
  • Performance with many tracked fields — Each tracked field adds overhead on write. Track only fields users need to see in the chatter.
  • sudo() and tracking — When using sudo(), the tracked changes show SUPERUSER as the author. Use with_user() to preserve the actual user.
  • Batch creates — tracking fires for each record in a batch create. For high-volume imports, consider temporarily disabling tracking.