The QGIS Processing Framework Explained

On this page

Problem statement

The Processing Toolbox holds hundreds of algorithms, contributed by half a dozen 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: and native: both exist
  • why TEMPORARY_OUTPUT sometimes gives you a layer and sometimes an id
  • why the algorithm works in the GUI and reports "not found" in a script
  • how GRASS 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. Everything below was run headless under QGIS 4.2 on Rutland's land use, schools and Land Registry parcels.

Quick answer

Five concepts explain the whole framework:

  1. Providers contribute algorithms and own an id prefix: native:, qgis:, gdal:, grass:, pdal:, 3d:, model:, script:
  2. Algorithms declare typed parameters and typed outputs β€” that declaration is what generates the dialog, the CLI help and the validation
  3. A context carries the project, the invalid-geometry policy and the temporary layer store; feedback carries progress and messages
  4. processing.run() validates parameters, executes, and returns a results dictionary
  5. The registry is empty until Processing.initialize() runs β€” which the GUI does for you and a script must do itself
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()])
providers : ['3d', 'gdal', 'grass', 'model', 'native', 'pdal', 'project', 'qgis', 'script']
algorithms: 772
Buffer -> ['INPUT', 'DISTANCE', 'SEGMENTS', 'END_CAP_STYLE', 'JOIN_STYLE', 'MITER_LIMIT', 'DISSOLVE', 'SEPARATE_DISJOINT', 'OUTPUT']

Asking the registry beats guessing every time: it reflects your installation, not the documentation for some other version.

The shape of the framework

The Processing framework from front ends down to parameters. four front ends, one registryToolbox Β· Modeler Β· CLI Β· Pythonfront endsprocessing registryid β†’ algorithmprovidersnative, qgis, gdal, grassalgorithmsdeclare parametersparameters + outputstyped, validatedOne declaration generates the dialog, the CLI help and the validation spatialworkflow.io
Four front ends, one registry β€” which is why the Toolbox, the Modeler and the CLI agree.

Providers, algorithms, parameters, outputs and context

What happens inside a processing.run call. from dictionary to output1resolve the algorithm idagainst the registry2validate the parameterstypes, ranges, files3prepare the contextproject, CRS, temp store4execute, reporting progressthrough feedback5return the results dictOUTPUT and friendsA wrong value fails before anything runs; a misspelt parameter name is silently ignored spatialworkflow.io
What happens between your dictionary and the output layer.

Providers: where algorithms come from

for provider in registry.providers():
    print(f"{provider.id():8} {len(provider.algorithms()):>4} algorithms  {provider.name()}")
3d          1 algorithms  QGIS (3D)
gdal       59 algorithms  GDAL
grass     307 algorithms  GRASS
model       0 algorithms  Models
native    350 algorithms  QGIS (native c++)
pdal       24 algorithms  QGIS (PDAL)
project     0 algorithms  Project models
qgis       31 algorithms  QGIS
script      0 algorithms  Scripts

Each provider is a plugin that registers its algorithms under a prefix. native: is the C++ implementation; qgis: is the Python one, and the ids of algorithms that were ported to C++ still resolve as aliases; gdal: builds command lines for the GDAL tools; grass: runs GRASS modules and was registered here because GRASS is installed. model: and script: are empty until a profile holds models or scripts.

That model is why a missing algorithm is usually a missing provider, or an id from another release β€” the algorithm-not-found guide diffs 3.44 against 4.2.

Algorithms declare their own interface

An algorithm is a class that describes its parameters and outputs. Everything else is generated from that description.

import contextlib, io
buffer_help = io.StringIO()
with contextlib.redirect_stdout(buffer_help):
    processing.algorithmHelp("native:buffer")
lines = buffer_help.getvalue().splitlines()
start = lines.index("END_CAP_STYLE: End cap style")
print("\n".join(lines[start:start + 8]))
END_CAP_STYLE: End cap style

	Parameter type:	QgsProcessingParameterEnum

	Available values:
		- 0: Round
		- 1: Flat
		- 2: Square

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:

