Adding Enum and Field Parameters to a Processing Algorithm

Add rich parameters to a custom QGIS Processing algorithm — QgsProcessingParameterEnum for choices and QgsProcessingParameterField bound to an input layer,…

TL;DR: In initAlgorithm(), add QgsProcessingParameterEnum(options=[...], defaultValue=0) for a fixed choice and QgsProcessingParameterField(parentLayerParameterName="INPUT", type=QgsProcessingParameterField.Numeric) bound to your input layer. In processAlgorithm(), read them with self.parameterAsEnum(parameters, "OPERATION", context) (returns an integer index into your options list) and self.parameterAsFields(parameters, "FIELDS", context) (always returns a list of field names).

This page is part of the Building Custom Processing Algorithms in PyQGIS guide. It assumes you already have the algorithm scaffold from Creating a Reusable PyQGIS Processing Algorithm Template — the metadata methods, createInstance(), and provider registration. Rather than re-teaching the QgsProcessingAlgorithm skeleton, we focus on two parameter types that make an algorithm feel native: a constrained dropdown of choices, and a field selector that populates itself from the columns of whatever layer the user picks.

Complete Runnable Algorithm

The algorithm below aggregates one or more numeric attribute columns using an operation the user selects from a dropdown, then writes a small CSV of field,operation,value rows. It declares three parameters beyond the input and output: a multi-select field parameter (FIELDS) bound to INPUT, and an enum parameter (OPERATION) that constrains the user to a known set of aggregation functions.

python
"""
attribute_aggregator.py

A custom QgsProcessingAlgorithm demonstrating QgsProcessingParameterField
and QgsProcessingParameterEnum. Compatible with QGIS 3.28+ LTR.

Aggregates one or more numeric fields of the input layer using a
user-selected operation and writes the results to a CSV file.
"""
from __future__ import annotations

import csv
from typing import Optional

from qgis.core import (
    QgsFeatureRequest,
    QgsProcessingAlgorithm,
    QgsProcessingContext,
    QgsProcessingException,
    QgsProcessingFeedback,
    QgsProcessingParameterEnum,
    QgsProcessingParameterField,
    QgsProcessingParameterFeatureSource,
    QgsProcessingParameterFileDestination,
    QgsProcessing,
)


class AttributeAggregatorAlgorithm(QgsProcessingAlgorithm):
    """Aggregate selected numeric fields with a chosen operation.

    Algorithm ID: enterprise_provider:attributeaggregator
    """

    INPUT = "INPUT"
    FIELDS = "FIELDS"
    OPERATION = "OPERATION"
    OUTPUT = "OUTPUT"

    # The options list is the single source of truth for the enum.
    # parameterAsEnum() returns an index into THIS list — keep the order stable.
    OPERATIONS: list[str] = ["sum", "mean", "min", "max", "count"]

    def name(self) -> str:
        return "attributeaggregator"

    def displayName(self) -> str:
        return "Aggregate attribute fields"

    def group(self) -> str:
        return "Enterprise Workflows"

    def groupId(self) -> str:
        return "enterprise_workflows"

    def shortHelpString(self) -> str:
        return (
            "Aggregates one or more numeric fields of the input layer using a "
            "selected operation (sum, mean, min, max, count) and writes the "
            "results to a CSV file."
        )

    def createInstance(self) -> "AttributeAggregatorAlgorithm":
        return AttributeAggregatorAlgorithm()

    def initAlgorithm(self, config: Optional[dict] = None) -> None:
        """Declare inputs, the two rich parameters, and the output."""
        self.addParameter(
            QgsProcessingParameterFeatureSource(
                self.INPUT,
                "Input layer",
                [QgsProcessing.TypeVectorAnyGeometry],
            )
        )
        # Field parameter — the dropdown is populated from INPUT's numeric
        # columns because parentLayerParameterName ties it to that source.
        self.addParameter(
            QgsProcessingParameterField(
                self.FIELDS,
                "Numeric field(s) to aggregate",
                parentLayerParameterName=self.INPUT,
                type=QgsProcessingParameterField.Numeric,
                allowMultiple=True,
            )
        )
        # Enum parameter — a fixed choice list. defaultValue is the INDEX
        # of the pre-selected option ("mean" here), not the string.
        self.addParameter(
            QgsProcessingParameterEnum(
                self.OPERATION,
                "Aggregation operation",
                options=self.OPERATIONS,
                allowMultiple=False,
                defaultValue=self.OPERATIONS.index("mean"),
            )
        )
        self.addParameter(
            QgsProcessingParameterFileDestination(
                self.OUTPUT,
                "Aggregate results",
                fileFilter="CSV files (*.csv)",
            )
        )

    def processAlgorithm(
        self,
        parameters: dict,
        context: QgsProcessingContext,
        feedback: QgsProcessingFeedback,
    ) -> dict:
        """Read the field/enum parameters and aggregate the data."""
        source = self.parameterAsSource(parameters, self.INPUT, context)
        if source is None:
            raise QgsProcessingException(self.invalidSourceError(parameters, self.INPUT))

        # parameterAsFields ALWAYS returns a list[str], even for a single field.
        fields: list[str] = self.parameterAsFields(parameters, self.FIELDS, context)
        if not fields:
            raise QgsProcessingException("Select at least one field to aggregate.")

        # parameterAsEnum returns the selected INDEX; map it back to the name.
        op_index: int = self.parameterAsEnum(parameters, self.OPERATION, context)
        operation: str = self.OPERATIONS[op_index]

        out_path: str = self.parameterAsFileOutput(parameters, self.OUTPUT, context)

        # Request only the attribute columns we need — skip geometry entirely.
        layer_fields = source.fields()
        field_indices = [layer_fields.lookupField(name) for name in fields]
        request = QgsFeatureRequest().setSubsetOfAttributes(field_indices)
        request.setFlags(QgsFeatureRequest.NoGeometry)

        # Accumulate raw values per field, skipping NULLs.
        collected: dict[str, list[float]] = {name: [] for name in fields}
        total = source.featureCount() or 0
        step = 100.0 / total if total else 0.0

        for current, feature in enumerate(source.getFeatures(request)):
            if feedback.isCanceled():
                break
            for name in fields:
                value = feature[name]
                if value is not None and value != "":
                    collected[name].append(float(value))
            feedback.setProgress(int(current * step))

        results = {name: self._aggregate(values, operation)
                   for name, values in collected.items()}

        with open(out_path, "w", newline="", encoding="utf-8") as handle:
            writer = csv.writer(handle)
            writer.writerow(["field", "operation", "value"])
            for name, value in results.items():
                writer.writerow([name, operation, value])
                feedback.pushInfo(f"{operation}({name}) = {value}")

        return {self.OUTPUT: out_path}

    @staticmethod
    def _aggregate(values: list[float], operation: str) -> float:
        """Apply the named aggregation to a list of numeric values."""
        if operation == "count":
            return float(len(values))
        if not values:
            return 0.0
        if operation == "sum":
            return sum(values)
        if operation == "mean":
            return sum(values) / len(values)
        if operation == "min":
            return min(values)
        if operation == "max":
            return max(values)
        raise QgsProcessingException(f"Unknown operation: {operation}")

