Write a Custom Processing Algorithm in PyQGIS

Subclassing QgsProcessingAlgorithm lets you turn an ordinary PyQGIS function into a first-class Processing tool: it gets an auto-generated dialog, batch support, model integration, and a stable id you can call from any script. This page walks through a single complete, runnable algorithm — one that copies an input vector layer to an output, computes a new attribute, and reports progress through feedback — so you can see every required method working together rather than in isolated fragments.

This task belongs to the Processing Provider Plugins for QGIS cluster. The algorithm class you build here is exactly what a provider's loadAlgorithms() registers, so once it runs you can drop it straight into a plugin.

Prerequisites

  • QGIS 3.34 LTR (Python 3.12) with the Processing plugin active.
  • A vector layer to test against (any polygon or point layer in the project).
  • Basic Python class knowledge and the QGIS Python Console for quick iteration.
  • Optional: the provider scaffolding from Processing Provider Plugins for QGIS if you intend to ship the algorithm in a plugin.

How a Processing Algorithm Differs from a Plain Function

It is tempting to think of an algorithm as just a function wrapped in a class, but the framework imposes a contract that buys you a great deal. By declaring parameters instead of reading arguments, you let Processing build the dialog, validate types, support batch mode, and expose the tool to the Model Designer. By writing to a sink instead of a hardcoded file, you let the user pick GeoPackage, Shapefile, or an in-memory layer at run time. By reporting through feedback instead of print(), your progress appears in the dialog and your log lines land in the Processing history. None of this requires extra UI code — it falls out of implementing the contract correctly, which is why a Processing algorithm is almost always preferable to a custom dialog for data transformations.

The Methods Every Algorithm Must Implement

A Processing algorithm is defined by a small, fixed set of overrides. Identity methods describe the tool; the two lifecycle methods do the work.

  • name() — the machine id (lowercase, no spaces); combined with the provider id to address the algorithm.
  • displayName() — the human-readable label in the Toolbox and dialog.
  • group() / groupId() — optional sub-grouping under the provider.
  • createInstance() — returns a fresh copy of the algorithm; required by the framework.
  • initAlgorithm() — declares input and output parameters.
  • processAlgorithm() — reads the source, transforms features, writes the sink, reports progress.

Declaring Parameters in initAlgorithm

initAlgorithm() runs once when QGIS builds the dialog. Each addParameter() call adds one widget. Here we take a vector feature source as input, a numeric buffer distance, and a feature sink as output. Defining string constants for parameter keys keeps processAlgorithm() readable and typo-proof.

from qgis.core import (
    QgsProcessingAlgorithm,
    QgsProcessingParameterFeatureSource,
    QgsProcessingParameterFeatureSink,
    QgsProcessingParameterNumber,
)


class BufferAndMeasureAlgorithm(QgsProcessingAlgorithm):
    INPUT = "INPUT"
    DISTANCE = "DISTANCE"
    OUTPUT = "OUTPUT"

    def initAlgorithm(self, config=None):
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.INPUT,
                self.tr("Input layer"),
            )
        )
        self.addParameter(
            QgsProcessingParameterNumber(
                self.DISTANCE,
                self.tr("Buffer distance (layer units)"),
                type=QgsProcessingParameterNumber.Double,
                defaultValue=100.0,
                minValue=0.0,
            )
        )
        self.addParameter(
            QgsProcessingParameterFeatureSink(
                self.OUTPUT,
                self.tr("Buffered output"),
            )
        )

Breakdown: QgsProcessingParameterFeatureSource produces a layer picker that also accepts a "selected features only" toggle and on-the-fly reprojection. QgsProcessingParameterNumber with type=Double renders a spin box; minValue enforces a non-negative distance in the dialog. QgsProcessingParameterFeatureSink is the output target — Processing resolves it to a memory layer, a GeoPackage, or a Shapefile depending on what the user chooses, so you never hardcode a path.

Implementing processAlgorithm

