17. Writing a Processing plugin

Depending on the kind of plugin that you are going to develop, it might be a better option to add its functionality as a Processing algorithm (or a set of them). That would provide a better integration within QGIS, additional functionality (since it can be run in the components of Processing, such as the modeler or the batch processing interface), and a quicker development time (since Processing will take of a large part of the work).

To distribute those algorithms, you should create a new plugin that adds them to the Processing Toolbox. The plugin should contain an algorithm provider, which has to be registered when the plugin is instantiated.

17.1. Creating from scratch

To create a plugin from scratch which contains an algorithm provider, you can follow these steps using the Plugin Builder:

  1. Install the Plugin Builder plugin

  2. Create a new plugin using the Plugin Builder. When the Plugin Builder asks you for the template to use, select "Processing provider".

  3. The created plugin contains a provider with a single algorithm. Both the provider file and the algorithm file are fully commented and contain information about how to modify the provider and add additional algorithms. Refer to them for more information.

17.2. Updating a plugin

If you want to add your existing plugin to Processing, you need to add some code.

  1. In your metadata.txt file, you need to add a variable:

    hasProcessingProvider=yes
    
  2. In the Python file where your plugin is setup with the initGui method, you need to adapt some lines like this:

     1fromqgis.coreimport QgsApplication
     2from.processing_provider.providerimport Provider
     3
     4classYourPluginName:
     5
     6 def__init__(self):
     7 self.provider = None
     8
     9 definitProcessing(self):
    10 self.provider = Provider()
    11 QgsApplication.processingRegistry().addProvider(self.provider)
    12
    13 definitGui(self):
    14 self.initProcessing()
    15
    16 defunload(self):
    17 QgsApplication.processingRegistry().removeProvider(self.provider)
    
  3. You can create a folder processing_provider with three files in it:

    • __init__.py with nothing in it. This is necessary to make a valid Python package.

    • provider.py which will create the Processing provider and expose your algorithms.

       1fromqgis.coreimport QgsProcessingProvider
       2fromqgis.PyQt.QtGuiimport QIcon
       3
       4from.example_processing_algorithmimport ExampleProcessingAlgorithm
       5
       6classProvider(QgsProcessingProvider):
       7
       8""" The provider of our plugin. """
       9
      10 defloadAlgorithms(self):
      11""" Load each algorithm into the current provider. """
      12 self.addAlgorithm(ExampleProcessingAlgorithm())
      13 # add additional algorithms here
      14 # self.addAlgorithm(MyOtherAlgorithm())
      15
      16 defid(self) -> str:
      17"""The ID of your plugin, used for identifying the provider.
      18
      19 This string should be a unique, short, character only string,
      20 eg "qgis" or "gdal". This string should not be localised.
      21 """
      22 return 'yourplugin'
      23
      24 defname(self) -> str:
      25"""The human friendly name of your plugin in Processing.
      26
      27 This string should be as short as possible (e.g. "Lastools", not
      28 "Lastools version 1.0.1 64-bit") and localised.
      29 """
      30 return self.tr('Your plugin')
      31
      32 deficon(self) -> QIcon:
      33"""Should return a QIcon which is used for your provider inside
      34 the Processing toolbox.
      35 """
      36 return QgsProcessingProvider.icon(self)
      
    • example_processing_algorithm.py which contains the example algorithm file. Copy/paste the content of the script template file and update it according to your needs.

    You should have a tree similar to this:

    1└──your_plugin_root_folder
    2├──__init__.py
    3├──LICENSE
    4├──metadata.txt
    5└──processing_provider
    6├──example_processing_algorithm.py
    7├──__init__.py
    8└──provider.py
    
  4. Now you can reload your plugin in QGIS and you should see your example script in the Processing toolbox and modeler.

17.3. Implementing custom Processing algorithms

17.3.1. Creating a custom algorithm

Here’s a simple example of a custom buffer algorithm:

 1fromqgis.coreimport (
 2 QgsProcessingAlgorithm,
 3 QgsProcessingParameterFeatureSource,
 4 QgsProcessingParameterNumber,
 5 QgsProcessingParameterFeatureSink,
 6 QgsFeatureSink,
 7)
 8
 9classBufferAlgorithm(QgsProcessingAlgorithm):
10
11 INPUT = 'INPUT'
12 DISTANCE = 'DISTANCE'
13 OUTPUT = 'OUTPUT'
14
15 definitAlgorithm(self, config=None):
16 self.addParameter(QgsProcessingParameterFeatureSource(self.INPUT, 'Input layer'))
17 self.addParameter(QgsProcessingParameterNumber(self.DISTANCE, 'Buffer distance', defaultValue=100.0))
18 self.addParameter(QgsProcessingParameterFeatureSink(self.OUTPUT, 'Output layer'))
19
20 defprocessAlgorithm(self, parameters, context, feedback):
21 source = self.parameterAsSource(parameters, self.INPUT, context)
22 distance = self.parameterAsDouble(parameters, self.DISTANCE, context)
23 (sink, dest_id) = self.parameterAsSink(parameters, self.OUTPUT, context,
24 source.fields(), source.wkbType(), source.sourceCrs())
25
26 for f in source.getFeatures():
27 f.setGeometry(f.geometry().buffer(distance, 5))
28 sink.addFeature(f, QgsFeatureSink.FastInsert)
29
30 return {self.OUTPUT: dest_id}
31
32 defname(self):
33 return 'buffer'
34
35 defdisplayName(self):
36 return 'Buffer Features'
37
38 defgroup(self):
39 return 'Examples'
40
41 defgroupId(self):
42 return 'examples'
43
44 defcreateInstance(self):
45 return BufferAlgorithm()

