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.
Prerequisites
- QGIS 3.34 LTR or newer installed.
qgis_processships with the standard packages:/usr/bin/qgis_processon Linux,qgis_process-qgis-ltr.batinside thebinfolder on Windows, and/Applications/QGIS.app/Contents/MacOS/bin/qgis_processon macOS. - On a server with no display,
QT_QPA_PLATFORM=offscreenexported 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.
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.
QGIS version compatibility
The examples target QGIS 3.34 LTR (Python 3.12).
| QGIS version | Notes |
|---|---|
| 3.16 LTR | qgis_process introduced; no --json, and models must be installed rather than passed by path. |
| 3.22 LTR | --json available. |
| 3.28 LTR | Adds --project so algorithms can resolve layers from a project file. |
| 3.34 LTR | Baseline for this page. |
| 3.40 / 3.44 | Adds --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.batby 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
--profilewith the profile name, and confirm withqgis_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.