How to Run QGIS Processing Algorithms from Python

The Processing toolbox is the best part of QGIS and the least documented from Python. Behind every dialog is a string id and a dictionary, and once you can produce those two things reliably you can script anything the toolbox can do β€” including the GDAL, GRASS, and SAGA algorithms that arrive with it. The trick is not memorising parameters. It is knowing the four places QGIS will tell you what they are.

Problem statement

You want to replace a sequence of toolbox clicks with a script, and you hit the same wall each time:

  • You do not know the algorithm id. The dialog says "Buffer"; the call needs native:buffer, and there is also gdal:buffervectors and a legacy qgis: alias that behave differently.
  • You do not know the parameter names. DISTANCE or BUFFER_DISTANCE? INPUT or INPUT_LAYER? A wrong key is silently ignored or raises an unhelpful exception.
  • The algorithm "is not found". processing.run raises QgsProcessingException in a standalone script that works fine in the console.
  • The output is a string, and you wanted a layer. Or it is a layer and you wanted a file, or it is a temporary thing that vanished.
  • Errors are invisible. A run half-completes and the reason sits in a log panel you cannot see from a scheduled job.

The goal: a call you can copy from a real run, verify before executing, chain into the next algorithm, and log properly when it fails.

Quick answer

Every Processing call has the same three parts β€” an id, a parameter dictionary, and a results dictionary.

The anatomy of a processing.run call: algorithm id plus parameter dictionary in, results dictionary out.
One function, one dictionary in, one dictionary out β€” the whole toolbox reduces to this.
import processing

result = processing.run("native:buffer", {
    "INPUT": "data/parcels.gpkg|layername=parcels",
    "DISTANCE": 25,
    "SEGMENTS": 8,
    "END_CAP_STYLE": 0,
    "JOIN_STYLE": 0,
    "MITER_LIMIT": 2,
    "DISSOLVE": False,
    "OUTPUT": "out/parcels_buffer.gpkg",
})

print(result["OUTPUT"])   # 'out/parcels_buffer.gpkg'

To find the id and the parameter names for any algorithm, run it once from the toolbox and read Processing β†’ History, or ask QGIS directly:

processing.algorithmHelp("native:buffer")

In a standalone script the framework needs initialising first β€” see running PyQGIS headless and the PyQGIS overview.

Step-by-step solution

Find the algorithm id

Four routes, in the order you will actually use them.

Four ways to discover an algorithm id and its parameters: history log, algorithmHelp, the toolbox tooltip, and qgis_process.
Never guess a parameter name β€” every one of these four routes prints the truth.

The History panel. Run the algorithm once from the toolbox with real inputs, then open Processing β†’ History. QGIS records the exact processing.run(...) call it executed, id and full parameter dictionary included. Copy, paste, edit the paths. This is the single fastest route and it is correct by construction.

algorithmHelp. From the Python Console or a script:

import processing
processing.algorithmHelp("native:buffer")

It prints every parameter with its type, whether it is optional, and the accepted enum values β€” including the numbers behind END_CAP_STYLE and JOIN_STYLE, which are otherwise pure folklore.

Listing everything. When you know the word but not the id:

from qgis.core import QgsApplication

for alg in QgsApplication.processingRegistry().algorithms():
    if "buffer" in alg.displayName().lower():
        print(alg.id(), "β€”", alg.displayName())

The command line. qgis_process list prints every algorithm on the system, and qgis_process help native:buffer prints the same detail as algorithmHelp without starting Python at all.

Understand the id namespaces

The prefix before the colon is the provider, and it tells you what you are getting.

Prefix Provider Notes
native: QGIS core, C++ Fastest, best maintained β€” prefer these
qgis: QGIS core, legacy Python Many are aliases of a native: algorithm; some are unique
gdal: GDAL/OGR command wrappers Shell out to ogr2ogr, gdalwarp etc. β€” great for rasters and format conversion
grass: / saga: External providers Powerful, but only if that provider is installed and configured
model: Your saved Processing models See building a model and running it from Python
script: Your own script algorithms See writing a custom script algorithm

Automation should prefer native: where it exists. A grass: call in a scheduled job means the job now depends on GRASS being installed and correctly configured on that machine, which is a real deployment cost.

Pass inputs in whatever form you have

The INPUT parameter is forgiving in a genuinely useful way. All of these work:

processing.run("native:buffer", {"INPUT": "data/parcels.gpkg", ...})                     # a path
processing.run("native:buffer", {"INPUT": "data/parcels.gpkg|layername=parcels", ...})   # a path + layer
processing.run("native:buffer", {"INPUT": layer, ...})                                   # a QgsVectorLayer
processing.run("native:buffer", {"INPUT": previous["OUTPUT"], ...})                      # an earlier result