processAlgorithm() receives the resolved parameters, a context, and a feedback object. The standard shape is: resolve the source, create the sink, iterate features, push each transformed feature into the sink, and update progress. Buffering each geometry and recording its area demonstrates a real transformation rather than a passthrough.

    def processAlgorithm(self, parameters, context, feedback):
        source = self.parameterAsSource(parameters, self.INPUT, context)
        if source is None:
            raise QgsProcessingException(
                self.invalidSourceError(parameters, self.INPUT))

        distance = self.parameterAsDouble(parameters, self.DISTANCE, context)

        # Build the output field set: input fields plus a new area field.
        out_fields = source.fields()
        out_fields.append(QgsField("buff_area", QVariant.Double))

        sink, dest_id = self.parameterAsSink(
            parameters,
            self.OUTPUT,
            context,
            out_fields,
            QgsWkbTypes.Polygon,
            source.sourceCrs(),
        )
        if sink is None:
            raise QgsProcessingException(
                self.invalidSinkError(parameters, self.OUTPUT))

        total = source.featureCount()
        step = 100.0 / total if total else 0

        for current, feature in enumerate(source.getFeatures()):
            if feedback.isCanceled():
                break

            geom = feature.geometry().buffer(distance, 5)
            new_feature = QgsFeature(out_fields)
            new_feature.setGeometry(geom)
            new_feature.setAttributes(
                feature.attributes() + [geom.area()])
            sink.addFeature(new_feature, QgsFeatureSink.FastInsert)

            feedback.setProgress(int(current * step))

        feedback.pushInfo(f"Buffered {total} features by {distance} units.")
        return {self.OUTPUT: dest_id}

Breakdown: parameterAsSource and parameterAsSink convert the raw parameter values into usable objects, raising a clear error through the invalidSourceError/invalidSinkError helpers when resolution fails. The output sink needs an explicit field set, geometry type, and CRS up front — here we copy the source fields and append buff_area. feedback.isCanceled() lets the user stop a long run from the dialog; feedback.setProgress() drives the progress bar; feedback.pushInfo() writes to the log panel. The method must return a dictionary keyed by output name, mapping to the sink's dest_id.

Identity and createInstance

The remaining methods describe the algorithm and let the framework clone it. createInstance() must return a new object every time so concurrent batch runs do not share state. The tr() helper marks strings for translation.

    def name(self):
        return "bufferandmeasure"

    def displayName(self):
        return self.tr("Buffer and Measure")

    def group(self):
        return self.tr("Vector geometry")

    def groupId(self):
        return "vectorgeometry"

    def shortHelpString(self):
        return self.tr(
            "Buffers each input feature by a fixed distance and records "
            "the resulting polygon area in a new 'buff_area' field.")

    def tr(self, string):
        return QCoreApplication.translate("Processing", string)

    def createInstance(self):
        return BufferAndMeasureAlgorithm()

Breakdown: name() is the permanent id used as provider_id:bufferandmeasure. displayName() is cosmetic and translatable. group()/groupId() slot the tool under a "Vector geometry" node, matching the grouping conventions described in Processing Provider Plugins for QGIS. shortHelpString() populates the help panel beside the dialog. createInstance() returning a brand-new object is mandatory — returning self causes subtle bugs under batch and model execution.

Complete Imports and a Test Run

Put the imports at the top of the file, then test the class directly in the Python Console without a full plugin. Registering it temporarily on a throwaway provider is the fastest way to confirm it loads.

from qgis.PyQt.QtCore import QCoreApplication, QVariant
from qgis.core import (
    QgsField,
    QgsFeature,
    QgsFeatureSink,
    QgsWkbTypes,
    QgsProcessingException,
)
import processing

# Run the algorithm headlessly once it is registered on a provider:
result = processing.run("mytools:bufferandmeasure", {
    "INPUT": "/data/sites.gpkg|layername=sites",
    "DISTANCE": 250.0,
    "OUTPUT": "memory:buffered_sites",
})
print(result["OUTPUT"].featureCount(), "features written")

