Use the qgis_process Command-Line Runner

There is a whole class of automation that needs no Python at all: clip this to that, reproject a folder, buffer the roads every morning. qgis_process runs any Processing algorithm straight from a shell, with the same algorithm ids and parameter names a PyQGIS script would use, and exits non-zero when it fails — which is all a scheduler needs to know.

This recipe belongs to Headless QGIS and Server Automation. It covers finding the executable, discovering an algorithm's parameters, running it with real inputs, reading machine-parsable output, and knowing when to switch to a Python script instead.

Three front ends, one algorithmA dialog in the QGIS toolbox, a Python call to processing dot run, and a qgis process command line all converge on the same registered algorithm. The algorithm executes once in compiled code and writes the same output regardless of which front end invoked it.Same algorithm id, same parameter names, no GUI requiredtoolbox dialoga person clickingprocessing.run()inside a Python processqgis_process runfrom any shell or cronprocessing registrynative:bufferone implementationidentical output

Prerequisites

  • QGIS 3.34 LTR or newer installed. qgis_process ships with the standard packages: /usr/bin/qgis_process on Linux, qgis_process-qgis-ltr.bat inside the bin folder on Windows, and /Applications/QGIS.app/Contents/MacOS/bin/qgis_process on macOS.
  • On a server with no display, QT_QPA_PLATFORM=offscreen exported in the environment.
  • Input data reachable by absolute path.

Discover what you can run

qgis_process list | grep -i buffer
qgis_process help native:buffer

Breakdown: list prints every algorithm the current profile can see, grouped by provider — which is also how you confirm that a provider plugin is actually loaded in this environment. help prints the parameters with their types, defaults and accepted values, and it is authoritative for the exact spelling of each parameter name. The same information is available in Python through processing.algorithmHelp("native:buffer"), so the two interfaces never disagree.

Run an algorithm

qgis_process run native:buffer -- \
  INPUT="/data/roads.gpkg|layername=roads" \
  DISTANCE=25 \
  SEGMENTS=8 \
  DISSOLVE=true \
  OUTPUT="/data/output/roads_buffer.gpkg"

Breakdown: Everything after the bare -- is a NAME=value parameter assignment. Quoting the GeoPackage source protects the pipe from the shell. Enumerated parameters accept either their index or their name — DISSOLVE=true for a boolean, METHOD=0 for a choice. The output path's extension picks the driver, so ending it in .gpkg writes a GeoPackage and .shp writes a shapefile with all the limitations that implies, as covered in Write a Vector Layer to GeoPackage in PyQGIS.

Progress goes to standard error, results to standard output, and the exit code is zero only when the algorithm reported success — so set -euo pipefail at the top of a wrapper script is enough to stop a chain at the first failure.

Get machine-readable output

Parsing human-readable progress text is a losing game. Ask for JSON instead.

qgis_process --json run native:zonalstatisticsfb -- \
  INPUT="/data/wards.gpkg|layername=wards" \
  INPUT_RASTER="/data/rainfall.tif" \
  RASTER_BAND=1 \
  STATISTICS=2 \
  OUTPUT="/data/output/wards_rain.gpkg" \
  | jq -r '.results.OUTPUT'

Breakdown: --json switches the whole output to a single JSON document containing the algorithm's inputs, its results and any log messages, which makes it safe to consume from another program. .results.OUTPUT is the written path — useful when the algorithm chose a name, and essential when chaining, because the next command needs the real output rather than the one you assumed. Note that --json goes before run; options after the -- are parameters, not switches.

Anatomy of a qgis_process commandA command line is split into five labelled parts: the executable, global options such as json and project which must come before the verb, the run verb with the algorithm identifier, the double dash separator, and the parameter assignments which are everything after it. A note marks the common mistake of placing a global option after the separator, where it is read as a parameter.Options before the verb, parameters after the dashesqgis_processexecutable--json --projectglobal optionsrun native:bufferverb + algorithm id--INPUT=… DISTANCE=…parametersqgis_process run native:buffer -- --json INPUT=…the option lands after the separator and is read as a parameter, then rejectedOther useful globals: --project to supply a project's layers and CRS, --skip-loading-plugins tostart faster, and --profile to select the profile whose plugins and connections you need

Run a saved model

Models built in the graphical modeller are algorithms too, so they run exactly the same way.

qgis_process run "/srv/models/flood_pipeline.model3" -- \
  CATCHMENT="/data/catchments.gpkg|layername=catchments" \
  RAINFALL="/data/rainfall.tif" \
  native:buffer_1:OUTPUT="/data/output/flood_zones.gpkg"

Breakdown: Passing a .model3 path where an algorithm id would go runs that model directly, without installing it into a profile. Model inputs use the names given in the modeller. Outputs of intermediate steps are addressed with the algorithmid:PARAMETER form, which is how you capture a middle step's result rather than only the final one. This is the cleanest way to hand a repeatable pipeline to a colleague who does not write Python — they run one command, and the pipeline is a file under version control.

Chain several runs in a shell script

A fixed sequence of algorithms does not need Python — it needs a small script with the discipline that makes a chain safe to re-run.