The |layername= suffix matters for multi-layer containers like GeoPackage: without it you get the first layer, which is rarely what you meant and never what you meant on the day it changes.

To operate on a subset without writing an intermediate file, wrap the layer in a QgsProcessingFeatureSourceDefinition with a selection, or filter first with native:extractbyexpression β€” the second is clearer and works headlessly, where there is no selection to speak of.

Choose where the output goes

Three output targets for a Processing algorithm: a temporary scratch layer, an in-memory layer, and a real file on disk.
Intermediate steps should not touch disk; final steps should not be temporary.
  • "TEMPORARY_OUTPUT" β€” a scratch layer that lives as long as the process. Use it for every intermediate step. The result is a layer id or path you pass straight into the next algorithm.
  • "memory:" β€” an in-memory layer, similar in effect; TEMPORARY_OUTPUT is the current idiom and behaves better with algorithms that need a real sink.
  • "out/result.gpkg" β€” a real file. The driver comes from the extension, so .gpkg, .geojson, .shp, and .fgb all just work.
  • "ogr:dbname='…' table=\"…\"" β€” a database sink, for writing straight into PostGIS.

Chain intermediates in memory and write once at the end. A four-step workflow that writes four GeoPackages spends most of its time in I/O for files nobody reads.

Read the results dictionary properly

processing.run returns a dict, and the key is the output parameter name β€” usually but not always OUTPUT.

result = processing.run("native:joinattributesbylocation", {...})
print(result.keys())     # dict_keys(['OUTPUT', 'JOINED_COUNT', 'UNJOINABLE_COUNT'])

Several algorithms return extra values worth logging: counts, statistics, or a second sink for the non-matching features. native:checkvalidity returns three (VALID_OUTPUT, INVALID_OUTPUT, ERROR_OUTPUT), and using only the first quietly discards the diagnosis.

What you get inside those keys depends on the output target. With a file path you get the path back as a string. With TEMPORARY_OUTPUT you get an identifier that other algorithms accept but that is not a layer object. When you need a real layer, use processing.runAndLoadResults in the desktop, or construct it yourself:

from qgis.core import QgsVectorLayer
out = processing.run("native:buffer", {..., "OUTPUT": "TEMPORARY_OUTPUT"})["OUTPUT"]
layer = out if isinstance(out, QgsVectorLayer) else QgsVectorLayer(out, "buffered", "ogr")

Capture feedback and errors

By default, warnings and progress go to a place a scheduled job cannot see. Pass a feedback object and route it into logging.

import logging
from qgis.core import QgsProcessingFeedback

log = logging.getLogger("processing")

class LogFeedback(QgsProcessingFeedback):
    def pushInfo(self, info):        log.info(info)
    def pushWarning(self, warning):  log.warning(warning)
    def reportError(self, error, fatalError=False):
        log.error(error)
    def setProgressText(self, text): log.info(text)

processing.run("native:buffer", {...}, feedback=LogFeedback())

Now a run that half-fails leaves a trace. Combine it with alerting on failure and an overnight job stops being a black box.

Validate before you run

For anything long, check the parameters before spending the time:

from qgis.core import QgsApplication, QgsProcessingContext

alg = QgsApplication.processingRegistry().algorithmById("native:buffer")
params = {"INPUT": "data/parcels.gpkg", "DISTANCE": 25, "OUTPUT": "TEMPORARY_OUTPUT"}

ok, message = alg.checkParameterValues(params, QgsProcessingContext())
if not ok:
    raise ValueError(f"bad parameters: {message}")

This is the Processing equivalent of validating pipeline inputs before the run starts β€” a second of checking against twenty minutes of wasted work.

Code examples

Example 1: A three-algorithm chain with no intermediate files

import processing

parcels = "data/parcels.gpkg|layername=parcels"

reprojected = processing.run("native:reprojectlayer", {
    "INPUT": parcels,
    "TARGET_CRS": "EPSG:27700",
    "OUTPUT": "TEMPORARY_OUTPUT",
})["OUTPUT"]

valid = processing.run("native:fixgeometries", {
    "INPUT": reprojected,
    "OUTPUT": "TEMPORARY_OUTPUT",
})["OUTPUT"]

final = processing.run("native:clip", {
    "INPUT": valid,
    "OVERLAY": "data/district.geojson",
    "OUTPUT": "out/parcels_clean.gpkg",
})["OUTPUT"]

print("wrote", final)

Reproject, repair, clip β€” one file written. Note the order: fixing geometries after reprojection catches the invalidities that reprojection itself can introduce.

Example 2: A thin wrapper that logs and times every call

