Chain Dependent QgsTasks in PyQGIS
Real plugin jobs are rarely one step. A sync job downloads data, validates it, then writes it into a layer; an analysis clips inputs, runs three independent calculations, then combines the results. Running each step as its own background task keeps QGIS responsive and shows progress, but the steps have an order: validation cannot start before the download finishes, and the combine step needs all three calculations. QgsTask supports this directly with subtasks and dependencies, so the task manager runs work in the right order, in parallel where possible, and cancels the rest when one step fails.
This recipe belongs to Background Tasks & Plugin Performance and builds on running a background task with QgsTask. It chains tasks serially and in parallel, passes results between steps, protects layers that tasks depend on, and handles cancellation.
Prerequisites
- QGIS 3.34 LTR or newer, or the QGIS 4 series.
- Familiarity with subclassing
QgsTaskand itsrunandfinishedmethods.
Add serial subtasks
A task can own subtasks. With ParentDependsOnSubTask, the parent's own run waits until its subtasks complete; serial order between subtasks is expressed with dependencies.
from qgis.core import QgsApplication, QgsTask, QgsMessageLog, Qgis
class Step(QgsTask):
def __init__(self, name, work, results):
super().__init__(name, QgsTask.CanCancel)
self.work, self.results = work, results
self.error = None
def run(self):
try:
self.results[self.description()] = self.work(self, self.results)
return not self.isCanceled()
except Exception as e:
self.error = e
return False
def finished(self, ok):
level = Qgis.Info if ok else Qgis.Warning
QgsMessageLog.logMessage(f"{self.description()}: {'ok' if ok else self.error}", "Chain", level)
results = {}
download = Step("download", lambda t, r: "/tmp/input.gpkg", results)
validate = Step("validate", lambda t, r: f"checked {r['download']}", results)
load = Step("load", lambda t, r: f"loaded after {r['validate']}", results)
parent = QgsTask.fromFunction("Sync job", lambda task: results)
parent.addSubTask(download, [], QgsTask.ParentDependsOnSubTask)
parent.addSubTask(validate, [download], QgsTask.ParentDependsOnSubTask)
parent.addSubTask(load, [validate], QgsTask.ParentDependsOnSubTask)
QgsApplication.taskManager().addTask(parent)
Breakdown: addSubTask(task, dependencies, type) takes the list of tasks this one must wait for. validate depends on download, and load on validate, so they run in sequence even though the task manager could run them in parallel. ParentDependsOnSubTask makes the parent finish only after its subtasks — and fail if any of them fails. Results pass through a shared dictionary keyed by step name; each step reads what earlier steps wrote. Only add the parent to the task manager; subtasks are scheduled with it.
Run independent steps in parallel
Steps with no dependency on each other run at the same time on the task manager's thread pool. Give them the same upstream dependency and a downstream task that depends on all of them.
results = {}
clip_a = Step("clip A", lambda t, r: "a.gpkg", results)
clip_b = Step("clip B", lambda t, r: "b.gpkg", results)
clip_c = Step("clip C", lambda t, r: "c.gpkg", results)
combine = Step("combine", lambda t, r: [r["clip A"], r["clip B"], r["clip C"]], results)
download = Step("download", lambda t, r: "/tmp/input.gpkg", results)
job = QgsTask.fromFunction("Analysis", lambda task: results)
job.addSubTask(download, [], QgsTask.ParentDependsOnSubTask)
for clip in (clip_a, clip_b, clip_c):
job.addSubTask(clip, [download], QgsTask.ParentDependsOnSubTask)
job.addSubTask(combine, [clip_a, clip_b, clip_c], QgsTask.ParentDependsOnSubTask)
QgsApplication.taskManager().addTask(job)
Breakdown: The three clips depend only on the download, so the task manager starts them together once it finishes. combine lists all three as dependencies and waits for the last. How many actually run at once depends on the thread pool size, which follows the number of cores. Writing to separate outputs is what makes parallel steps safe; two tasks writing the same GeoPackage must be serialised with a dependency.
Depend on layers, not just tasks
A task that reads a layer in the background would crash if the user removed that layer mid-run. Declaring layer dependencies makes the task manager cancel the task instead.
class ExportTask(QgsTask):
def __init__(self, layer, path):
super().__init__(f"Export {layer.name()}", QgsTask.CanCancel)
self.source = layer.dataProvider().dataSourceUri()
self.path = path
self.setDependentLayers([layer])
def run(self):
from qgis.core import QgsVectorLayer
src = QgsVectorLayer(self.source, "copy", "ogr")
n = src.featureCount()
for i, f in enumerate(src.getFeatures()):
if self.isCanceled():
return False
self.setProgress(100 * i / max(n, 1))
return True
Breakdown: setDependentLayers tells the task manager which layers the task needs; removing any of them cancels the task. Even so, the task opens its own copy of the source rather than iterating the project layer from a worker thread, which is the safe pattern for layer access off the main thread. The cancellation check in the loop lets the task stop promptly, and progress feeds the task bar in the status bar.
Handle failure and cancellation across the chain
When a subtask fails or is cancelled, its dependants never run and the parent reports failure. The parent's finished is the single place to tell the user what happened.
class Job(QgsTask):
def __init__(self, steps):
super().__init__("Analysis job", QgsTask.CanCancel)
self.steps = steps
def run(self):
return True # subtasks do the work
def finished(self, ok):
from qgis.utils import iface
if ok:
iface.messageBar().pushSuccess("Analysis", "All steps finished")
return
failed = [s.description() for s in self.steps if s.error]
reason = ", ".join(failed) or "cancelled"
iface.messageBar().pushWarning("Analysis", f"Stopped: {reason}")
Breakdown: The parent's run can do nothing when the subtasks carry the work; its finished runs on the main thread, so it can safely touch the interface. Collecting the error recorded on each step names the failure precisely. Cancelling the parent — from the task bar or with job.cancel() — cancels all its subtasks too, so a single cancel button covers the whole chain. Decide whether partial outputs should be deleted or kept for a retry.
Keep references alive
Tasks created in a function and not stored anywhere may be garbage collected by Python before they run, which crashes QGIS or silently drops the work.
class Plugin:
def start_job(self):
self.job = Job([]) # keep a reference on the plugin
QgsApplication.taskManager().addTask(self.job)
Breakdown: Storing the parent task on the plugin instance keeps it and its subtasks alive until they finish. Tasks created with QgsTask.fromFunction need the same care. This is the single most common cause of "my task never ran" reports.
Add the result to the map when the chain ends
Background steps must never add layers to the project themselves — the project belongs to the main thread. The parent's finished method, which runs on the main thread, is where the output of the last step becomes a map layer.
from qgis.core import QgsProject, QgsVectorLayer
class LoadingJob(Job):
def __init__(self, steps, results):
super().__init__(steps)
self.results = results
def finished(self, ok):
super().finished(ok)
if not ok:
return
for path in self.results.get("combine", []):
layer = QgsVectorLayer(path, path.rsplit("/", 1)[-1], "ogr")
if layer.isValid():
QgsProject.instance().addMapLayer(layer)
Breakdown: The worker steps only write files and record their paths in the shared results. Once every step has succeeded, finished opens those files as layers and adds them to the project, safely on the main thread. Checking isValid guards against a step that reported success but wrote an empty or unreadable file. The same pattern applies to updating a dock widget, refreshing the canvas or writing project entries: do it in finished, never in run.
Wait for a chain in a standalone script
In a script run outside the QGIS window, nothing processes events while the tasks run, so the script must wait for the task manager explicitly.
from qgis.core import QgsApplication
manager = QgsApplication.taskManager()
manager.addTask(job)
while manager.countActiveTasks() > 0:
QgsApplication.processEvents()
print("job finished with status", job.status())
Breakdown: countActiveTasks drops to zero once the parent and all its subtasks have completed, been cancelled or failed. Processing events in the loop lets finished handlers run, since they are delivered on the main thread. In headless scripts that only need the work done, calling each step's logic directly in order is often simpler than tasks at all; chaining tasks pays off inside the QGIS interface, where responsiveness matters.
QGIS version compatibility
addSubTask, the ParentDependsOnSubTask flag, setDependentLayers and the task manager's dependency handling are available on QGIS 3.34 LTR, 3.40 LTR and QGIS 4. In QGIS 4, enum names may also appear in scoped form; the classic names remain accepted.
Troubleshooting
- Tasks never start. The task object was garbage collected; keep a reference.
- Steps run out of order. A dependency is missing from
addSubTask. - QGIS crashes during a task. A project layer was read from the worker thread; open a copy from its source instead.
- The parent reports success despite a failing step. The subtask was added without
ParentDependsOnSubTask.
Conclusion
Model multi-step jobs as a parent task with subtasks, declare order with dependency lists, let independent steps run in parallel, pass results through a shared structure, declare layer dependencies, report outcomes in the parent's finished, and keep references to tasks until they complete.
Frequently Asked Questions
Can subtasks have their own subtasks? Yes; nesting works, though a flat list with dependencies is easier to follow.
How do I show overall progress? The parent's progress is derived from its subtasks in the task bar; you can also set it yourself.
Can a task depend on a task that is not its sibling? Dependencies are expressed between subtasks of the same parent, or between top-level tasks via the task manager.
Should I use Processing instead? For chains of Processing algorithms, a model or chained processing.run running in a task is often simpler.