On this page
Odoo Automated Action: Fix forbidden opcode STORE_ATTR with record.write()
Short answer: ValueError: forbidden opcode(s) in "...": STORE_ATTR means your Python code assigns a field directly, like record.stage_id = 5, which Odoo's safe_eval forbids. Replace it with record.write({'stage_id': 5}) and the error goes away.
Environment
- Odoo 17 / 18 / 19 (
safe_evalbehaves the same in all three) - Settings > Technical > Automation > Automated Actions
- Action To Do set to Execute Python Code
Reproducer (do not do this)
Say you want approved quotations to move stage automatically. This crashes immediately:
for record in records:
record.x_stage = 'approved'
The resulting error:
ValueError: forbidden opcode(s) in "<string>": STORE_ATTR
Why it happens
Automated Actions do not run plain Python. They run through safe_eval, a restricted evaluator where Odoo removes risky opcodes — including STORE_ATTR, i.e. setting an attribute on an object directly.
The reason is security: action code lives in the database and can be edited from the UI, so Odoo forces all writes through the ORM API (write, create, unlink), which enforces access rights on top.
The correct fix
Use record.write() instead of direct assignment:
for record in records:
record.write({'x_stage': 'approved'})
To update several fields at once, pass a single dict:
for record in records:
record.write({
'x_stage': 'approved',
'x_approved_by': user.id,
'x_approved_date': datetime.date.today(),
})
Filter before writing so you do not touch records unnecessarily:
for record in records:
if record.x_stage != 'approved' and record.amount_total > 0:
record.write({'x_stage': 'approved'})
Common mistakes
- Using
record.field = valuein a loop — always fails withSTORE_ATTR, no exceptions. - Calling
records.write(...)on the whole set without filtering — works but overwrites records it should not touch. Loop and check conditions first. - Wrong field name — writing to a non-existent field raises
ValueError: Invalid field. Verify the technical name under Settings > Technical > Database Structure > Fields first. - Assuming a single record —
recordscan hold many. Do not write code that assumes one record; userecord(available when triggered from a form) or handle the set explicitly. - Trying to fix it with
sudo()—STORE_ATTRis not about access rights. Direct assignment still fails undersudo().
Variables available in Execute Python Code
records— the recordset that triggered the actionrecord— a single record (when available)env— the environment, e.g.env['res.partner']for other modelsuser— the current userdatetime,dateutil,time— date handling without imports
If you need to import a library yourself you will hit a different error, covered in Odoo Automated Action: why imports fail and how to fix IMPORT_NAME.
References
- Odoo 19 Automated Actions docs: https://www.odoo.com/documentation/19.0/applications/studio/automated_actions.html
- Odoo developer reference: https://www.odoo.com/documentation/master/developer/reference/frontend/qweb.html
Related notes
- Odoo Automated Action: why imports fail and how to fix IMPORT_NAME
- Odoo QWeb: fixing PDF page breaks with real examples
- All Odoo notes