Context and Domain in Odoo — Developer Reference (Odoo 16–19)
A practical guide to Odoo domains (record filtering) and context (key–value state passing) — how they work, where they appear, and the syntax rules for Odoo 16 through 19. Verified for Odoo 18 & 19.
Two concepts appear throughout Odoo's XML views, Python models, RPC calls, and JavaScript code: **domain** and **context**. A domain is a list of conditions used to filter records — the Odoo equivalent of a SQL `WHERE` clause. A context is a Python dictionary that carries state between the UI and server, controlling default values, view switches, grouping, and behaviour flags. Getting both right is essential for building reliable views, actions, and model methods.
Part 1 — Domain Syntax
A domain is a Python list of triples (tuples), where each triple has the form `(field_name, operator, value)`. Multiple triples are combined with `AND` logic by default. Prefix operators `'|'` (OR) and `'!'` (NOT) change the logic for the immediately following conditions.
# Basic domain — AND logic (default)
# Returns records where state is 'sale' AND partner_id is 7
domain = [('state', '=', 'sale'), ('partner_id', '=', 7)]
# OR logic — use '|' prefix before the two conditions it joins
# Returns records where state is 'sale' OR state is 'done'
domain = ['|', ('state', '=', 'sale'), ('state', '=', 'done')]
# NOT logic — use '!' prefix before the condition to negate
# Returns records where state is NOT 'cancel'
domain = ['!', ('state', '=', 'cancel')]
# Empty domain — matches all records
domain = []Domain Operators Reference
| Operator | Meaning | Value type | Example |
|---|---|---|---|
| = | Equal | Scalar | ('state', '=', 'done') |
| != | Not equal | Scalar | ('state', '!=', 'cancel') |
| > | Greater than | Number / date | ('amount_total', '>', 0) |
| >= | Greater than or equal | Number / date | ('date', '>=', '2026-01-01') |
| < | Less than | Number / date | ('qty', '<', 10) |
| <= | Less than or equal | Number / date | ('date', '<=', '2026-12-31') |
| =? | Equal or field is False/None (null-safe equal) | Scalar or False | ('partner_id', '=?', False) |
| like | Contains substring (case-sensitive) | String | ('name', 'like', 'Odoo') |
| ilike | Contains substring (case-insensitive) | String | ('name', 'ilike', 'odoo') |
| =like | Matches pattern with % wildcard (case-sensitive) | String with % | ('ref', '=like', 'INV%') |
| =ilike | Matches pattern with % wildcard (case-insensitive) | String with % | ('ref', '=ilike', 'inv%') |
| in | Value is in list | List | ('state', 'in', ['sale', 'done']) |
| not in | Value is not in list | List | ('state', 'not in', ['draft', 'cancel']) |
| child_of | Record or its children (hierarchical models) | ID or list of IDs | ('category_id', 'child_of', 5) |
| parent_of | Record or its parents (hierarchical models) | ID or list of IDs | ('category_id', 'parent_of', 12) |
Domain in XML Views
In XML views, domains are written as strings (the Python list is expressed as a string literal). Odoo evaluates the string in the context of the current record's field values. Use `uid` for the current user's ID and `context_today()` for today's date.
<!-- Domain on a Many2one field — filters dropdown options -->
<field name="user_id"
domain="[('groups_id', 'in', [ref('base.group_user')])]"/>
<!-- Domain on an action button — filters records opened by the action -->
<button name="action_view_invoices" type="object"
domain="[('state', '=', 'posted'), ('partner_id', '=', active_id)]"/>
<!-- Domain referencing current user -->
<field name="assigned_to"
domain="[('share', '=', False), ('id', '!=', uid)]"/>
<!-- Domain using context_today() for date comparison -->
<filter name="overdue"
domain="[('date_deadline', '<', context_today().strftime('%Y-%m-%d'))]"/>Domain in Python (ORM search and search_count)
# search() — returns recordset matching domain
invoices = self.env['account.move'].search([
('state', '=', 'posted'),
('partner_id', '=', self.partner_id.id),
('invoice_date', '>=', '2026-01-01'),
], order='invoice_date desc', limit=10)
# search_count() — returns integer count
count = self.env['sale.order'].search_count([
('user_id', '=', self.env.uid),
('state', 'not in', ['cancel', 'draft']),
])
# filtered() — applies a domain-like condition to an existing recordset
# (uses a lambda, not a domain list — different API)
confirmed = orders.filtered(lambda o: o.state == 'sale')
# _search() / _domain — advanced: used inside compute methods
# to build dynamic domains programmatically
domain = [('company_id', '=', self.env.company.id)]
if self.partner_id:
domain += [('partner_id', '=', self.partner_id.id)]Part 2 — Context
The context is a Python dictionary (`dict`) passed along with every RPC call, action open, and wizard invocation. It carries user-session information (language, timezone, active record) and developer-set flags that modify how models, views, and actions behave. You can read it in Python with `self.env.context` and extend it with `self.with_context(...)`.
Standard Context Keys
| Key | Type | Set by | Effect |
|---|---|---|---|
| lang | str | User profile | Active language code — e.g. `'en_US'`, `'ar_001'`. Used for translations. |
| tz | str | User profile | Timezone string — e.g. `'Asia/Riyadh'`. Used by date/datetime fields. |
| uid | int | Session | Current user ID. Available in domain expressions in XML views. |
| active_id | int | UI / action | ID of the currently selected/active record. Passed when opening a wizard or related action. |
| active_ids | list[int] | UI / action | List of IDs of all selected records (multi-record actions). |
| active_model | str | UI / action | Technical name of the model the action was triggered from — e.g. `'sale.order'`. |
| default_field_name | any | Developer / action | Sets a default value for `field_name` when a new record is created in the opened view. Replace `field_name` with the actual field name. |
| no_create | bool | Developer | When True on a Many2one field, disables the quick-create option in the dropdown. |
| no_open | bool | Developer | When True on a Many2one field, disables the external link / open record option. |
| group_by | list[str] | Developer / search filter | List of field names to group records by in list/kanban views. |
| search_default_field_name | 1 | Action context | Pre-applies a search filter for `field_name` when the action opens. Value must be `1` (truthy). |
Context in XML Views and Actions
<!-- Set default values when opening a new record form -->
<field name="partner_id"
context="{'default_company_type': 'company', 'default_country_id': ref('base.sa')}"/>
<!-- Pre-apply a search filter when an action opens -->
<record id="action_sale_order_my_orders" model="ir.actions.act_window">
<field name="name">My Sales Orders</field>
<field name="res_model">sale.order</field>
<field name="context">{
'search_default_user_id': uid,
'search_default_state_sale': 1
}</field>
</record>
<!-- Group by partner when the action opens -->
<record id="action_invoices_grouped" model="ir.actions.act_window">
<field name="name">Invoices by Partner</field>
<field name="res_model">account.move</field>
<field name="context">{'group_by': ['partner_id']}</field>
</record>
<!-- Disable quick-create on a Many2one field -->
<field name="product_id"
context="{'no_create': True, 'no_open': True}"/>Context in Python Model Methods
# Read context in a model method
def action_confirm(self):
# Check if called from a specific flow
if self.env.context.get('from_purchase'):
# custom behaviour
pass
# with_context() — extend context for a specific ORM call
# Does NOT modify self.env.context — returns a new recordset
records_ar = self.env['product.template'].with_context(lang='ar_001').search([])
# Common: set a flag to skip a compute or onchange in a loop
self.with_context(skip_onchange=True).write({'state': 'done'})
# Pass context when opening a wizard action programmatically
return {
'type': 'ir.actions.act_window',
'res_model': 'my.wizard',
'view_mode': 'form',
'target': 'new',
'context': {
'default_partner_id': self.partner_id.id,
'default_amount': self.amount_total,
'active_id': self.id,
'active_model': self._name,
},
}Using Domain and Context Together
Fields, buttons, and actions can carry both a `domain` and a `context` simultaneously. The domain filters which records are shown or selectable; the context controls defaults and flags for records created or opened in the resulting view.
<!-- Smart button — open related records filtered by domain,
with a default set via context for any new record created there -->
<button name="action_view_picking"
type="object"
domain="[('sale_id', '=', active_id), ('state', '!=', 'cancel')]"
context="{
'default_sale_id': active_id,
'default_partner_id': partner_id,
'search_default_state_ready': 1
}"
string="Transfers"/>Version Notes
| Odoo version | Key domain / context changes |
|---|---|
| Odoo 16 | Domain syntax unchanged. `attrs` deprecated (see companion D6 guide). `<tree>` → `<list>` rename. `context_today()` available in XML domains. |
| Odoo 17 | `attrs` removed (domain on fields unaffected). `parent_of` operator added for hierarchical models. Context propagation in actions unchanged. |
| Odoo 18 | No breaking changes to domain or context API. `=?` null-safe operator behaviour documented in ORM changelog. |
| Odoo 19 | No breaking changes. Domain and context behaviour stable. ORM performance improvements to `search()` for large datasets. |
Need Help with Custom Odoo Development or Module Upgrades?
Our Odoo-certified developers handle custom domain logic, context-driven workflows, and module migrations from Odoo 15 to 17/18/19 — with full ZATCA and GOSI compliance verification.
Frequently Asked Questions
What is the difference between a domain on a field and a domain on an action?
Can I use a computed field's value inside a domain?
How do I pass context from a Python action to a wizard?
What does with_context() do and does it modify the current environment?
How do I pre-apply a search filter when an action opens?
Can I use context keys to disable a @api.onchange in a write() call?

iWesabe Editorial Team
Practitioner insights on Odoo ERP, ZATCA compliance, and Saudi enterprise digital operations — written by iWesabe's consulting, finance, and engineering teams.
Related Articles
Odoo attrs Deprecated: Migrating to invisible, readonly, and required in Odoo 16–19
The attrs dictionary was deprecated in Odoo 16 and removed in Odoo 17. This guide maps every attrs pattern to its direct-attribute replacement — with before/after code for the most common use cases. Verified for Odoo 18 & 19.
How to Add Colors to List View Rows and Cells in Odoo
Use decoration-* attributes to colour-code rows and individual cells in Odoo list views based on field values — with expression syntax, all decoration types, and a migration note for the tree → list rename. Verified for Odoo 18 & 19.
Access Security in Odoo — ir.model.access.csv and Record Rules (Odoo 16–19)
A complete developer reference for Odoo's two-layer security system: model-level CRUD permissions via ir.model.access.csv and record-level filtering via ir.rule. Verified for Odoo 18 & 19.