Breakdown: The first import block lists every class the algorithm references. The processing.run() call exercises the algorithm exactly as the Toolbox dialog would, using the parameter keys you defined. Passing "memory:buffered_sites" as OUTPUT keeps the result in RAM for inspection. For the broader patterns around invoking algorithms programmatically, see Run a Processing Algorithm from a Script.

To register the class on a temporary provider purely for testing — before you commit to the full plugin scaffolding — you can create a one-off provider and add it to the registry directly from the console:

from qgis.core import QgsApplication, QgsProcessingProvider


class _TempProvider(QgsProcessingProvider):
    def loadAlgorithms(self):
        self.addAlgorithm(BufferAndMeasureAlgorithm())

    def id(self):
        return "mytools"

    def name(self):
        return "My Tools (dev)"


provider = _TempProvider()
QgsApplication.processingRegistry().addProvider(provider)

Breakdown: This mirrors what a real plugin's initProcessing() does, but runs inline so you can iterate without restarting QGIS. After editing the algorithm, call QgsApplication.processingRegistry().removeProvider(provider) and re-run the snippet to refresh. Once the class is stable, move it into a packaged provider as described in Processing Provider Plugins for QGIS so it survives QGIS restarts and can be distributed.

QGIS Version Compatibility

The example targets QGIS 3.34 LTR (Python 3.12). The algorithm API is stable across current releases; the only common pitfall is the QVariant import on older Pythons.

QGIS releasePythonNotes
3.28 LTR3.9Works unchanged; QVariant.Double and all parameter classes available.
3.34 LTR3.12Baseline for this guide.
3.40 / 3.443.12No breaking changes; newer builds also accept native Python type hints for fields, but QVariant.Double remains valid and portable.

For maximum portability across all three lines, keep using QgsField("buff_area", QVariant.Double) rather than the newer QMetaType-based constructors, which only exist in the most recent releases.

Troubleshooting

QgsProcessingException: There was an error initializing the algorithm Your createInstance() is missing or returns self. It must return a new BufferAndMeasureAlgorithm() instance.

The output layer is empty. You forgot to return the result dictionary, or addFeature() was never reached. Confirm processAlgorithm() ends with return {self.OUTPUT: dest_id} and that the source actually contains features.

Wrong parameter value for OUTPUT. The sink geometry type or CRS does not match what you write. Ensure the QgsWkbTypes value passed to parameterAsSink matches the geometry your transformation produces (buffering points and polygons both yield Polygon).

Progress bar never moves.feedback.setProgress() is missing or featureCount() returned -1 for a streamed source. Guard the step calculation with if total else 0 as shown, and call setProgress() inside the loop.

NameError: name 'QVariant' is not defined. Add from qgis.PyQt.QtCore import QVariant to the import block. This is the single most common omission when copying field-creation code.

Conclusion

A custom Processing algorithm is just a QgsProcessingAlgorithm subclass with declared parameters and a processAlgorithm() that reads a source, transforms features, and fills a sink while reporting through feedback. Once it runs in isolation, registering it on a provider — covered in Processing Provider Plugins for QGIS — makes it installable, batchable, and chainable with no further changes to the class itself.

Frequently Asked Questions

Do I have to write a provider to use a custom algorithm? No. You can drop the same class into the user processing/scripts/ folder, where it appears under the built-in Scripts provider. A provider is for branding, grouping, and distribution.

How do I add a dropdown or boolean option? Use QgsProcessingParameterEnum for a fixed list and QgsProcessingParameterBoolean for a checkbox inside initAlgorithm(), then read them with parameterAsEnum() and parameterAsBool() in processAlgorithm().

Why must createInstance return a new object? Batch and model execution may run several copies concurrently. Returning self shares mutable state between runs and produces corrupted or interleaved results.

Can I chain my algorithm with others? Yes. Because it is registered with Processing, it works in the Model Designer and in scripts — see Chain Buffer and Clip in PyQGIS for a worked pipeline.