for params in ({"INPUT": "data/rutland.gpkg|layername=schools", "DISTANCE": 25, "END_CAP_STYLE": "Flat",
                "OUTPUT": "TEMPORARY_OUTPUT"},
               {"INPUT": "data/rutland.gpkg|layername=schools", "DISTANCE": 25, "END_CAPSTYLE": 1,
                "OUTPUT": "TEMPORARY_OUTPUT"}):
    try:
        out = processing.run("native:buffer", params)["OUTPUT"]
        print("ran:", out.featureCount(), "features β€” the misspelt key was ignored")
    except Exception as exc:
        print(type(exc).__name__ + ":", " ".join(str(exc).split())[:100])
QgsProcessingException: Unable to execute algorithm Incorrect parameter value for END_CAP_STYLE
ran: 26 features β€” the misspelt key was ignored

An enum given as its label was refused before anything ran. A misspelt parameter name was not: END_CAPSTYLE is not a parameter of native:buffer, so it was ignored, and the buffer ran with the default cap. Validation checks the values of the parameters it knows about; it does not reject keys it has never heard of. 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

schools = QgsVectorLayer("data/rutland.gpkg|layername=schools", "schools", "ogr")
result = processing.run("native:buffer", {
    "INPUT": schools,                     # 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": "out/buffered.gpkg",        # a sink: a path, or TEMPORARY_OUTPUT
})
print(result)
{'OUTPUT': 'out/buffered.gpkg'}

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

from qgis.core import QgsProcessingContext, QgsProcessingFeedback

params = {"INPUT": schools, "DISTANCE": 25, "OUTPUT": "TEMPORARY_OUTPUT"}
plain = processing.run("native:buffer", params)
print("processing.run:                 ", type(plain["OUTPUT"]).__name__)

context = QgsProcessingContext()
feedback = QgsProcessingFeedback()
own = processing.run("native:buffer", params, context=context, feedback=feedback)
print("with your own context:          ", type(own["OUTPUT"]).__name__)

child = processing.run("native:buffer", params, context=context, feedback=feedback, is_child_algorithm=True)
held = context.getMapLayer(child["OUTPUT"])
print("is_child_algorithm=True:        ", type(child["OUTPUT"]).__name__, child["OUTPUT"][:17] + "...",
      "->", held.featureCount(), "features in the context")
processing.run:                  QgsVectorLayer
with your own context:           QgsVectorLayer
is_child_algorithm=True:         str Buffered_6a6e9319... -> 26 features in the context

That is the "sometimes a layer, sometimes an id" question answered, and passing a context is not what decides it. A temporary output is created in the context's layer store; by default processing.run then takes it out and hands you the layer object, whether the context was yours or its own. With is_child_algorithm=True β€” what a script algorithm or model uses for its inner steps β€” the layer stays in the context and you get its id, which is only good for as long as that context lives. Temporary outputs are the reason chained algorithms are fast: intermediate results stay in memory rather than being written and re-read.

Where a TEMPORARY_OUTPUT layer goes after processing.run, with and without is_child_algorithm. who owns a temporary outputalgorithm writesitinto the context's storeprocessing.runreturnspost-processing stepdefault: moved toyoua QgsVectorLayerchild algorithmis_child_algorithm=True:an idPassing your own context did not change the return type β€” the child-algorithm flag did spatialworkflow.io
The run above in four boxes: an id only means something while its context lives.

Context and feedback: the two objects to pass explicitly

from qgis.core import QgsFeatureRequest, QgsProject

class Verbose(QgsProcessingFeedback):
    def __init__(self):
        super().__init__()
        self.messages = []
    def pushInfo(self, info):                       self.messages.append(("INFO", info))
    def pushWarning(self, warning):                 self.messages.append(("WARN", warning))
    def reportError(self, error, fatalError=False): self.messages.append(("ERROR", error))