#!/usr/bin/env bash
set -euo pipefail

export QT_QPA_PLATFORM=offscreen
DATA=/srv/gis/data
WORK=$(mktemp -d)
trap 'rm -rf "$WORK"' EXIT

qgis_process run native:reprojectlayer -- \
  INPUT="$DATA/roads.gpkg|layername=roads" \
  TARGET_CRS=EPSG:27700 \
  OUTPUT="$WORK/roads_27700.gpkg"

qgis_process run native:buffer -- \
  INPUT="$WORK/roads_27700.gpkg" \
  DISTANCE=25 DISSOLVE=true \
  OUTPUT="$WORK/roads_buffer.gpkg"

mv "$WORK/roads_buffer.gpkg" /srv/gis/output/roads_buffer.gpkg

Breakdown: set -euo pipefail stops the chain at the first failure instead of feeding a missing file to the next step — without it, a failed reprojection produces a confusing error two commands later. The mktemp -d plus trap pair gives every run a private scratch directory that is cleaned up whether the script succeeds, fails or is interrupted. The final mv is the publish step: the output appears in its destination only once the whole chain has succeeded, and because a rename within one filesystem is atomic, anyone reading that path sees either the previous version or the new one, never a half-written file.

Two habits make such a script pleasant to operate. Log the algorithm and its parameters before each call so the log can be replayed by hand, and accept the date or region as an argument rather than hard-coding it, so a re-run over yesterday's data needs no editing. At the point where the script grows a conditional or a loop over a variable set of inputs, it has outgrown the shell — move it to Python before it becomes unreadable.

Scratch directory in, atomic publish outTwo qgis process runs write their intermediate outputs into a temporary directory created for this run. Only the final result is moved into the published location, and the temporary directory is removed whether the script succeeded or failed. Readers of the published path never see a partial file.Nothing lands in the published path until everything workedsource dataread-only$WORK — removed by the trapreprojectlayerroads_27700.gpkgbufferroads_buffer.gpkgpublished pathatomic mvset -euo pipefail stops the chain at the first failure, so step two never runs on a missing fileWhen the sequence grows a conditional, move it to Python

QGIS version compatibility

The examples target QGIS 3.34 LTR (Python 3.12).

QGIS versionNotes
3.16 LTRqgis_process introduced; no --json, and models must be installed rather than passed by path.
3.22 LTR--json available.
3.28 LTRAdds --project so algorithms can resolve layers from a project file.
3.34 LTRBaseline for this page.
3.40 / 3.44Adds --skip-loading-plugins and faster start-up; parameter syntax unchanged.

Troubleshooting

  • "command not found". The executable is not on PATH. On Windows use the OSGeo4W shell or call the .bat by full path; on macOS the binary is inside the app bundle.
  • "algorithm not found" for a plugin algorithm. The plugin lives in a profile the run is not using. Pass --profile with the profile name, and confirm with qgis_process list.
  • A run succeeds but writes nothing. The algorithm ran against an input that matched no features — a subset string on the source, or a filter in the parameters. Check the input's feature count before blaming the algorithm.
  • The command hangs on a server. Qt is looking for a display. Export QT_QPA_PLATFORM=offscreen.
  • A parameter is rejected as invalid. Check the exact name with qgis_process help; parameter names are case-sensitive and several differ from the label shown in the dialog.
  • The output file is empty but the exit code is zero. The algorithm succeeded on an input that matched nothing. Check the feature count of the input rather than trusting the exit code alone.
  • Layers referenced from a project do not resolve. Pass --project /path/to/project.qgz, which also supplies the project's CRS and datum-transform settings.

Conclusion

qgis_process turns the whole Processing catalogue into shell commands: list and help to discover, run with -- to execute, --json to consume the result programmatically, and a model file path where an algorithm id would go. Keep it for single algorithms and fixed chains, and move to a Python script the moment the work needs branching, retries or per-feature logic.

Frequently Asked Questions

Does qgis_process need a running QGIS? No. It starts its own headless QGIS instance, runs the algorithm and exits. Nothing else needs to be open.

How do I pass multiple input layers? Parameters that accept several layers take a ;-separated list, quoted as one argument. Check qgis_process help — the parameter type will say multilayer.

Can I set the output CRS? Only where the algorithm exposes one, such as native:reprojectlayer's TARGET_CRS. Otherwise the output inherits the input's CRS, as it does in the GUI.

Is it faster than Python? For one algorithm, effectively identical — the same compiled code runs. For a thousand calls, a single Python process wins easily, because qgis_process pays the start-up cost every time.

Can I see what a run actually did afterwards? Yes — --json output includes the resolved input parameters alongside the results, so redirecting it to a file next to the output gives a record of exactly which parameters produced which artefact. That pairing is worth keeping for anything published.

Does it respect the project's datum transformations? Only when you pass --project. Without it the run uses defaults, which for most CRS pairs is fine and for a national grid with its own transformation grid is not — the difference is metres, and silent.

How do I use it inside cron? Wrap it in a small shell script that sets the environment explicitly and redirects both streams to a log file. The details are in Schedule PyQGIS Scripts with cron.