How the Parameters Reach Your Code

Both parameter types follow the same round trip: initAlgorithm() declares the parameter, the Processing dialog (or the CLI, or the modeler) supplies a raw value, and a parameterAs* helper decodes that raw value into a usable Python type inside processAlgorithm(). The diagram below traces that flow for the two parameters this page adds.

Enum and Field Parameter Flow Two horizontal lanes. The top lane follows an enum parameter: initAlgorithm declares options, the dialog supplies an index, parameterAsEnum returns an integer index. The bottom lane follows a field parameter: initAlgorithm declares it bound to INPUT, the dialog supplies field names, parameterAsFields returns a list of strings. initAlgorithm() dialog / CLI supplies processAlgorithm() QgsProcessing ParameterEnum options=[...] chosen index e.g. 1 parameterAsEnum() int index 1 -> "mean" QgsProcessing ParameterField parent=INPUT chosen column(s) "pop_2020" parameterAsFields() list[str] ["pop_2020"]

Architecture Breakdown

QgsProcessingParameterEnum — a constrained choice

An enum parameter renders as a combo box (or a checkable list when allowMultiple=True) and guarantees the algorithm only ever receives a value from your predefined set. The constructor signature you will use most often is:

python
QgsProcessingParameterEnum(
    name,                    # stable machine name, e.g. "OPERATION"
    description,             # human label shown in the dialog
    options=[],              # list[str] of choice labels
    allowMultiple=False,     # True renders checkboxes; value becomes a list
    defaultValue=None,       # INDEX (int) or list of indices — never the label
    optional=False,
    usesStaticStrings=False, # store the label string instead of its index
)

The single most common mistake is treating the value as a string. By default the framework stores and returns the index into options, not the label. That is why defaultValue=self.OPERATIONS.index("mean") passes an integer, and why processAlgorithm() calls self.parameterAsEnum(parameters, "OPERATION", context) to get an int and then maps it back with self.OPERATIONS[op_index]. Because the index is positional, never reorder options after publishing — an existing modeler file or qgis_process command that stored index 2 will silently point at a different operation.

Two parsing helpers exist depending on cardinality. Use parameterAsEnum() for a single choice (returns int) and parameterAsEnums() for allowMultiple=True (returns list[int]). If you set usesStaticStrings=True, the parameter stores the label text itself instead of its position — a good choice when the value is written to a config file or CLI script that humans read. In that mode you read it back with parameterAsEnumString() or parameterAsEnumStrings(), and defaultValue becomes the label string rather than an index.

QgsProcessingParameterField — a column bound to a layer