context = QgsProcessingContext()
context.setProject(QgsProject.instance())           # so @project variables resolve
listening = Verbose()
processing.run("native:clip", {"INPUT": "data/rutland.gpkg|layername=parcels",
                               "OVERLAY": "data/rutland.gpkg|layername=schools",
                               "OUTPUT": "TEMPORARY_OUTPUT"}, context=context, feedback=listening)
for level, text in listening.messages[:4]:
    print(f"{level:5} {text[:95]}")
INFO  Results: {'OUTPUT': 'Clipped_ac2cdc17_26b8_4eb9_a827_8b159a52b8e0'}

Without a feedback object, those messages β€” here, the algorithm's report of what it did, and in other runs skipped features or CRS notes β€” go to a default object nobody reads. That is the usual reason an empty result seems to have no explanation.

The invalid-geometry policy

The context also decides what happens when an input geometry is invalid. Rutland's parcels, OSM land use and buildings were all valid, so this test makes a bow-tie from one parcel's corners, joined in the wrong order so the outline crosses itself:

parcel = next(QgsVectorLayer("data/rutland.gpkg|layername=parcels", "p", "ogr").getFeatures())
box = parcel.geometry().boundingBox()                         # its corners, joined in the wrong order
ring = [QgsPointXY(box.xMinimum(), box.yMinimum()), QgsPointXY(box.xMaximum(), box.yMaximum()),
        QgsPointXY(box.xMaximum(), box.yMinimum()), QgsPointXY(box.xMinimum(), box.yMaximum()),
        QgsPointXY(box.xMinimum(), box.yMinimum())]
broken = QgsVectorLayer("Polygon?crs=EPSG:27700&field=id:integer", "broken", "memory")
feature = QgsFeature(broken.fields())
feature.setGeometry(QgsGeometry.fromPolygonXY([ring]))
feature["id"] = 1
broken.dataProvider().addFeatures([feature])
print("valid:", feature.geometry().isGeosValid())

def outcome(alg_id, context=None):
    params = {"INPUT": broken, "OUTPUT": "TEMPORARY_OUTPUT", **({"DISTANCE": 5} if alg_id == "native:buffer" else {})}
    try:
        run = processing.run(alg_id, params, context=context) if context else processing.run(alg_id, params)
        return f"{run['OUTPUT'].featureCount()} out"
    except Exception:
        return "stopped"

contexts = {"processing.run, no context": None}
for policy in (Qgis.InvalidGeometryCheck.NoCheck, Qgis.InvalidGeometryCheck.SkipInvalid,
               Qgis.InvalidGeometryCheck.AbortOnInvalid):
    contexts[f"own context, {policy.name}"] = QgsProcessingContext()
    contexts[f"own context, {policy.name}"].setInvalidGeometryCheck(policy)
print(f"{'':32}{'native:centroids':>18}{'native:buffer':>15}{'native:fixgeometries':>22}")
for label, ctx in contexts.items():
    print(f"{label:32}{outcome('native:centroids', ctx):>18}{outcome('native:buffer', ctx):>15}"
          f"{outcome('native:fixgeometries', ctx):>22}")
print("a new QgsProcessingContext() defaults to:", QgsProcessingContext().invalidGeometryCheck().name)
valid: False
                                  native:centroids  native:buffer  native:fixgeometries
processing.run, no context                 stopped          1 out                 1 out
own context, NoCheck                         1 out          1 out                 1 out
own context, SkipInvalid                     0 out          1 out                 1 out
own context, AbortOnInvalid                stopped          1 out                 1 out
a new QgsProcessingContext() defaults to: NoCheck

This setting explains a class of "the GUI and my script disagree" reports, and the direction is not the one usually assumed. processing.run() without a context builds one from the Processing settings, whose default β€” the same as the GUI's β€” is to stop on an invalid geometry. A QgsProcessingContext() you create yourself defaults to no check at all, so the same bow-tie that stops native:centroids in the GUI sails through a script that passes its own context. And the policy is not universal: native:buffer and native:fixgeometries produced their feature under every setting, because algorithms that can cope with invalid input switch the check off for themselves. Set the policy explicitly whenever you create a context, and do not assume it covers every algorithm.

