Use when creating custom Blender operators -- bpy.types.Operator subclasses with execute, invoke, or modal methods. Prevents the common mistake of not implementing poll() (causing silent failures) or using wrong bl_idname format. Covers operator lifecycle, bl_options, return values, properties, and context.temp_override (4.0+ replacement for context override). Keywords: Operator, execute, invoke, modal, poll, bl_idname, bl_options, temp_override, REGISTER, UNDO, operator properties, bpy.ops, create custom operator, add button, operator CANCELLED.
license
MIT
compatibility
Designed for Claude Code. Requires Blender 3.x/4.x/5.x with Python.
metadata
{"author":"OpenAEC-Foundation","version":"1.0"}
blender-syntax-operators
Quick Reference
Critical Warnings
ALWAYS return a set from execute(), invoke(), and modal() — e.g., {'FINISHED'}, {'CANCELLED'}, {'RUNNING_MODAL'}. Returning a plain string or None crashes Blender.
ALWAYS use context.temp_override() in Blender 4.0+. Dict-based context overrides () were REMOVED in 4.0.
bpy.ops.foo(override_dict, ...)
ALWAYS set bl_options = {'REGISTER', 'UNDO'} for operators that modify scene data. Omitting 'UNDO' means users cannot Ctrl+Z the operation.
ALWAYS implement poll() as a @classmethod. Forgetting @classmethod causes a TypeError at registration.
NEVER call bpy.ops.* inside Panel.draw() — draw callbacks are read-only. Use operator buttons via layout.operator() instead.
NEVER use uppercase letters in the category part of bl_idname. The format is "category.operator_name" — both parts MUST be lowercase with underscores.
NEVER store mutable state as class-level attributes on operators expecting per-instance behavior. Use self instance attributes set in invoke() or execute(), or use operator properties.
Operator Decision Tree
Need to create a Blender operation?
│
├─ Runs once, no user interaction needed?
│ └─ Implement execute() only
│ └─ Set invoke = execute (optional shorthand)
│
├─ Needs a dialog/popup before running?
│ └─ Implement invoke() → wm.invoke_props_dialog(self)
│ └─ Implement draw() for dialog layout
│ └─ Implement execute() for the actual work
│
├─ Needs continuous event handling (drag, timer, mouse)?
│ └─ Implement invoke() → wm.modal_handler_add(self) + return {'RUNNING_MODAL'}
│ └─ Implement modal() for event processing
│ └─ Implement cancel() for cleanup
│
└─ Needs confirmation popup?
└─ Implement invoke() → wm.invoke_confirm(self, event)
└─ Implement execute() for the confirmed action
# Override active object for operator executionwith bpy.context.temp_override(active_object=obj, selected_objects=[obj]):
bpy.ops.object.shade_smooth()
# Override area type for viewport operatorsdefrun_in_viewport(operator_call):
"""Execute an operator that requires a VIEW_3D area context."""for window in bpy.context.window_manager.windows:
for area in window.screen.areas:
if area.type == 'VIEW_3D':
with bpy.context.temp_override(window=window, area=area):
return operator_call()
raise RuntimeError("No VIEW_3D area found")
# Usage:
run_in_viewport(lambda: bpy.ops.view3d.snap_cursor_to_center())
Pattern 6: Confirmation Dialog
classMYCAT_OT_delete_all(bpy.types.Operator):
"""Delete all objects with confirmation"""
bl_idname = "mycat.delete_all"
bl_label = "Delete All Objects"
bl_options = {'REGISTER', 'UNDO'}
@classmethoddefpoll(cls, context):
returnlen(bpy.data.objects) > 0definvoke(self, context, event):
return context.window_manager.invoke_confirm(self, event)
defexecute(self, context):
for obj in bpy.data.objects:
bpy.data.objects.remove(obj)
self.report({'WARNING'}, "All objects deleted")
return {'FINISHED'}
Common Operations
Calling Operators from Scripts
# Direct call: uses current context
bpy.ops.mesh.primitive_cube_add(size=2.0, location=(0, 0, 1))
# With context override (Blender 4.0+)with bpy.context.temp_override(active_object=obj):
bpy.ops.object.modifier_apply(modifier="Boolean")
# Check if operator can run before callingif bpy.ops.object.mode_set.poll():
bpy.ops.object.mode_set(mode='EDIT')
Registration and Menus
# Register operator and add to menudefmenu_func(self, context):
self.layout.operator(MYCAT_OT_simple_action.bl_idname)
defregister():
bpy.utils.register_class(MYCAT_OT_simple_action)
bpy.types.VIEW3D_MT_object.append(menu_func)
defunregister():
bpy.types.VIEW3D_MT_object.remove(menu_func)
bpy.utils.unregister_class(MYCAT_OT_simple_action)
Operator Reporting
# Report types: 'DEBUG', 'INFO', 'OPERATOR', 'WARNING', 'ERROR', 'ERROR_INVALID_INPUT'self.report({'INFO'}, "Operation completed")
self.report({'WARNING'}, "Check the result")
self.report({'ERROR'}, "Something went wrong") # Shows in status bar, does NOT raise
bl_idname Naming Convention
CLASS_OT_operator_name
│ │ │
│ │ └─ snake_case descriptive name
│ └───── OT = Operator Type (ALWAYS "OT" for operators)
└──────────── UPPERCASE category prefix (matches addon/module)
The bl_idname string format: "category.operator_name" — both parts lowercase.
The class name format: CATEGORY_OT_operator_name — category uppercase, rest snake_case.
# CORRECT:classMESH_OT_add_custom(bpy.types.Operator):
bl_idname = "mesh.add_custom"# lowercase.lowercase# WRONG: bl_idname has uppercase:classMESH_OT_add_custom(bpy.types.Operator):
bl_idname = "Mesh.AddCustom"# WILL FAIL at registration
bl_options Reference
Flag
Use When
'REGISTER'
ALWAYS — makes operator visible in info log and F3 search
'UNDO'
Operator modifies scene data (objects, meshes, materials)
'UNDO_GROUPED'
Multiple rapid calls should be one undo step (e.g., timer-based updates)
'BLOCKING'
Modal operator should block ALL other event handlers
'GRAB_CURSOR'
Modal with mouse movement should wrap cursor at screen edges
'GRAB_CURSOR_X'
Wrap cursor on X axis only
'GRAB_CURSOR_Y'
Wrap cursor on Y axis only
'INTERNAL'
Operator should NOT appear in F3 search menu
'PRESET'
Show preset selector in operator properties panel
'MACRO'
Operator is a macro containing sub-operators
'MODAL_PRIORITY'
(4.2+) Receive events before other modal operators
Return Values
Value
When to Use
{'FINISHED'}
Operator completed successfully
{'CANCELLED'}
Operator was cancelled, no changes made
{'RUNNING_MODAL'}
Operator is entering modal mode (from invoke())
{'PASS_THROUGH'}
Modal: allow other operators to also handle this event
{'INTERFACE'}
Operator handled event but did not execute (popup shown)
event.type# str: 'LEFTMOUSE', 'RIGHTMOUSE', 'ESC', 'TIMER', 'A', 'B', etc.
event.value # str: 'PRESS', 'RELEASE', 'CLICK', 'DOUBLE_CLICK', 'NOTHING'
event.mouse_x # int: absolute mouse X position
event.mouse_y # int: absolute mouse Y position
event.mouse_region_x # int: mouse X relative to region
event.mouse_region_y # int: mouse Y relative to region
event.shift # bool: Shift held
event.ctrl # bool: Ctrl held
event.alt # bool: Alt held
event.oskey # bool: OS/Super key held
Reference Links
references/methods.md — Complete API signatures for Operator, WindowManager, Event, and registration functions