# Trigger event when CLI run has completed

**URL:** <https://discourse.slicer.org/t/trigger-event-when-cli-run-has-completed/3774>\
**Category:** Support\
**Created:** [August 14, 2018, 3:07pm UTC](https://discourse.slicer.org/t/trigger-event-when-cli-run-has-completed/3774 "2018-08-14T15:07:55Z")\
**Posts on this page:** 1\
**Showing post:** 6

<div class="post-metadata">

**Author:** ![pieper](https://sea2.discourse-cdn.com/flex002/user_avatar/discourse.slicer.org/pieper/32/8_2.png) [@pieper](https://discourse.slicer.org/u/pieper)\
**Post date:** [February 21, 2026, 3:55pm UTC](https://discourse.slicer.org/t/trigger-event-when-cli-run-has-completed/3774/6 "2026-02-21T15:55:03Z")

</div>

I’ve been working on a [slicer-skill](https://github.com/pieper/slicer-skill) to see if AI agents can help with slicer development. I put your question in and I got the answer below. I don’t know if it’ll fix your problem but it sounds about right.

`<ai>`

## Root Cause Analysis: CLI `StatusModifiedEvent` Not Firing Until UI Interaction

This is a well-understood architectural issue in Slicer. Here’s exactly what’s happening and why.

### The Status Update Chain

When a CLI module runs asynchronously via `Apply()`, status updates follow this path:

```auto
Background Thread (ApplyTask):
  node->SetStatus(Completing, false) // flags InvokeStatusModifiedEvent=true, but NO Modified() call
  appLogic->RequestModified(node) // pushes node onto a thread-safe queue
          |
          v
Main Thread Queue (InternalModifiedQueue):
  [node sits waiting in queue...]
          |
          v
QTimer::singleShot chain: ← REQUIRES Qt EVENT LOOP TO BE SPINNING
  processAppLogicModified()
    → ProcessModified()
      → node->Modified()
        → StatusModifiedEvent ← YOUR OBSERVER FIRES HERE

```

The critical link is **step 3** : `ProcessModified()` is triggered by a self-rescheduling `QTimer::singleShot` chain managed in `qSlicerCoreApplication`. These timers **can only fire when Qt’s event loop is actively processing events**.

### The Key Question: Is This a Scripted CLI or Executable CLI?

**If your CLI module is a _scripted_ CLI module** (Python-based, `*.py` with the CLI XML descriptor), this is almost certainly the cause:

- **Scripted CLI modules run in Slicer’s main thread** due to Python’s GIL (Global Interpreter Lock). Even though Slicer tries to run them in a background thread, the GIL forces Python execution back onto the main thread.
- While your `torch + cv2` pipeline is running, it monopolizes the main thread. The Qt event loop **cannot spin** , so `QTimer::singleShot` callbacks never fire, and `ProcessModified()` is never called.
- `time.sleep()` works because it **releases the GIL** , allowing the Qt event loop to process pending events during the sleep.

This is confirmed by Steve Pieper in a [related discourse thread](https://discourse.slicer.org/t/running-scripted-cli-prevents-qprocess-from-finishing/26534): _“CLI modules are run in independent threads, while python code runs in the main thread, so that may be the difference.”_

**If your CLI module is an _executable_ CLI** (separate process), the computation itself shouldn’t block the main thread, but something else in your setup might be (e.g., synchronous I/O waiting, a blocking call in the module widget, etc.).

### Why UI Interaction “Fixes” It

When you click in the Python console, switch modules, or perform any UI interaction, Qt processes pending events as part of handling that interaction. This finally allows the queued `QTimer::singleShot` to fire → `ProcessModified()` runs → `Modified()` is called → `StatusModifiedEvent` fires.

### Solutions

**Option 1: Convert to an executable CLI module** (recommended for heavy computation)

Executable CLI modules run as a completely separate process, so they don’t block Slicer’s main thread or the Qt event loop. This is the cleanest solution for torch/cv2 pipelines.

**Option 2: If you must use a scripted CLI, call `slicer.app.processEvents()` periodically**

Insert calls to process the event loop during your pipeline:

```auto
import slicer

def run_long_pipeline():
    step1_result = step1()
    slicer.app.processEvents() # let Qt process pending events
    
    step2_result = step2(step1_result)
    slicer.app.processEvents()
    
    # ... etc

```

This is a workaround, not ideal for production, but it unblocks the event loop during execution.

**Option 3: Use `slicer.cli.run()` with `wait_for_completion=False` and an observer**

If you’re launching the CLI from Python, the standard pattern is:

```auto
cliNode = slicer.cli.run(module, parameters, wait_for_completion=False)
cliNode.AddObserver('ModifiedEvent', onCLIStatusChanged)

```

But this still requires the event loop to be spinning in the main thread.

**Option 4: Use QProcess or subprocess to run the heavy pipeline externally**

Wrap your torch/cv2 pipeline as a standalone script that runs in a separate process via `QProcess` or Python’s `subprocess`, keeping the main thread free.

### Relevant Discourse Threads

- [Trigger event when CLI run has completed](https://discourse.slicer.org/t/trigger-event-when-cli-run-has-completed/3774) — the thread where this exact issue is discussed (including your recent posts)
- [Running scripted-cli prevents QProcess from finishing](https://discourse.slicer.org/t/running-scripted-cli-prevents-qprocess-from-finishing/26534) — confirms scripted CLIs block the main thread
- [Problems listening to vtkMRMLCommandLineNode events](https://discourse.slicer.org/t/problems-listening-to-vtkmrmlcommandlinenode-events-from-loadable-module/2711) — related observer issues

### TL;DR

Your heavy Python pipeline (torch + cv2) is blocking Slicer’s main thread via the GIL, which prevents the Qt event loop from processing the `QTimer::singleShot` callbacks that deliver `StatusModifiedEvent`. `time.sleep()` works because it releases the GIL. The fix is to either convert to an executable CLI module or ensure the main thread’s event loop can spin during execution.  
`</ai>`

---

_[View the full topic](https://discourse.slicer.org/t/trigger-event-when-cli-run-has-completed/3774)._
