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.
"""
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.
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:
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.
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:
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:
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
defaultValueto enum parameters (or a label string only whenusesStaticStrings=True). Passing the label in index mode selects the wrong option or none at all. - Prefer
usesStaticStrings=Truewhen the chosen value ends up in a human-readable config file, CLI script, or log — a stored"mean"survives reordering and reads far better than1. - Always set
parentLayerParameterNameon a field parameter, and declare the parent source parameter before the field parameter ininitAlgorithm()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 withlookupField()and reuse them in the feature loop. - Validate resolved parameters up front and raise
QgsProcessingException— especially foroptional=Trueparameters, which can arrive as-1(enum) or an empty list (fields). - Restrict the fetched attributes with
QgsFeatureRequest().setSubsetOfAttributes()andNoGeometrywhen the algorithm only needs a few columns, cutting I/O on large layers.
Related
- Building Custom Processing Algorithms in PyQGIS — parent guide covering the full algorithm contract, sinks, and provider registration
- Creating a Reusable PyQGIS Processing Algorithm Template — the drop-in scaffold these enum and field parameters plug into
- How to Run a Processing Algorithm Headlessly in Python — invoking your parameterised algorithm from a standalone script or CI runner