Creating a Reusable PyQGIS Processing Algorithm Template
Step-by-step guide to building a production-ready, reusable QgsProcessingAlgorithm subclass in QGIS 3.x — covering parameter definition, safe feature…
Subclass QgsProcessingAlgorithm, implement the mandatory metadata and I/O methods, register the class with a QgsProcessingProvider, and route all progress and cancellation signals through QgsProcessingFeedback. This is part of the Building Custom Processing Algorithms guide, which covers the full lifecycle from parameter chaining to graphical model integration.
Complete Runnable Template
The code below is a drop-in, QGIS 3.x-compatible template. Replace the business logic comment with your own spatial or attribute operations; every other piece — parameter resolution, sink creation, feature iteration, progress tracking — is production-ready as written.
from __future__ import annotations
from qgis.core import (
QgsProcessingAlgorithm,
QgsProcessingContext,
QgsProcessingException,
QgsProcessingFeedback,
QgsProcessingParameterFeatureSource,
QgsProcessingParameterNumber,
QgsProcessingParameterVectorDestination,
QgsProcessing,
QgsFeature,
QgsFeatureSink,
)
class ReusableTemplateAlgorithm(QgsProcessingAlgorithm):
"""
Production-ready template for a custom PyQGIS processing algorithm.
Drop this into a QgsProcessingProvider subclass and extend
processAlgorithm() with your domain logic. See the registration
snippet below for how to wire it into a plugin provider.
"""
# ------------------------------------------------------------------ #
# Parameter keys — define as class constants to avoid typos #
# ------------------------------------------------------------------ #
INPUT: str = "INPUT"
OUTPUT: str = "OUTPUT"
THRESHOLD: str = "THRESHOLD"
# ------------------------------------------------------------------ #
# Metadata #
# ------------------------------------------------------------------ #
def name(self) -> str:
"""Unique, lowercase identifier used by processing.run()."""
return "reusable_template"
def displayName(self) -> str:
"""Human-readable name shown in the Processing Toolbox."""
return "Reusable Processing Template"
def group(self) -> str:
"""Toolbox group label (localisable)."""
return "Custom Automation"
def groupId(self) -> str:
"""Stable lowercase group identifier."""
return "custom_automation"
def shortHelpString(self) -> str:
"""Shown in the algorithm's Help panel inside QGIS."""
return (
"A production-ready template for building custom PyQGIS processing "
"algorithms. Extend processAlgorithm() with your spatial logic."
)
def createInstance(self) -> "ReusableTemplateAlgorithm":
"""
Called by the framework to spawn a fresh instance per execution.
Never share state via class attributes — createInstance() is the
boundary that prevents state leakage between runs.
"""
return ReusableTemplateAlgorithm()
# ------------------------------------------------------------------ #
# Parameter / output declaration #
# ------------------------------------------------------------------ #
def initAlgorithm(self, config: dict | None = None) -> None:
"""
Declare all inputs and outputs. The Processing Framework renders
these as widgets in the dialog, batch processor, and modeler
automatically — no UI code required here.
"""
self.addParameter(
QgsProcessingParameterFeatureSource(
self.INPUT,
"Input vector layer",
types=[QgsProcessing.TypeVectorAnyGeometry],
)
)
self.addParameter(
QgsProcessingParameterNumber(
self.THRESHOLD,
"Processing threshold",
type=QgsProcessingParameterNumber.Double,
defaultValue=0.5,
minValue=0.0,
maxValue=1.0,
optional=False,
)
)
self.addParameter(
QgsProcessingParameterVectorDestination(
self.OUTPUT,
"Output vector layer",
)
)
# ------------------------------------------------------------------ #
# Execution #
# ------------------------------------------------------------------ #
def processAlgorithm(
self,
parameters: dict,
context: QgsProcessingContext,
feedback: QgsProcessingFeedback,
) -> dict:
"""
Core worker method. Runs on the main thread inside QGIS Desktop
and on the calling thread in headless scripts.
Returns a dict mapping output parameter keys to their resolved IDs.
Raise QgsProcessingException for any unrecoverable error — the
framework converts this into a user-visible error message and
cleans up temporary files.
"""
# 1. Resolve and validate input source
source = self.parameterAsSource(parameters, self.INPUT, context)
if source is None:
raise QgsProcessingException(
self.invalidSourceError(parameters, self.INPUT)
)
# 2. Resolve scalar parameters
threshold: float = self.parameterAsDouble(
parameters, self.THRESHOLD, context
)
# 3. Create output sink — inherits fields, geometry type, and CRS
# from the source so downstream tools receive a compatible layer.
sink, dest_id = self.parameterAsSink(
parameters,
self.OUTPUT,
context,
source.fields(),
source.wkbType(),
source.sourceCrs(),
)
if sink is None:
raise QgsProcessingException(
self.invalidSinkError(parameters, self.OUTPUT)
)
# 4. Iterate features with progress reporting and cancellation support
feature_count: int = source.featureCount()
step: float = 100.0 / feature_count if feature_count else 0.0
processed: int = 0
for current, feature in enumerate(source.getFeatures()):
if feedback.isCanceled():
feedback.pushWarning("Processing cancelled by user.")
break
# ---- Replace this block with your domain logic ---- #
# Example: pass-through, but threshold could filter or
# transform attributes before writing to the sink.
if threshold >= 0.0:
out_feature = QgsFeature(source.fields())
out_feature.setGeometry(feature.geometry())
out_feature.setAttributes(feature.attributes())
# FastInsert skips redundant geometry validation — up to 50%
# faster for bulk writes. Use AddFeatures if you need
# feature-level error signals during debugging.
sink.addFeature(out_feature, QgsFeatureSink.FastInsert)
processed += 1
# ---- End domain logic ---- #
feedback.setProgress(int(current * step))
feedback.pushInfo(f"Processed {processed} of {feature_count} features.")
return {self.OUTPUT: dest_id}
Execution Flow Diagram
The diagram below traces the algorithm lifecycle from the moment the Processing Framework calls processAlgorithm() through feature iteration to the returned result dictionary.
Architecture Breakdown
Metadata and createInstance()
name() returns the stable lowercase identifier that processing.run() uses to locate the algorithm. displayName(), group(), and groupId() control how the algorithm appears in the Processing Toolbox’s search index and UI tree. shortHelpString() populates the in-dialog Help panel.
createInstance() is the critical lifecycle boundary. The Processing Framework calls it to produce a fresh object for each execution. Because QGIS can batch-run algorithms and queue them for the graphical modeler, you must never store mutable execution state in class attributes — all per-run data must live in local variables inside processAlgorithm().
initAlgorithm() — Parameter Contract
initAlgorithm() declares the algorithm’s public interface. The Processing Framework reads this declaration to render the dialog widgets, construct the modeler port, and validate inputs before calling processAlgorithm(). Using QgsProcessingParameterFeatureSource (rather than a plain layer parameter) accepts layers, memory layers, and outputs piped from upstream algorithm steps in a model — it is the correct choice for any vector input. QgsProcessingParameterVectorDestination similarly handles file-backed, memory-backed, and GeoPackage outputs transparently.
Define parameter keys as class constants (INPUT = "INPUT"). String literals scattered across initAlgorithm() and processAlgorithm() are a common source of silent failures that only surface at runtime.
processAlgorithm() — Execution Contract
processAlgorithm() receives three arguments: parameters (a dict of resolved parameter values), context (a QgsProcessingContext carrying the project reference, temporary directory, and CRS transform settings), and feedback (a QgsProcessingFeedback instance that bridges to the progress bar and log panel).
The method contract has four non-negotiable invariants:
- Validate every source and sink.
parameterAsSource()returnsNonewhen the referenced layer is invalid or missing. RaiseQgsProcessingExceptionwithself.invalidSourceError()immediately — do not proceed to the feature loop with aNonesource. - Check
feedback.isCanceled()every iteration. Without this check the algorithm is unresponsive to the Cancel button and blocks the QGIS event loop during long-running operations. - Call
feedback.setProgress()with a 0–100 float. Pre-computestep = 100.0 / feature_countbefore the loop rather than dividing inside it. - Return a dict mapping every output key to its resolved ID. Omitting an output key from the return dict causes downstream model steps that reference that output to silently receive
None.
Output Sink Geometry and Field Inheritance
Passing source.fields(), source.wkbType(), and source.sourceCrs() to parameterAsSink() ensures the output sink inherits the input schema exactly. This matters for coordinate transformations — if your algorithm needs to reproject features, apply a QgsCoordinateTransform inside the loop and pass the target CRS to parameterAsSink() instead of source.sourceCrs().
Registration and Integration
Plugin Provider Registration
To expose the algorithm in the Processing Toolbox, register it through a QgsProcessingProvider subclass. This is the standard approach for plugin lifecycle management — the provider is added during initGui() and removed in unload().
from __future__ import annotations
from qgis.core import QgsApplication, QgsProcessingProvider
from .reusable_template import ReusableTemplateAlgorithm
class CustomAutomationProvider(QgsProcessingProvider):
"""Registers all custom algorithms for this plugin."""
def id(self) -> str:
return "custom_automation"
def name(self) -> str:
return "Custom Automation"
def loadAlgorithms(self) -> None:
"""Called by the framework when the provider is activated."""
self.addAlgorithm(ReusableTemplateAlgorithm())
# Add further algorithm instances here as the library grows.
# In your plugin class __init__.py:
class MyPlugin:
def __init__(self, iface):
self._provider = CustomAutomationProvider()
def initGui(self) -> None:
QgsApplication.processingRegistry().addProvider(self._provider)
def unload(self) -> None:
QgsApplication.processingRegistry().removeProvider(self._provider)
Standalone Headless Script
For CI pipelines or server-side batch jobs that run outside QGIS Desktop, initialise the QGIS application context before calling processing.run():
from __future__ import annotations
import sys
from qgis.core import QgsApplication
# Adjust the prefix path to your QGIS installation.
QgsApplication.setPrefixPath("/usr", True)
qgs = QgsApplication([], False)
qgs.initQgis()
# Import processing after initQgis() so the framework is ready.
from qgis import processing
from qgis.core import QgsProcessingFeedback
feedback = QgsProcessingFeedback()
result = processing.run(
"custom_automation:reusable_template",
{
"INPUT": "/data/input.gpkg|layername=parcels",
"THRESHOLD": 0.75,
"OUTPUT": "TEMPORARY_OUTPUT",
},
feedback=feedback,
)
print(result["OUTPUT"])
qgs.exitQgis()
Note that processing.run() resolves the algorithm by the <provider_id>:<algorithm_name> string returned by id() and name() respectively. Registering the provider before the call is mandatory — the framework will raise a QgsProcessingException if the algorithm ID is unknown.
Production Best Practices
- Never mutate input layers in place. Write exclusively to the output sink. The framework assumes
processAlgorithm()is side-effect-free so it can safely replay executions in the modeler. - Use
QgsFeatureSink.FastInsertfor bulk writes. It bypasses per-feature geometry validation and accelerates large writes by 30–50%. Switch toQgsFeatureSink.AddFeaturesonly when you need feature-level insert error signals during local debugging. - Keep algorithms stateless. Store configuration in
QgsProcessingContextextras or project variables, not in class attributes. Stateful algorithms fail silently when the framework spawns multiple instances for batch or parallel model execution. - Log with
feedback.pushInfo()andfeedback.pushWarning(). These route to the algorithm’s log panel in the Processing dialog.print()goes to the Python console only and is invisible when the algorithm runs headlessly or inside the modeler. - Handle CRS transformations explicitly. If your algorithm requires a fixed projection, accept the input CRS via
QgsProcessingParameterFeatureSourceand apply aQgsCoordinateTransforminside the loop rather than assuming the input layer’s CRS matches your target. See the coordinate transformations and CRS handling guide for the correct transform chain. - Test with a mock
QgsProcessingFeedback. Subclass or mockQgsProcessingFeedbackin your test suite to assert thatsetProgress()increases monotonically and thatisCanceled()triggers the break path. The QGIS PyQGIS API reference documents all method signatures you need to implement for a compliant mock. - Use type hints throughout.
parameterAsSource(),parameterAsSink(), andparameterAsDouble()all return typed values in QGIS 3.28+. Type hints make contract violations visible at static analysis time and reduce runtimeAttributeErrorsurprises.
Related
- Building Custom Processing Algorithms — parent guide covering parameter chaining, modeler integration, and provider debugging
- Plugin Lifecycle and Resource Management — how to register and clean up providers correctly during plugin load and unload
- Coordinate Transformations and CRS Handling — reprojecting features inside a processing algorithm without silently dropping geometries
- Plugin Development and UI Integration — overview of the full plugin development surface area