17.3.2. Customizing the algorithm dialog

Custom dialogs are especially useful when working with nested or dynamic inputs, when parameters depend on external data sources such as APIs (e.g. dynamically populated dropdowns), or when you need advanced validation and custom layout behavior that isn’t supported by the default Processing dialog. To override the default UI (e.g. for complex parameter types or dynamic logic), subclass QgsProcessingAlgorithmDialogBase. To render your custom UI in the standard Processing dialog window, you must call self.setMainWidget(panel), where panel is a QgsPanelWidget containing your custom layout. This ensures your interface is correctly displayed and interacts properly with the Processing framework.

Here is an example that integrates signal management using QTimer for debounced input:

 1fromqgis.PyQt.QtCoreimport Qt, QT_VERSION_STR, QTimer
 2fromqgis.coreimport (
 3 QgsProcessingAlgorithm,
 4 QgsProcessingContext,
 5 QgsProcessingFeedback,
 6 Qgis,
 7)
 8fromqgis.PyQt.QtWidgetsimport QWidget, QVBoxLayout, QLineEdit
 9fromqgisimport gui, processing
10fromdatetimeimport datetime
11fromtypingimport Dict, Optional
12fromosgeoimport gdal
13
14classCustomAlgorithmDialog(gui.QgsProcessingAlgorithmDialogBase):
15 def__init__(
16 self,
17 algorithm: QgsProcessingAlgorithm,
18 parent: Optional[QWidget] = None,
19 title: Optional[str] = None,
20 ):
21 super().__init__(
22 parent,
23 flags=Qt.WindowFlags(),
24 mode=gui.QgsProcessingAlgorithmDialogBase.DialogMode.Single,
25 )
26 self.context = QgsProcessingContext()
27 self.setAlgorithm(algorithm)
28 self.setModal(True)
29 self.setWindowTitle(title or algorithm.displayName())
30
31 self.panel = gui.QgsPanelWidget()
32 layout = self.buildDialog()
33 self.panel.setLayout(layout)
34 self.setMainWidget(self.panel)
35
36 self.cancelButton().clicked.connect(self.reject)
37
38 defbuildDialog(self) -> QVBoxLayout:
39 layout = QVBoxLayout()
40
41 self.input = QLineEdit()
42
43 # Set up a debounced signal using QTimer
44 self._update_timer = QTimer(self, singleShot=True)
45 self._update_timer.timeout.connect(self._on_collection_id_ready)
46 self.input.textChanged.connect(self._on_collection_id_changed)
47
48 layout.addWidget(self.input)
49
50 return layout
51
52 def_on_collection_id_changed(self):
53 self._update_timer.start(500) # Debounce input
54
55 def_on_collection_id_ready(self):
56 self.pushInfo("Fetching metadata for collection ID...")
57
58 defgetParameters(self) -> Dict:
59 try:
60 return {'DISTANCE': float(self.input.text())}
61 except ValueError:
62 raise ValueError("Invalid buffer distance")
63
64 defprocessingContext(self):
65 return self.context
66
67 defcreateFeedback(self):
68 return QgsProcessingFeedback()
69
70 defrunAlgorithm(self):
71 context = self.processingContext()
72 feedback = self.createFeedback()
73 params = self.getParameters()
74
75 self.pushDebugInfo(f"QGIS version: {Qgis.QGIS_VERSION}")
76 self.pushDebugInfo(f"QGIS code revision: {Qgis.QGIS_DEV_VERSION}")
77 self.pushDebugInfo(f"Qt version: {QT_VERSION_STR}")
78 self.pushDebugInfo(f"GDAL version: {gdal.VersionInfo('--version')}")
79 self.pushCommandInfo(f"Algorithm started at: {datetime.now().isoformat(timespec='seconds')}")
80 self.pushCommandInfo(f"Algorithm '{self.algorithm().displayName()}' starting...")
81 self.pushCommandInfo("Input parameters:")
82 for k, v in params.items():
83 self.pushCommandInfo(f" {k}: {v}")
84
85 results = processing.run(self.algorithm(), params, context=context, feedback=feedback)
86 self.setResults(results)
87 self.showLog()

To launch the custom dialog for a given algorithm, simply instantiate CustomAlgorithmDialog with your algorithm instance and call exec():

dlg = CustomAlgorithmDialog(BufferAlgorithm())
dlg.exec()

17.3.3. Managing Qt Signals

When building reactive dialogs, manage signal connections carefully. The above pattern uses a QTimer to debounce input from the text field, preventing rapid repeated calls. This is especially useful when fetching metadata or updating UI elements based on user input. Always connect signals once (typically in __init__) and use singleShot=True to ensure the slot is triggered only once after a delay.