What a self-intersecting polygon did to native:centroids and native:buffer under each invalid-geometry policy. one bow-tie, four policies, two algorithmshow the context was madenative:centroidsnative:bufferprocessing.run, no contextstopped1 outown context, NoCheck (default)1 out1 outown context, SkipInvalid0 out1 outown context, AbortOnInvalidstopped1 outbuffer switches the check off for itself β€” as fix geometries doesThe GUI stops on invalid input; a context you create yourself checks nothing spatialworkflow.io
The run above as a table: the default depends on who made the context, and not every algorithm consults it.

Running algorithms from every front end

QgsProject.instance().clear()
loaded = processing.runAndLoadResults("native:centroids", {"INPUT": schools, "OUTPUT": "TEMPORARY_OUTPUT"})
print("runAndLoadResults, headless:", [l.name() for l in QgsProject.instance().mapLayers().values()])

cli = subprocess.run(["qgis_process", "run", "native:centroids", "--", "INPUT=data/rutland.gpkg|layername=schools",
                      "OUTPUT=out/centroids_cli.gpkg"], capture_output=True, text=True, env=dict(os.environ))
print("qgis_process:", sorted({l for l in cli.stdout.splitlines() if l.startswith("OUTPUT")}))
runAndLoadResults, headless: ['Centroids']
qgis_process: ['OUTPUT:\tout/centroids_cli.gpkg']

runAndLoadResults is not GUI-only: headless, it added its result to QgsProject.instance(), where nothing will draw it. 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 of them resolve the same id against the same registry.

Exploring and chaining algorithms on Rutland's data

The data used on this page

Rutland's land use and schools from Geofabrik's OpenStreetMap extract of 1 January 2026 (Β© OpenStreetMap contributors, ODbL), and HM Land Registry's INSPIRE Index Polygons for the county (Β© Crown copyright and database rights, HM Land Registry; OGL), in one GeoPackage in British National Grid.

import os
import subprocess
import sys
from pathlib import Path

import requests
from osgeo import gdal

gdal.UseExceptions()
for folder in ("data", "out"):
    Path(folder).mkdir(exist_ok=True)
session = requests.Session()
if not Path("data/rutland.gpkg").exists():
    Path("data/rutland.shp.zip").write_bytes(session.get(
        "https://download.geofabrik.de/europe/united-kingdom/england/rutland-260101-free.shp.zip", timeout=300).content)
    osm = "/vsizip/data/rutland.shp.zip/"
    gdal.VectorTranslate("data/rutland.gpkg", osm + "gis_osm_landuse_a_free_1.shp", layerName="landuse",
                         dstSRS="EPSG:27700", reproject=True, selectFields=["fclass"])
    gdal.VectorTranslate("data/rutland.gpkg", osm + "gis_osm_pois_a_free_1.shp", accessMode="append",
                         layerName="schools", dstSRS="EPSG:27700", reproject=True,
                         SQLStatement="SELECT name FROM gis_osm_pois_a_free_1 WHERE fclass = 'school'")
    Path("data/Rutland.zip").write_bytes(session.get(
        "https://use-land-property-data.service.gov.uk/datasets/inspire/download/Rutland.zip", timeout=300).content)
    gdal.VectorTranslate("data/rutland.gpkg", "/vsizip/data/Rutland.zip/Land_Registry_Cadastral_Parcels.gml",
                         accessMode="append", layerName="parcels", SQLStatement="SELECT INSPIREID FROM PREDEFINED")