A field parameter gives the user a dropdown of attribute columns. Its defining feature is parentLayerParameterName, which must match the name of a layer or feature-source parameter declared earlier in initAlgorithm(). That link is what lets the dialog introspect the chosen layer and populate the field list live — pick a different input layer and the field dropdown repopulates automatically. Omit the parent name and the field box shows up empty, because the parameter has no layer to read columns from.

python
QgsProcessingParameterField(
    name,                              # e.g. "FIELDS"
    description,
    defaultValue=None,
    parentLayerParameterName="INPUT",  # ties the dropdown to the INPUT layer
    type=QgsProcessingParameterField.Any,  # Any | Numeric | String | DateTime
    allowMultiple=False,               # True lets the user pick several columns
    optional=False,
    defaultToAllFields=False,          # pre-select every field when multiple
)

The type argument filters which columns appear: Numeric hides text and date columns so the user cannot pick something the algorithm can not aggregate. Always read a field parameter with parameterAsFields(), which returns a list[str] in every case — even when allowMultiple=False you get a one-element list, so there is no separate scalar accessor to remember. (A parameterAsString() call also works for single fields and returns the bare name, but standardising on parameterAsFields() keeps your parsing uniform.) Convert names to indices for fast attribute access with layer.fields().lookupField(name), exactly as the runnable example does when building its QgsFeatureRequest.

Validation and optional parameters

Marking a parameter optional=True means the framework may hand processAlgorithm() an empty value, so you must handle the empty case yourself. An optional enum with no default returns -1 from parameterAsEnum() rather than raising, and an optional field parameter returns an empty list from parameterAsFields(). The example treats an empty field list as a hard error with QgsProcessingException, because there is nothing sensible to aggregate; choose per parameter whether “not supplied” is a valid state or a configuration mistake.

Validate immediately after resolving each parameter and raise QgsProcessingException on anything you cannot proceed with — the framework surfaces the message in the dialog, the log, and the CLI exit status alike. Do not defer validation into the feature loop, where a failure aborts a half-written output. For deeper guidance on parameter design across the whole algorithm, including sources, sinks, and expression parameters, see the parent Building Custom Processing Algorithms in PyQGIS guide.

Registration and Running It

Nothing about enum or field parameters changes registration — you still add the algorithm to a QgsProcessingProvider exactly as the reusable processing algorithm template describes:

python
from qgis.core import QgsApplication, QgsProcessingProvider


class EnterpriseProvider(QgsProcessingProvider):
    """Provider exposing the attribute-aggregator algorithm."""

    def id(self) -> str:
        return "enterprise_provider"

    def name(self) -> str:
        return "Enterprise Spatial Tools"

    def loadAlgorithms(self) -> None:
        self.addAlgorithm(AttributeAggregatorAlgorithm())


# In your plugin's initGui():
#   self._provider = EnterpriseProvider()
#   QgsApplication.processingRegistry().addProvider(self._provider)

Once registered, the algorithm appears in the Processing Toolbox with its combo box and field selector already wired up. To trigger it from a button rather than the toolbox, see Integrating Toolbars and Menu Actions in QGIS Plugins, which drives algorithms through processing.execAlgorithmDialog().

Because parameter values serialize cleanly, the same algorithm runs headlessly. Note how the enum is passed as its index and the multi-field parameter as a semicolon- or comma-joined string of column names:

bash
qgis_process run enterprise_provider:attributeaggregator \
  --INPUT=/data/census_tracts.gpkg \
  --FIELDS="pop_2020;households" \
  --OPERATION=1 \
  --OUTPUT=/data/aggregates.csv

For the full pattern of invoking algorithms without a display server, including QgsApplication bootstrap and reading results back in Python, see How to Run a Processing Algorithm Headlessly in Python.

Production Best Practices

  • Keep the enum options list as a class attribute and treat it as the single source of truth. Map indices back to names through that list, and never reorder it once published — stored indices are positional and silent to break.
  • Pass an integer defaultValue to enum parameters (or a label string only when usesStaticStrings=True). Passing the label in index mode selects the wrong option or none at all.
  • Prefer usesStaticStrings=True when the chosen value ends up in a human-readable config file, CLI script, or log — a stored "mean" survives reordering and reads far better than 1.
  • Always set parentLayerParameterName on a field parameter, and declare the parent source parameter before the field parameter in initAlgorithm() so the dialog can resolve it.
  • Constrain the field type (Numeric, String, DateTime) to the columns your logic can actually handle, so the dropdown never offers an unusable choice.
  • Read fields with parameterAsFields() and expect a list every time; convert names to attribute indices once with lookupField() and reuse them in the feature loop.
  • Validate resolved parameters up front and raise QgsProcessingException — especially for optional=True parameters, which can arrive as -1 (enum) or an empty list (fields).
  • Restrict the fetched attributes with QgsFeatureRequest().setSubsetOfAttributes() and NoGeometry when the algorithm only needs a few columns, cutting I/O on large layers.

Related