import logging
import time
import processing

log = logging.getLogger("alg")

def run(alg_id, params, **kwargs):
    started = time.perf_counter()
    log.info("β†’ %s", alg_id)
    try:
        result = processing.run(alg_id, params, **kwargs)
    except Exception as exc:
        log.error("βœ— %s failed after %.1fs: %s", alg_id, time.perf_counter() - started, exc)
        raise
    log.info("βœ“ %s in %.1fs", alg_id, time.perf_counter() - started)
    return result

Swap processing.run for run throughout a script and you get a timed, labelled trace of the whole workflow for four lines of cost. It also gives you one place to add retries β€” the pattern from adding retries and timeouts drops straight in.

Example 3: Discovering enum values instead of guessing them

from qgis.core import QgsApplication, QgsProcessingParameterEnum

alg = QgsApplication.processingRegistry().algorithmById("native:buffer")
for p in alg.parameterDefinitions():
    line = f"{p.name():<16} {p.type():<12} {'optional' if p.flags() else ''}"
    if isinstance(p, QgsProcessingParameterEnum):
        line += "  options=" + ", ".join(f"{i}={o}" for i, o in enumerate(p.options()))
    print(line)

This is how you learn that END_CAP_STYLE is 0=Round, 1=Flat, 2=Square rather than copying a magic number from a forum post.

Example 4: Raster work through the GDAL provider

Vector algorithms are mostly native:; raster work often lives under gdal:.

processing.run("gdal:warpreproject", {
    "INPUT": "data/dem.tif",
    "SOURCE_CRS": None,
    "TARGET_CRS": "EPSG:27700",
    "RESAMPLING": 1,               # 0=nearest, 1=bilinear, 2=cubic …
    "NODATA": -9999,
    "TARGET_RESOLUTION": 10,
    "OPTIONS": "COMPRESS=DEFLATE|PREDICTOR=2|TILED=YES",
    "DATA_TYPE": 0,
    "OUTPUT": "out/dem_bng.tif",
})

processing.run("native:zonalstatisticsfb", {
    "INPUT": "out/parcels_clean.gpkg",
    "INPUT_RASTER": "out/dem_bng.tif",
    "RASTER_BAND": 1,
    "COLUMN_PREFIX": "elev_",
    "STATISTICS": [2, 5, 6],       # mean, min, max
    "OUTPUT": "out/parcels_elevation.gpkg",
})

The OPTIONS string is passed to GDAL creation options verbatim β€” the same knobs you would use from the command line, which makes rasterio knowledge transfer directly.

Example 5: A config-driven step list

Because a call is just an id and a dictionary, a workflow is just a list of them β€” and a list is data you can put in a config file.

import processing

STEPS = [
    ("native:reprojectlayer", {"TARGET_CRS": "EPSG:27700"}),
    ("native:fixgeometries",  {}),
    ("native:buffer",         {"DISTANCE": 25, "SEGMENTS": 8, "DISSOLVE": False}),
]

def apply_all(source, steps, final_output):
    current = source
    for i, (alg_id, params) in enumerate(steps):
        is_last = i == len(steps) - 1
        result = processing.run(alg_id, {
            **params,
            "INPUT": current,
            "OUTPUT": final_output if is_last else "TEMPORARY_OUTPUT",
        })
        current = result["OUTPUT"]
    return current

apply_all("data/parcels.gpkg|layername=parcels", STEPS, "out/parcels_final.gpkg")

That is a Processing-flavoured version of the step registry from chaining GIS processing steps, and it accepts a YAML config without further work.

Explanation

Processing is a framework, not a library of functions. An algorithm declares its parameters as objects β€” this one is a feature source, that one is a number with a minimum of zero, this one is an enum with four options β€” and the framework uses those declarations to build the dialog, to validate input, to expose the algorithm in the model builder, and to accept a dictionary from Python. That single declaration is why the same algorithm works identically from a toolbox click, a model, qgis_process, and your script. It is also why the parameter names are stable enough to script against: they are part of the algorithm's public definition, not an implementation detail.

The consequence for automation is that processing.run is a genuinely thin surface. There is no separate scripting API to learn and no divergence between what the GUI does and what your script does β€” the History panel proves it, because what it records is literally the call the dialog made. When a colleague asks why the script gives different numbers from their manual run, the answer is always a parameter difference, and the two dictionaries can be diffed.

The performance story is worth understanding too. native: algorithms are C++ operating on feature sources, streaming features rather than materialising whole tables, which is why a native: buffer on a large layer will usually beat a hand-written PyQGIS feature loop by an order of magnitude β€” and why chaining through TEMPORARY_OUTPUT is not merely tidier but faster than writing and re-reading GeoPackages between steps. The gdal: algorithms are different in kind: they build a command line and run the GDAL binary, so their cost includes process startup and they always write real files.