os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from qgis.core import Qgis, QgsApplication, QgsFeature, QgsGeometry, QgsPointXY, QgsVectorLayer
qgs = QgsApplication([], False)
qgs.initQgis()
print("before Processing.initialize():", len(QgsApplication.processingRegistry().algorithms()), "algorithms")
from processing.core.Processing import Processing
import processing
Processing.initialize()
print("after:", len(QgsApplication.processingRegistry().algorithms()), "| QGIS", Qgis.QGIS_VERSION.split("-")[0])
before Processing.initialize(): 0 algorithms
after: 772 | QGIS 4.2.2

Example 1: Explore the registry properly

import csv


def dump_algorithms(path="out/algorithms.csv"):
    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()),
            ])
    return sum(1 for _ in open(path)) - 1


def find(term: str):
    for alg in registry.algorithms():
        if term.lower() in alg.id().lower() or term.lower() in alg.displayName().lower():
            print(f"  {alg.id():28} {alg.displayName()}")


find("dissolve")
print(dump_algorithms(), "algorithms written to out/algorithms.csv")
  gdal:dissolve                Dissolve
  grass:v.dissolve             v.dissolve
  native:coverageunion         Dissolve coverage
  native:dissolve              Dissolve
772 algorithms written to out/algorithms.csv

Example 2: Inspect parameters programmatically

from qgis.core import QgsProcessingParameterEnum, QgsProcessingParameterField, QgsProcessingParameterNumber


def describe(alg_id: str) -> None:
    alg = registry.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():20} {type(param).__name__.replace('QgsProcessingParameter', ''):14}"
        if isinstance(param, QgsProcessingParameterEnum):
            line += " " + ", ".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" field of {param.parentLayerParameterName()}"
        if param.flags() & Qgis.ProcessingParameterFlag.Optional:
            line += "  [optional]"
        print(line[:108])


describe("native:joinattributesbylocation")
Join attributes by location (native:joinattributesbylocation)
  INPUT                FeatureSource 
  PREDICATE            Enum           0=intersect, 1=contain, 2=equal, 3=touch, 4=overlap, 5=are within, 6=c
  JOIN                 FeatureSource 
  JOIN_FIELDS          Field          field of JOIN  [optional]
  METHOD               Enum           0=Create separate feature for each matching feature (one-to-many), 1=T
  DISCARD_NONMATCHING  Boolean       
  PREFIX               String          [optional]
  OUTPUT               FeatureSink     [optional]
  NON_MATCHING         FeatureSink     [optional]

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

def chain(source, steps, final_output):
    """Run a list of (alg_id, params) with temporary outputs between them."""
    context, feedback = QgsProcessingContext(), QgsProcessingFeedback()
    context.setInvalidGeometryCheck(Qgis.InvalidGeometryCheck.AbortOnInvalid)
    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:24} -> {'file' if is_last else 'memory'}")

    return current, context      # the context holds the settings the chain ran with


output, chain_context = chain(
    "data/rutland.gpkg|layername=landuse",
    [("native:fixgeometries", {}),
     ("native:reprojectlayer", {"TARGET_CRS": "EPSG:27700"}),
     ("native:buffer", {"DISTANCE": 25, "SEGMENTS": 8, "DISSOLVE": False}),
     ("native:dissolve", {"FIELD": ["fclass"]})],
    "out/landuse_zones.gpkg",
)
print(output, "->", QgsVectorLayer(output, "zones", "ogr").featureCount(), "zones")
1. native:fixgeometries     -> memory
2. native:reprojectlayer    -> memory
3. native:buffer            -> memory
4. native:dissolve          -> file
out/landuse_zones.gpkg -> 19 zones

Each intermediate comes back as a layer object that the next step takes as its INPUT, so nothing touches the disk until the last step. Sharing one context is what makes every step run under the same settings β€” and setting its invalid-geometry policy is the detail that makes the chain behave like the GUI.

Example 4: Initialise Processing correctly in a script

