The QGIS Processing Framework Explained
Problem statement
The Processing Toolbox holds well over a thousand entries, contributed by five or six different engines, and from Python they all reduce to one call:
processing.run("native:buffer", {"INPUT": layer, "DISTANCE": 25, "OUTPUT": "TEMPORARY_OUTPUT"})
That uniformity is not an accident, and understanding the machinery behind it answers most of the questions people have when they start automating QGIS:
- why an algorithm id has a prefix, and why
qgis:andnative:both exist - why
TEMPORARY_OUTPUTsometimes gives you a layer and sometimes a path - why the algorithm works in the GUI and reports "not found" in a script
- how GRASS and SAGA algorithms appear alongside QGIS's own
- what the Modeler is actually producing, and why it can be run headless
Processing is a plugin-based framework: a registry, a parameter system and a runner. Everything else β the Toolbox, the Modeler, qgis_process, batch mode β is a front end onto it.
Quick answer
Five concepts explain the whole framework:
- Providers contribute algorithms and own an id prefix:
native:,qgis:,gdal:,grass:,saga:,model:,script: - Algorithms declare typed parameters and typed outputs β that declaration is what generates the dialog, the CLI help and the validation
- A context carries the project, CRS settings and temporary layer store; feedback carries progress and messages
processing.run()validates parameters, executes, and returns a results dictionary- The registry is empty until
Processing.initialize()runs β which the GUI does for you and a script must do itself
from qgis.core import QgsApplication
import processing
registry = QgsApplication.processingRegistry()
print("providers :", [p.id() for p in registry.providers()])
print("algorithms:", len(registry.algorithms()))
alg = registry.algorithmById("native:buffer")
print(alg.displayName(), "β", [p.name() for p in alg.parameterDefinitions()])
Asking the registry beats guessing every time: it reflects your installation, not the documentation for some other version.
The shape of the framework
Step-by-step solution
Providers: where algorithms come from
from qgis.core import QgsApplication
for provider in QgsApplication.processingRegistry().providers():
algorithms = provider.algorithms()
print(f"{provider.id():12} {len(algorithms):>4} algorithms {provider.name()}")
native ~370 algorithms QGIS (native c++)
qgis ~90 algorithms QGIS
gdal ~60 algorithms GDAL
3d ~5 algorithms QGIS (3D)
grass ~300 algorithms GRASS
Each provider is a plugin that registers its algorithms under a prefix. native: is the C++ implementation and is the fastest; qgis: is the older Python one, with some ids kept as aliases; gdal: wraps the GDAL command-line tools; grass: and saga: shell out to those packages and require them to be installed.
That model is why a missing algorithm is usually a missing provider β the algorithm exists, but nothing registered it in this process.
Algorithms declare their own interface
An algorithm is a class that describes its parameters and outputs. Everything else is generated from that description.
import processing
processing.algorithmHelp("native:buffer")
Buffer (native:buffer)
Arguments
---------
INPUT: Input layer Argument type: source
DISTANCE: Distance Argument type: distance
SEGMENTS: Segments Argument type: number
END_CAP_STYLE: End cap style
Argument type: enum
Available values: 0: Round 1: Flat 2: Square
DISSOLVE: Dissolve result Argument type: boolean
OUTPUT: Buffered Argument type: sink
The same declaration produces the Toolbox dialog, the batch-mode table, the qgis_process help output, and the validation that rejects a bad parameter before anything runs. Writing your own algorithm means writing that declaration β which is why a script algorithm gets a dialog for free.
Parameter types, and what they accept
import processing
from qgis.core import QgsVectorLayer
layer = QgsVectorLayer("data/parcels.gpkg|layername=parcels", "parcels", "ogr")
processing.run("native:buffer", {
"INPUT": layer, # a source: layer object, path, or path|layername=
"DISTANCE": 25, # a distance, in the layer's units
"SEGMENTS": 8, # a number
"END_CAP_STYLE": 0, # an enum β the integer, not the label
"DISSOLVE": False, # a boolean
"OUTPUT": "data/out/buffered.gpkg", # a sink: a path, memory:, or TEMPORARY_OUTPUT
})
The types worth knowing: source (an input layer), sink (an output layer), enum (an integer), distance and number, field (a column name), extent (xmin,xmax,ymin,ymax[CRS]), crs, matrix, and multiple layers. A source accepts a QgsVectorLayer, a file path, or a URI string β which is what lets the same call work in a script and in the Modeler.
Outputs: files, memory, or the context
import processing
from qgis.core import QgsProcessingContext, QgsProcessingFeedback
context = QgsProcessingContext() # keep this alive
feedback = QgsProcessingFeedback()
# a real file: survives the run
processing.run("native:buffer", {"INPUT": layer, "DISTANCE": 25,
"OUTPUT": "data/out/buffered.gpkg"})
# temporary: lives in the context, ideal for chaining
result = processing.run("native:buffer",
{"INPUT": layer, "DISTANCE": 25, "OUTPUT": "TEMPORARY_OUTPUT"},
context=context, feedback=feedback)
out = context.getMapLayer(result["OUTPUT"])
print(type(out), out.featureCount() if out else "not in context")
TEMPORARY_OUTPUT is the reason chained algorithms are fast: intermediate results stay in memory in the context rather than being written and re-read. It is also the reason a result "disappears" if the context is garbage-collected β the layer belonged to it.
Context and feedback: the two objects to pass explicitly
from qgis.core import (QgsProcessingContext, QgsProcessingFeedback,
QgsProject, QgsCoordinateTransformContext)
context = QgsProcessingContext()
context.setProject(QgsProject.instance()) # so @project variables resolve
context.setInvalidGeometryCheck(
QgsFeatureRequest.GeometryAbortOnInvalid) # or SkipInvalid / NoCheck
class Verbose(QgsProcessingFeedback):
def pushInfo(self, info): print("INFO :", info)
def pushWarning(self, warning): print("WARN :", warning)
def reportError(self, error, fatalError=False): print("ERROR:", error)
def setProgress(self, progress): pass
result = processing.run("native:clip", params, context=context, feedback=Verbose())
Without a feedback object, the messages an algorithm emits β skipped features, invalid geometries, CRS notes β go nowhere. That is the usual reason an empty result seems to have no explanation.
The invalid-geometry policy
from qgis.core import QgsProcessingContext, QgsFeatureRequest
context = QgsProcessingContext()
# abort the whole run on the first invalid geometry
context.setInvalidGeometryCheck(QgsFeatureRequest.GeometryAbortOnInvalid)
# skip invalid features and carry on (the GUI default)
context.setInvalidGeometryCheck(QgsFeatureRequest.GeometrySkipInvalid)
# do not check at all β fastest, and you own the consequences
context.setInvalidGeometryCheck(QgsFeatureRequest.GeometryNoCheck)
This single setting explains a class of "the GUI and my script disagree" reports: the GUI skips invalid input features by default, so a script with a different policy can produce a different count from the same data.
Running algorithms from every front end
# Python API
import processing
processing.run("native:buffer", params)
# with the results loaded into the project (GUI only)
processing.runAndLoadResults("native:buffer", params)
# command line β same registry, same parameters
qgis_process run native:buffer -- INPUT=parcels.gpkg DISTANCE=25 OUTPUT=out.gpkg
# a saved model is just another algorithm
processing.run("model:parcel_cleanup", {"INPUT": "parcels.gpkg", "OUTPUT": "clean.gpkg"})
The uniformity is the payoff: anything the Toolbox can run, a script can run, the CLI can run, and a model can contain β because all four resolve the same id against the same registry.
Code examples
Example 1: explore the registry properly
from qgis.core import QgsApplication
import csv
def dump_algorithms(path="algorithms.csv"):
registry = QgsApplication.processingRegistry()
with open(path, "w", newline="", encoding="utf-8") as fh:
writer = csv.writer(fh)
writer.writerow(["id", "display_name", "provider", "group", "parameters", "outputs"])
for alg in sorted(registry.algorithms(), key=lambda a: a.id()):
writer.writerow([
alg.id(), alg.displayName(), alg.provider().id(), alg.group(),
"|".join(p.name() for p in alg.parameterDefinitions()),
"|".join(o.name() for o in alg.outputDefinitions()),
])
print(f"wrote {path}")
def find(term: str):
registry = QgsApplication.processingRegistry()
for alg in registry.algorithms():
if term.lower() in alg.id().lower() or term.lower() in alg.displayName().lower():
print(f"{alg.id():45} {alg.displayName()}")
find("dissolve")
dump_algorithms()
Example 2: inspect parameters programmatically
from qgis.core import (QgsApplication, QgsProcessingParameterEnum,
QgsProcessingParameterNumber, QgsProcessingParameterField)
def describe(alg_id: str) -> None:
alg = QgsApplication.processingRegistry().algorithmById(alg_id)
if alg is None:
raise ValueError(f"{alg_id} is not registered")
print(f"{alg.displayName()} ({alg.id()})")
for param in alg.parameterDefinitions():
line = f" {param.name():22} {type(param).__name__.replace('QgsProcessingParameter', ''):14}"
if isinstance(param, QgsProcessingParameterEnum):
line += " options: " + ", ".join(f"{i}={v}" for i, v in enumerate(param.options()))
elif isinstance(param, QgsProcessingParameterNumber):
line += f" default={param.defaultValue()}"
elif isinstance(param, QgsProcessingParameterField):
line += f" of layer parameter {param.parentLayerParameterName()}"
if param.flags() & param.FlagOptional:
line += " [optional]"
print(line)
describe("native:joinattributesbylocation")
This is algorithmHelp() with structure, and it is what you want when generating configuration or validating a model programmatically.
Example 3: chain algorithms through one context
import processing
from qgis.core import QgsProcessingContext, QgsProcessingFeedback
def chain(source, steps, final_output):
"""Run a list of (alg_id, params) with temporary outputs between them."""
context, feedback = QgsProcessingContext(), QgsProcessingFeedback()
current = source
for i, (alg_id, params) in enumerate(steps):
is_last = i == len(steps) - 1
run_params = {**params, "INPUT": current,
"OUTPUT": final_output if is_last else "TEMPORARY_OUTPUT"}
result = processing.run(alg_id, run_params, context=context, feedback=feedback)
current = result["OUTPUT"]
print(f"{i+1}. {alg_id} β {'file' if is_last else 'memory'}")
return current, context # return the context so temporaries stay alive
output, context = chain(
"data/raw/parcels.gpkg",
[("native:fixgeometries", {}),
("native:reprojectlayer", {"TARGET_CRS": "EPSG:27700"}),
("native:buffer", {"DISTANCE": 25, "SEGMENTS": 8, "DISSOLVE": False}),
("native:dissolve", {"FIELD": ["class"]})],
"data/out/parcels_zones.gpkg",
)
Returning the context alongside the result is the detail that prevents temporaries vanishing mid-chain.
Example 4: initialise Processing correctly in a script
import os, sys
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from qgis.core import QgsApplication
def boot(prefix="/usr", plugins="/usr/share/qgis/python/plugins"):
QgsApplication.setPrefixPath(prefix, True)
app = QgsApplication([], False)
app.initQgis() # 1. providers, CRS database
if plugins not in sys.path:
sys.path.append(plugins)
from processing.core.Processing import Processing
Processing.initialize() # 2. THEN the registry
n = len(QgsApplication.processingRegistry().algorithms())
if not n:
raise RuntimeError("Processing registry is empty after initialize()")
print(f"{n} algorithms available")
return app
app = boot()
try:
import processing
print(processing.run("native:buffer", {
"INPUT": "data/raw/parcels.gpkg", "DISTANCE": 25,
"OUTPUT": "data/out/buffered.gpkg"})["OUTPUT"])
finally:
from qgis.core import QgsProject
QgsProject.instance().clear()
app.exitQgis()
The order β initQgis() before Processing.initialize() β is the single most common mistake in headless PyQGIS, and it produces an empty registry rather than an error.
Explanation
Processing began life as the SEXTANTE plugin and was absorbed into QGIS as its general-purpose analysis framework. Its central design decision is that an algorithm declares its interface rather than implementing one, and everything else follows from that.
Because parameters are typed objects with names, defaults, ranges and optionality, one description generates every front end. The Toolbox builds a dialog from it. Batch mode builds a table. qgis_process help prints it. The Modeler uses it to know which outputs can feed which inputs. And processing.run() uses it to validate your dictionary before anything executes β which is why a wrong parameter name fails immediately with a clear message rather than halfway through.
The provider model is what makes a thousand algorithms possible without QGIS implementing them. GRASS and SAGA providers shell out to those packages, translating parameters in and results back; the GDAL provider builds ogr2ogr and gdalwarp command lines; native: calls straight into C++. From the caller's side they are identical, which is a genuinely impressive abstraction and also the reason availability varies between installations: no GRASS on the machine, no grass: algorithms in the registry.
Context and feedback are the two objects that carry everything an algorithm needs that is not a parameter. The context holds the project, the transform context, the invalid-geometry policy and β importantly β a temporary layer store, which is what TEMPORARY_OUTPUT writes into. Feedback carries progress and messages back out. Passing your own instances of both is what turns a script from a black box into something you can watch and control.
Finally, the registry's emptiness at start-up explains the most common headless failure. Inside QGIS, the application and Processing were initialised long before your code ran. In a standalone script, QgsApplication.initQgis() sets up providers and the CRS database, and only then can Processing.initialize() populate the algorithm registry. Reverse the order and you get a registry with nothing in it β and the error you eventually see is "algorithm not found", pointing at the wrong thing entirely.
Edge cases or notes
native:beatsqgis:where both exist: The C++ implementations are faster; someqgis:ids survive only as aliases.- Enum parameters take integers: The labels in the dialog are display text.
algorithmHelp()lists the mapping. TEMPORARY_OUTPUTneeds a living context: Keep a reference, or fetch the layer before the context goes out of scope.runAndLoadResultsis GUI-only: In a headless script there is no canvas to load into.- Invalid-geometry policy differs by front end: The GUI skips invalid features by default; set the policy explicitly in scripts for reproducible counts.
- Provider availability is per installation: Check
registry.providers()rather than assuming GRASS or SAGA exist. - Models and scripts are algorithms too: They register under
model:andscript:once the profile is loaded, which is why a headless run may need the.model3file path instead.
Internal links
- How to Run QGIS Processing Algorithms from Python
- QGIS Processing "Algorithm Not Found" Error in Python: How to Fix It
- How to Build a QGIS Processing Model and Run It from Python
- How to Write a Custom QGIS Processing Script Algorithm in Python
- How to Automate QGIS from the Command Line with qgis_process
- PyQGIS Processing Output Is Empty: How to Fix It
FAQ
What is the difference between native: and qgis: algorithms?
native: are the C++ implementations and are faster. qgis: are the older Python ones; many were ported and their ids kept as aliases. Prefer native: and confirm against your own registry.
Why does my script say the algorithm is not found?
Almost always because Processing.initialize() has not run, or ran before QgsApplication.initQgis(). The registry is empty until both have happened in that order.
What does TEMPORARY_OUTPUT actually create?
A layer in the QgsProcessingContext, held in memory. Retrieve it with context.getMapLayer(result["OUTPUT"]), and keep the context alive or the layer goes with it.
How do I find an algorithm's parameter names?
processing.algorithmHelp("id"), qgis_process help id, or iterate alg.parameterDefinitions() for structured access. The Processing History panel also records the exact call the GUI made.
Why do GRASS or SAGA algorithms not appear?
Their providers are separate plugins that need those packages installed and registered. In a headless environment you usually have to add the provider explicitly after Processing.initialize().
What is the context for?
It carries the project, the coordinate transform context, the invalid-geometry policy and the temporary layer store. Passing your own gives you control over all four and keeps temporary outputs alive.
Can I write my own algorithm?
Yes β subclass QgsProcessingAlgorithm, declare parameters and outputs, and implement processAlgorithm. It then appears in the Toolbox, in batch mode, in the Modeler and in qgis_process with no extra work.