Finally, the error model. processing.run raises QgsProcessingException on a hard failure, but a great deal of what goes wrong is a warning β€” features skipped for invalid geometry, a CRS assumed, an attribute truncated. Those go to the feedback object, and if you did not pass one they go nowhere you will look. Passing a logging feedback object is the difference between "the job ran" and "the job ran and dropped 412 features", which is exactly the kind of thing a batch job's error report exists to surface.

Edge cases or notes

Algorithm not found in a standalone script

The registry is empty until Processing is initialised. In a standalone script that means appending the plugins directory to sys.path and calling Processing.initialize() before the first processing.run. If native: algorithms resolve but grass: ones do not, that provider is genuinely not installed rather than not initialised.

Legacy qgis: ids still work but are not equivalent

Many qgis: ids are kept as aliases and forward to the native: implementation, but a handful are separate Python implementations with different parameters and slower execution. If you inherited a script full of qgis: ids, check each one with algorithmHelp before assuming an id swap is safe.

None is meaningful for optional parameters

Omitting an optional key and passing None are both accepted, but for parameters like SOURCE_CRS passing None explicitly means "detect it", which is what you usually want. Passing an empty string is not the same and often fails validation.

Selections do not exist headlessly

QgsProcessingFeatureSourceDefinition(layer.id(), True) restricts an algorithm to selected features, which is useful in the desktop and meaningless in a scheduled job. Express the subset as an expression instead, so the same script produces the same result whoever runs it.

Output file overwriting

Writing to an existing GeoPackage with the same layer name replaces that layer; writing a shapefile silently replaces the whole set of sidecar files. If a run must never clobber, check Path(out).exists() first β€” the framework will not stop you.

Threads and progress

processing.run blocks. To keep a GUI responsive you would use QgsProcessingAlgRunnerTask, but for automation blocking is what you want. Do not try to run several algorithms on threads in one process; run separate processes instead, as covered in parallel batch processing.

FAQ

How do I find the parameter names for an algorithm?

Three reliable ways: run it once from the toolbox and read the recorded call in Processing β†’ History; call processing.algorithmHelp("native:buffer"), which prints every parameter with its type and enum options; or run qgis_process help native:buffer from a terminal. Never copy parameter names from a tutorial without checking β€” several changed between QGIS versions.

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

native: algorithms are implemented in C++ and are the current, maintained implementations. qgis: is the older Python provider; many of its ids are now aliases forwarding to the native version, but some remain distinct and slower. For new automation, use native: wherever the algorithm exists there.

Why does processing.run say the algorithm is not found?

The Processing registry has not been initialised. Inside QGIS this is done for you; in a standalone script you must append the QGIS plugins directory to sys.path, import Processing from processing.core.Processing, and call Processing.initialize() before the first run. If only third-party providers are missing, install and enable them instead.

Should I use TEMPORARY_OUTPUT or write files between steps?

Use TEMPORARY_OUTPUT for every intermediate. It avoids disk I/O, keeps the workspace clean, and is faster on any non-trivial dataset. Write a real file only where you want a result someone will open, and at the end of the chain β€” plus, when debugging, at the one step whose output you need to inspect.

How do I see warnings from an algorithm in a scheduled job?

Pass a QgsProcessingFeedback subclass that forwards pushInfo, pushWarning, and reportError into the logging module. Without it, those messages go to the QGIS log panel, which does not exist in a headless run β€” so a job that skipped hundreds of invalid features looks identical to a clean one.

Can I run Processing algorithms without writing Python at all?

Yes β€” qgis_process run native:buffer -- INPUT=in.gpkg DISTANCE=25 OUTPUT=out.gpkg runs any algorithm from the shell, with --json for machine-readable output. It is ideal for a single algorithm in a Makefile or CI step. Once you need conditionals, loops, or error handling, move to Python.

Do Processing algorithms handle CRS transformation for me?

Only where the algorithm says so. Overlay algorithms like native:clip will reproject the overlay to the input's CRS, but distance and area parameters are always interpreted in the input layer's units β€” so a 25-unit buffer on a layer in EPSG:4326 means 25 degrees. Reproject to a metric CRS first; the same trap is covered for the pure-Python stack in calculating area and distance correctly.

How do I pass a selection or a filtered subset to an algorithm?

In the desktop you can wrap the layer in QgsProcessingFeatureSourceDefinition with "selected features only". For automation, filter explicitly with native:extractbyexpression and feed its output into the next step β€” it is reproducible, it works headlessly, and the expression is visible in the script rather than in someone's session.