Use when implementing complex Blender operators -- modal operators with timer callbacks, file browsers, batch processing, or multi-step workflows. Prevents the common mistake of blocking the UI thread in long operations instead of using modal + timer pattern. Covers modal operators, file browser integration, undo/redo support, progress reporting, and batch processing operators. Keywords: modal operator, timer callback, file browser, batch processing, progress reporting, undo support, multi-step workflow, INVOKE_DEFAULT, make custom button, add menu item, create toolbar button.
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-impl-operators
Quick Reference
Critical Warnings
ALWAYS clean up timers in BOTH cancel() AND the {'FINISHED'} path. Leaked timers fire indefinitely and reference deleted operator instances.
ALWAYS call wm.modal_handler_add(self) BEFORE returning {'RUNNING_MODAL'} from invoke(). Returning {'RUNNING_MODAL'} without a handler registration means events go nowhere.
ALWAYS handle ESC and RIGHTMOUSE in modal() to allow user cancellation. Without an escape path, the user is locked in the modal state.
ALWAYS set bl_options = {'REGISTER', 'UNDO'} for operators that modify scene data. 'UNDO' alone has NO effect — 'REGISTER' is required for undo to work.
NEVER modify bpy.data from a background thread. Use bpy.app.timers.register() with a queue.Queue to marshal work to the main thread.
NEVER expect viewport redraws during a synchronous execute(). The UI is frozen until execute() returns. Use a modal operator with timer for progressive visual updates.
NEVER use threading.Timer for deferred Blender operations. Use bpy.app.timers.register() instead.
Implementation Decision Tree
Need to implement a complex Blender operator?
│
├─ Long-running operation that must show progress?
│ └─ Use modal operator with timer (Section 1)
│ └─ Add wm.progress_begin/update/end (Section 5)
│ └─ Process items incrementally per TIMER event
│
├─ Need user to select a file?
│ └─ Use ImportHelper/ExportHelper mixins (Section 2)
│ └─ Or manual fileselect_add for custom file dialogs
│
├─ Need to process many objects in batch?
│ ├─ Fast operation (< 1 second)?
│ │ └─ Use synchronous execute() with context.temp_override (Section 4)
│ └─ Slow operation (> 1 second)?
│ └─ Use modal timer + progress reporting (Section 1 + 5)
│
├─ Need multi-step user interaction?
│ ├─ Sequential clicks/points in viewport?
│ │ └─ Use state machine in modal() (Section 6)
│ └─ Parameter dialog before execution?
│ └─ Use invoke_props_dialog (Section 6)
│
├─ Need deferred execution outside an operator?
│ └─ Use bpy.app.timers.register() (Section 7)
│ └─ Return None to run once, return float to repeat
│
└─ Need undo support?
├─ Single operation?
│ └─ bl_options = {'REGISTER', 'UNDO'} (Section 3)
└─ Repeated rapid calls (e.g., timer-driven)?
└─ bl_options = {'REGISTER', 'UNDO_GROUPED'} (Section 3)
Version Compatibility Matrix
Feature
Blender 3.x
Blender 4.0+
Blender 5.x
wm.event_timer_add(t, window=w)
Keyword arg
Keyword arg
Keyword arg
bpy.app.timers.register()
Available
Available
Available
context.temp_override()
Available from 3.2
REQUIRED
REQUIRED
Dict context override
Deprecated (3.2+)
REMOVED
REMOVED
UILayout.progress()
Not available
Available (4.0+)
Available
ImportHelper/ExportHelper
Stable
Stable
Stable
UNDO_GROUPED bl_option
Available
Available
Available
Section 1: Modal Operators with Timer Callbacks
When to Use
Use modal operators with timers when an operation takes more than ~0.5 seconds and must:
Use filepath: StringProperty(subtype='FILE_PATH') or directory: StringProperty(subtype='DIR_PATH'), call context.window_manager.fileselect_add(self) in invoke(), return {'RUNNING_MODAL'}.
Menu Registration
Register import operators to bpy.types.TOPBAR_MT_file_import.append(menu_func). Register export operators to bpy.types.TOPBAR_MT_file_export.append(menu_func). ALWAYS remove in unregister() to prevent duplicate entries on addon reload.
Section 3: Undo/Redo Support
bl_options for Undo
Option
Effect
{'REGISTER', 'UNDO'}
One undo step on {'FINISHED'}. ALWAYS use for data-modifying operators.
{'REGISTER', 'UNDO_GROUPED'}
Consecutive calls of the same operator produce one undo step. Use for repeated rapid calls.
{'REGISTER'} alone
NO undo step. Use only for read-only / reporting operators.
Rules
'UNDO' requires 'REGISTER' — without 'REGISTER', 'UNDO' has NO effect
Returning {'CANCELLED'} NEVER creates an undo step
Modal operators with {'UNDO'} push ONE undo step when {'FINISHED'} is returned — all intermediate modifications are bundled
If execute() modifies data then returns {'CANCELLED'}, those changes persist WITHOUT being undoable — ALWAYS restore state before cancelling
UNDO_GROUPED consolidates consecutive calls of the SAME operator into one undo step — use for timer-driven modifications
defbatch_apply_modifiers(objects):
"""Apply all modifiers on multiple objects."""for obj in objects:
with bpy.context.temp_override(active_object=obj, object=obj):
for mod in obj.modifiers[:]: # Copy list — modifiers removed during iterationtry:
bpy.ops.object.modifier_apply(modifier=mod.name)
except RuntimeError as e:
print(f"Cannot apply {mod.name} on {obj.name}: {e}")
Rules
ALWAYS copy collections before iterating if the loop modifies them: list(context.selected_objects)
ALWAYS copy modifier lists before applying: obj.modifiers[:]
Use context.temp_override() when calling operators that check context.active_object
For batch operations on 100+ objects, use the modal timer pattern (Section 1) for responsiveness
Section 5: Progress Reporting
WindowManager Progress API
wm = context.window_manager
wm.progress_begin(0, total_count) # Initialize range
wm.progress_update(current_index) # Update cursor indicator
wm.progress_end() # Finish — ALWAYS call, even on error
Header Text as Progress (alternative)
# In modal() during processing:
context.area.header_text_set(f"Processing: {i}/{total} ({i/total*100:.0f}%)")
# On completion or cancel: ALWAYS restore:
context.area.header_text_set(None)
UILayout.progress() (Blender 4.0+)
# In Panel.draw() or Operator.draw() only:
layout.progress(factor=0.66, type='BAR', text="66%")
Known Issue
wm.progress_end() does not immediately reset the cursor — it stays as a busy indicator until the user moves the cursor. This is a confirmed Blender bug, not a coding error.
import bpy
import queue
import threading
_queue = queue.Queue()
def_process_queue():
whilenot _queue.empty():
fn = _queue.get()
fn()
return1.0# Check every seconddefrun_on_main_thread(fn):
"""Schedule a function to run on Blender's main thread."""
_queue.put(fn)
# Register in addon register():
bpy.app.timers.register(_process_queue, persistent=True)
bpy.app.timers vs wm.event_timer_add
bpy.app.timers
wm.event_timer_add
Requires modal operator
No
Yes
Tied to specific window
No
Yes
Survives file load
Yes (persistent=True)
No
User-cancellable
No (must unregister)
Yes (ESC in modal)
Use case
Background tasks, thread bridge
Interactive modal tools
Reference Links
references/methods.md — Complete API signatures for modal, timer, progress, and file browser APIs