Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

The Long Task Contract

In Chapter 10, we introduced cooperative long-running tasks.

In this chapter, we define the contract precisely.

This is not an implementation detail. It is an execution model.


Why the Contract Exists

Blender operators execute on the main thread.

If a function performs long-running work in a single call:

  • The UI freezes
  • The user cannot cancel
  • Progress is invisible
  • Blender appears unstable

The long task contract solves this by enforcing cooperative execution.


Declaring a Long Task

A plugin declares:

@op(
    label="Process Frames",
    long_task=True,
)
def process_frames(...):
    ...

If long_task=True, the function must follow the long task contract.


Strict Conformance

If long_task=True is declared:

  • The function must be a python generator.
  • The generator must yield at least once.

If the function:

  • Does not contain yield
  • Returns normally without yielding
  • Raises StopIteration immediately

The build must fail.

QuickAddon enforces this at build time.

Long tasks are not optional behavior. They are a contract.

Strict conformance guarantees:

  • Non-blocking execution
  • Predictable scheduling
  • Safe cancellation
  • Stable host control

The Core Rule

A long task must:

Perform incremental work and yield control back to the host after each step.

It must not block until completion.


Required Execution Model

A long task function must behave as a generator.

Conceptually:

def process_frames(...):
    total = compute_total()

    for i in range(total):
        do_one_unit_of_work(i)

        yield {
            "progress": i,
            "total": total,
        }

The function:

  • Executes a small unit of work
  • Yields a progress payload
  • Allows the host to resume later

It does not run to completion in a single call.


Yield Payload

Each yield must return a dictionary.

Minimum required fields:

progress
total

Optional fields:

message

Example:

yield {
    "progress": i,
    "total": total,
    "message": f"Processing frame {i}",
}

The host uses this data to:

  • Update the progress bar
  • Display status messages
  • Determine completion

Completion

When the generator finishes:

  • The task is considered complete.
  • The host performs cleanup.
  • Progress UI closes.
  • Execution returns FINISHED.

Cancellation

The host must provide an explicit cancellation mechanism.

Recommended UI model:

  • Status bar progress indicator
  • Visible cancel button (“X”)
  • ESC key support only while modal operator owns execution

When cancellation is triggered:

  • Host stops calling next()
  • Generator is discarded
  • Progress UI closes
  • Operator returns CANCELLED

Cancellation must be:

  • Explicit
  • Predictable
  • Scope-aware (v2)
  • Safe

Plugins must not rely on guaranteed completion.

They must tolerate interruption at any yield boundary.

Error Handling

If a long task generator raises an exception:

  • The host must stop execution.
  • Progress UI must close.
  • The error must be reported via Blender operator report.

If a yield payload:

  • Is not a dictionary
  • Omits required fields
  • Contains invalid values

The host must:

  • Cancel execution
  • Report a contract violation

The long task contract is enforced at runtime as well as at build time.


Host Responsibilities

The host must:

  • Detect long_task=True
  • Wrap execution in a modal operator
  • Schedule incremental next() calls
  • Update progress display
  • Provide cancellation
  • Ensure scope isolation (if scoped)

The host owns scheduling.

Plugins must not implement modal logic directly.


Interaction with Scoped Instances

If HostAPI v2 is used:

  • Each scope maintains its own long-task state.
  • Progress and cancellation apply only to that scope.
  • Multiple scoped instances may run independently.

Isolation must be enforced at the host level.


Forbidden Patterns

A long task must not:

  • Execute blocking loops without yielding
  • Spawn uncontrolled threads
  • Access scene storage directly
  • Assume uninterrupted execution
  • Manipulate modal handlers directly

The contract enforces discipline.


Short Tasks vs Long Tasks

Short task:

@op(...)
def simple_task(...):
    perform_all_work()

Long task:

@op(long_task=True)
def incremental_task(...):
    yield ...

The decorator determines the execution strategy.


Why This Matters

The long task contract provides:

  • Non-blocking UI
  • Visible progress
  • Safe cancellation
  • Predictable lifecycle
  • Stable host-controlled execution

It allows complex processing without destabilizing Blender. In chapter 15 we will expand further we examine how the generator implements this model internally.

In the next chapter, we will examine how the HostAPI paradigm can be used for more complex need.