Run a Graphical Model from Python in PyQGIS
The Processing modeller lets somebody build a workflow by dragging boxes, and the result is an algorithm like any other — which means Python can run it, schedule it, loop it over a folder and wrap it in error handling. That combination is genuinely useful: the analyst who understands the workflow maintains the model, and the developer who understands automation drives it.
This recipe belongs to Chaining Processing Algorithms in PyQGIS. It covers running an installed model by id, loading a .model3 file that is not installed, discovering its parameter names without opening the modeller, and the trade-off against writing the chain in Python.
Prerequisites
- QGIS 3.34 LTR (bundled Python 3.12) or newer with Processing initialised.
- A model saved from the Processing modeller as a
.model3file, either installed in the profile or somewhere readable. - The algorithms the model uses must be available — a model built with SAGA steps needs the SAGA provider on the machine running it.
Run an installed model
A model saved into the user profile is registered automatically and has an id in the model namespace.
import processing
from qgis.core import QgsApplication
registry = QgsApplication.processingRegistry()
for alg in registry.algorithms():
if alg.provider().id() == "model":
print(alg.id(), "|", alg.displayName())
Breakdown: Listing the registry is the reliable way to find the id, because the id is derived from the model's name at save time and is not always what the display name suggests — spaces, capitals and punctuation are normalised. Running this once and pasting the exact id into the script beats guessing. Models saved into a project rather than the profile appear under the project provider instead.
With the id in hand it is an ordinary call:
result = processing.run("model:catchment_summary", {
"dem": "/data/dem_27700.tif",
"catchments": "/data/catchments.gpkg",
"native:zonalstatisticsfb_1:summary": "/data/output/summary.gpkg",
})
Breakdown: Input parameter names are whatever the model author typed in the modeller's parameter definitions, lower-cased and normalised — hence dem rather than INPUT. Output names are uglier: the modeller composes them from the algorithm id, an index and the output name the author gave. They are stable as long as the model is not restructured, and they are the reason a script driving a model should be re-checked whenever the model changes.
Discover the parameters without opening the modeller
Rather than reading the ids off a diagram, ask the algorithm.
alg = QgsApplication.processingRegistry().algorithmById("model:catchment_summary")
if alg is None:
raise LookupError("model not registered — check the id and the profile")
for param in alg.parameterDefinitions():
print(f"in {param.name():40} {type(param).__name__}")
for output in alg.outputDefinitions():
print(f"out {output.name():40} {type(output).__name__}")
Breakdown: parameterDefinitions() and outputDefinitions() give the exact strings the parameter dictionary needs, plus the type each expects — which distinguishes a QgsProcessingParameterFeatureSource that accepts a layer or a path from a QgsProcessingParameterNumber that does not. algorithmById() returning None is the usual symptom of a model that is present as a file but not registered, which happens when it was dropped into the wrong directory or added after QGIS started.
Load a model that is not installed
For a model kept in a repository alongside the script — the arrangement that makes it reviewable — load it directly.
from qgis.core import QgsProcessingModelAlgorithm, QgsProcessingContext, QgsProcessingFeedback
model = QgsProcessingModelAlgorithm()
if not model.fromFile("/data/models/catchment_summary.model3"):
raise RuntimeError("could not read the model file")
context = QgsProcessingContext()
feedback = QgsProcessingFeedback()
results, ok = model.run(
{"dem": "/data/dem_27700.tif", "catchments": "/data/catchments.gpkg"},
context, feedback,
)
if not ok:
raise RuntimeError("model run failed")
Breakdown: fromFile() returns a boolean rather than raising, and a False usually means a version mismatch — a model saved by a newer QGIS can fail to load in an older one. run() on the algorithm object takes an explicit context and feedback and returns a (results, ok) tuple, unlike processing.run() which raises. The context matters more here than usual: temporary outputs are owned by it, so letting it go out of scope while the results are still needed loses them.
Registering the loaded model instead makes it available by id for the rest of the session, which is tidier when several scripts share it:
QgsApplication.processingRegistry().providerById("model").refreshAlgorithms()
Breakdown: Refreshing the model provider re-scans the models directory, which picks up a file copied into place after startup. It is the programmatic equivalent of the modeller's reload, and worth calling in a plugin that ships models of its own.
Keeping a model and its driver in step
The awkward part of this arrangement is that a model's output parameter ids are generated, and restructuring the model changes them. A script written against yesterday's ids fails today with an error that names a string nobody chose.
The defence is a validation step that runs before the work does.
from qgis.core import QgsApplication
EXPECTED_INPUTS = {"dem", "catchments"}
EXPECTED_OUTPUTS = {"native:zonalstatisticsfb_1:summary"}
def check_model(model_id):
alg = QgsApplication.processingRegistry().algorithmById(model_id)
if alg is None:
raise LookupError(f"{model_id} is not registered")
inputs = {p.name() for p in alg.parameterDefinitions()}
outputs = {o.name() for o in alg.outputDefinitions()}
if not EXPECTED_INPUTS <= inputs:
raise RuntimeError(f"model inputs changed: expected {EXPECTED_INPUTS}, found {inputs}")
if not EXPECTED_OUTPUTS <= outputs:
raise RuntimeError(f"model outputs changed: expected {EXPECTED_OUTPUTS}, found {outputs}")
return alg
Breakdown: Using subset comparison rather than equality lets the model gain parameters without breaking the driver, which is the common and harmless change; it still catches a rename or a removal, which are the breaking ones. Raising with both the expected and the actual sets means the fix is visible in the error message rather than requiring somebody to open the modeller. Calling this once at the top of a scheduled job converts a mid-run failure into a startup failure, which is far cheaper when the run takes an hour.
Pairing the check with a note in the model's own description — "driven by nightly_catchments.py; renaming outputs will break it" — closes the loop from the other direction, so the analyst editing the model has some warning that something depends on it.
Loop a model over many inputs
The combination that justifies the whole approach: the model holds the analysis, the script holds the iteration.
from pathlib import Path
import processing
feedback = QgsProcessingFeedback()
failures = []
for dem in sorted(Path("/data/tiles").glob("*.tif")):
out = Path("/data/output") / f"{dem.stem}_summary.gpkg"
try:
processing.run("model:catchment_summary", {
"dem": str(dem),
"catchments": "/data/catchments.gpkg",
"native:zonalstatisticsfb_1:summary": str(out),
}, feedback=feedback)
except Exception as exc:
failures.append((dem.name, str(exc)))
if failures:
print(f"{len(failures)} tile(s) failed:")
for name, message in failures:
print(f" {name}: {message}")
Breakdown: Collecting failures rather than stopping on the first is right for a tile sweep, where one corrupt input should not abandon two hundred good ones — but the summary at the end is what stops the failures being invisible. Catching broad Exception is defensible in this specific shape because the loop's purpose is resilience; the printed message preserves what went wrong. Pairing this with the feedback object gives warnings as well as failures.
Model or Python
Models win when the workflow is linear, the person who owns it does not write Python, and it needs to appear in the toolbox for interactive use. They also get batch mode and the qgis_process runner for free.
Python wins the moment control flow appears — a loop, a conditional, a retry, anything that depends on a value computed halfway through. It also wins on review: a .model3 is XML that diffs unreadably, while a chain of processing.run() calls is a diff anybody can read.
The productive arrangement is usually both: keep the analysis in a model so the domain expert can adjust it, and drive it from a script that handles inputs, iteration, errors and outputs.
QGIS version compatibility
The examples target QGIS 3.34 LTR (Python 3.12).
| QGIS version | Python | Notes |
|---|---|---|
| 3.16 LTR | 3.7 | .model3 format and model: provider namespace in use. |
| 3.22 LTR | 3.9 | Models can be embedded in a project and appear under the project provider. |
| 3.28 LTR | 3.9 | Model output parameter naming stabilised. |
| 3.34 LTR | 3.12 | Baseline for this page. |
| 3.40+ | 3.12 | Modeller gains conditional branches, narrowing the gap with scripted chains. |
Troubleshooting
algorithmById()returns None. The model is not registered. Check the profile directory and callrefreshAlgorithms().- A parameter name is rejected. Names are normalised from the modeller's labels. List
parameterDefinitions()rather than guessing. fromFile()returns False. The model was saved by a newer QGIS, or the file is corrupt. Check the version it was authored in.- The model runs but writes nothing. The output parameter id is wrong, so the result went to a temporary file. Compare against
outputDefinitions(). - It works interactively and fails in a script. A provider the model depends on is not initialised headlessly. Initialise Processing fully.
- Results differ from the modeller run. The script's transform context differs from the project's. Pass the project.
Conclusion
List the registry to get the exact model id, ask the algorithm for its parameter names rather than reading them off a diagram, and drive the model from a script that owns the iteration and the error handling. Keep the analysis in the model where a domain expert can maintain it, and everything with control flow in Python.
Frequently Asked Questions
Where are models stored?
In processing/models/ inside the active user profile directory, which QgsApplication.qgisSettingsDirPath() reports. Models embedded in a project live in the project file instead.
Can a plugin ship models? Yes — install them into the profile's models directory on first run, or add a custom provider. Refreshing the model provider afterwards makes them available without a restart.
Can I convert a model into Python? The modeller offers an export to a Processing script, which produces a class-based algorithm rather than a flat chain. It is a reasonable starting point when a model has outgrown the modeller — see writing a custom processing algorithm.
Do models work with qgis_process?
Yes, by id, exactly like a native algorithm. That is often the simplest way to schedule one without writing any Python at all.