Path("boot_and_buffer.py").write_text('''
import os, sys
os.environ.setdefault("QT_QPA_PLATFORM", "offscreen")
from qgis.core import QgsApplication, QgsProject


def boot(plugins="/usr/share/qgis/python/plugins"):
    app = QgsApplication([], False)
    app.initQgis()                                   # 1. the application
    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/rutland.gpkg|layername=schools", "DISTANCE": 25,
                                           "OUTPUT": "out/boot_buffered.gpkg"})["OUTPUT"])
finally:
    QgsProject.instance().clear()
    app.exitQgis()
''')
run = subprocess.run([sys.executable, "boot_and_buffer.py"], capture_output=True, text=True)
print("exit", run.returncode, "|", " | ".join(run.stdout.split("\n")[:2]))
exit 0 | 772 algorithms available | out/boot_buffered.gpkg

The order β€” initQgis() before Processing.initialize() β€” is the most common mistake in headless PyQGIS. Reversed, it raises nothing: it prints "Application path not initialized" dozens of times and leaves a registry that later reports "algorithm not found", pointing at the wrong thing entirely.

The page's QGIS session ends here:

QgsProject.instance().clear()
qgs.exitQgis()
print("QGIS closed")
QGIS closed

From SEXTANTE to a general framework

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.

Processing providers, their prefixes and what each requires. six providersimplementationrequiresnative:βœ“C++, 350 in 4.2always availableqgis:Python, 31Python supportgdal:GDAL command lines, 59GDAL tools + Pythongrass:!GRASS modules, 307GRASS installedmodel:your saved modelsthe profile, or a pathscript:your script algorithmsthe profileCounts from QGIS 4.2 here; a missing algorithm is usually a missing provider spatialworkflow.io
Six providers, one registry β€” and only native: is there without Python and without anything else installed.

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 the values in your dictionary before anything executes β€” though, as the misspelt key showed, not to reject names it does not know.

The provider model is what makes hundreds of algorithms possible without QGIS implementing them all. The GRASS provider runs GRASS modules, 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 β€” as long as you also give the context the policy you meant, since a new one checks nothing.

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 the page's own setup printed 0 algorithms after initQgis() and the full set only after Processing.initialize(). Get the order wrong and the error you eventually see is "algorithm not found", pointing at the wrong thing entirely.

Ids, enums and parameters that are silently ignored

  • native: and qgis: ids: algorithms ported to C++ keep answering to their old qgis: ids as aliases on newer releases; the new native: ids do not exist on older ones.
  • Enum parameters take integers: the labels in the dialog are display text, and a label was refused. algorithmHelp() lists the mapping.
  • Unknown parameter names are ignored: a typo in a key runs the algorithm with that parameter's default.
  • TEMPORARY_OUTPUT needs a living context: keep a reference, or fetch the layer before the context goes out of scope.
  • runAndLoadResults works headless: it adds the result to QgsProject.instance() β€” useful only if something reads the project afterwards.
  • Invalid-geometry policy differs by who made the context: processing.run() without one stops on invalid input, like the GUI; a bare QgsProcessingContext() checks nothing.
  • Provider availability is per installation: check registry.providers() rather than assuming GRASS exists.
  • Models and scripts are algorithms too: they register under model: and script: from the profile β€” and a standalone script has a profile of its own, which is why a headless run may need the .model3 file path instead.

FAQ

What is the difference between native: and qgis: algorithms?

native: are the C++ implementations; qgis: are Python ones. When an algorithm is ported, its old qgis: id keeps working as an alias on newer releases β€” but the new native: id does not exist on older ones. Use whichever the QGIS that runs the script has, and confirm against its 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. If you passed your own context, the result is an id: retrieve the layer with context.getMapLayer(result["OUTPUT"]) and keep the context alive. If you did not, processing.run returns the layer object itself.

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?

GRASS algorithms need GRASS installed; where it was, Processing.initialize() registered all 307 of them with no extra code. SAGA comes from a separate plugin, SAGA NextGen, which has to be installed and loaded.

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 β€” but a new context checks no geometries, where processing.run without one stops on invalid input, so set the policy you want.

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.