Skip to content

Processing algorithms and safe outputs

qgis_sdk.algorithms is the authoring surface decision-42 settled on: an algorithm is a plain function declared on a Provider, the same shape as a @job. Its first parameter is the AlgorithmContext the runtime injects, the remaining parameters are typed keyword inputs, and it returns the VectorLayerInfo of the layer it produced. Describing one never calls it, and every invocation builds a fresh context over a child scope that is closed again when the invocation ends:

from qgis_sdk.algorithms import describe_algorithm, invoke
from qgis_sdk.core import AlgorithmContext
from qgis_sdk.models import BufferRequest, CrsInput, LayerInput, Metres, VectorLayerInfo
from qgis_sdk.providers import Provider
tools = Provider(id="field_tools", title="Field tools")
@tools.algorithm(title="Buffer in a projected CRS", group="Geometry")
def buffer(
ctx: AlgorithmContext, layer_id: LayerInput, distance: Metres, crs: CrsInput
) -> VectorLayerInfo:
return ctx.algorithms.execute(
BufferRequest(layer_id=layer_id, distance_metres=distance, working_crs=crs)
)
described = describe_algorithm(buffer)
print(described.qualified_id, described.kinds, described.required)
# field_tools:buffer {'layer_id': 'layer', 'distance': 'metres', 'crs': 'crs'} ('layer_id', 'distance', 'crs')

invoke(function, ctx, **inputs) runs one from the context the caller already holds - the PluginContext of an action or the AlgorithmContext of another algorithm. A @job body cannot: it runs on a worker, so its WorkerContext carries neither algorithms nor project (decision-65) - invoke the algorithm from the subscriber that hears the job succeed. It reads algorithms, project and scope from that context, builds the invocation’s own AlgorithmContext, and adds the output to ctx.project after success:

# inside an @app.action function
output = invoke(
buffer, ctx, layer_id=source_id, distance=500.0, crs="EPSG:3857"
)
# invoke already added `output` to the project - after success only.

progress= and cancellation= left out are inherited from a job or algorithm context (so a job’s cancellation reaches the algorithm it runs); a plugin context has neither, and the call runs silently and uncancelled. add_to_project=False is for a caller that owns the destination itself: the Processing bridge hands the layer to a feature sink, and the reference workflow styles the output before presenting it. A context without the service a call needs refuses by name (QgisSdkUnsupportedCapabilityError("algorithms")) before anything runs.

No execution context is injected at decoration time, so a unit test needs no runner: call buffer(fake_context, layer_id=..., distance=..., crs=...) with an AlgorithmContext built over the qgis_sdk.testing fakes.

The input schemas come from the same qgis_sdk.declarations generator that describes plugin actions, so an algorithm’s inputs are held to one documented typing subset. AlgorithmDescription carries parameters, required, annotations, kinds (input name to host parameter kind), inputs_schema, result_type / result_schema, group, provider_id and qualified_id; to_dict()["inputs"] is the object schema itself.

Non-destructive by construction (decision-19)

Section titled “Non-destructive by construction (decision-19)”

Algorithms.execute directs the built-in buffer and centroid operations to a new SDK-owned output, then opens it as a layer. The SDK attempts to remove its run directory on failure and its intermediate buffer reprojection on success; filesystem cleanup can itself fail. The reference native algorithms are tested for source preservation, but Processing providers are trusted code, not a sandbox: the SDK cannot prevent a provider from editing inputs or writing elsewhere, or roll back those side effects. invoke adds the returned output to the project only after success, type validation and a cancellation check; an algorithm or provider may still have its own project side effects.

Explicit CRS and units where a distance is involved

Section titled “Explicit CRS and units where a distance is involved”

BufferRequest requires working_crs and distance_metres; the native backend refuses a working CRS that is geographic (a distance there would be degrees) or whose units are not metres, instead of converting behind the caller’s back. CentroidRequest has no distance and therefore no working CRS: decision-19 requires a projected CRS where the operation has one, not everywhere.

Two honest limits of this slice: cancellation is observed at phase boundaries (before the function runs, and after it before the project is touched), not by aborting a native operation mid-flight; and an algorithm’s result is a vector layer - other layer kinds arrive with the services that produce them.

Any registered algorithm: list, describe, run and the generated registry

Section titled “Any registered algorithm: list, describe, run and the generated registry”

BufferRequest and CentroidRequest are the hand-written predecessors of a generated family. Since 0.7.0 (decision-64) ctx.algorithms reaches every algorithm the host’s Processing registry holds - native, qgis, gdal, 3d, pdal, GRASS, SAGA, a third-party plugin’s - through one surface:

from qgis_sdk.processing import native
providers = ctx.algorithms.providers() # ProviderInfo per registered provider
summaries = ctx.algorithms.list(provider="gdal") # AlgorithmInfo, help and all
info = ctx.algorithms.describe("native:buffer") # parameters, outputs, defaults
# Three spellings of one run; all return a RunResult.
result = ctx.algorithms.run(native.Buffer(input=layer.id, distance=25.0))
result = ctx.algorithms.run("native:buffer", {"INPUT": layer.id, "DISTANCE": 25.0})
result = ctx.algorithms.run(AlgorithmRun("native:buffer", {"INPUT": layer.id}))
buffered = result.vector("OUTPUT") # an owned VectorLayerInfo
stats = ctx.algorithms.run(
"native:basicstatisticsforfields", {"INPUT_LAYER": layer.id, "FIELD_NAME": "id"}
)
stats.value("COUNT") # 3
report = stats.file("OUTPUT_HTML_FILE") # a read-only FileRef; ctx.files.resolve(report)

The request is validated against the algorithm’s description before execution (QgisSdkAlgorithmRejectedError describes a refusal; an unknown id is QgisSdkAlgorithmNotFoundError). A layer input is an owned layer id, a FileRef, or a path the plugin already holds. A destination left out is allocated under a fresh SDK-owned run directory: vector sinks return as VectorLayerInfo, files and folders as read-only FileRef objects, and scalar outputs as values.

Named destination safety (unreleased change). A run may name at most one new local GeoJSON or GeoPackage vector destination, as a path string or a write-authorized FileRef. Existing files, including input aliases and write-granted files with overwrite permission, are refused; so are multiple, remote, memory, folder, raster and other named destinations. The native adapter writes to a private directory beside the destination, verifies a single readable independent file with no sidecars, then atomically publishes it without replacing an existing name. This requires hard-link support; a FileRef write grant is rechecked before publication. Leave a destination unspecified and export the resulting vector with ctx.layers.export_vector when you need explicit overwrite consent; for other output types use SDK-allocated output and handle publication separately with suitable safeguards.

Cleanup of SDK-owned run and staging directories is best-effort. The SDK never intentionally deletes an existing destination, but a complete file already published may remain if later result conversion fails. Keep paths stable during a run. Processing providers are trusted code and can modify inputs, project state or other files outside the SDK’s directories; the SDK cannot prevent or roll back such side effects. The fake uses scripted answers and validates destination requests but does not run Processing or prove filesystem behavior. run is synchronous on the GUI thread, like processing.run; a long algorithm belongs in a @job.

qgis_sdk.processing.native, .qgis, .gdal, .p3d and .pdal are generated, never edited: each carries a TypedDict per algorithm (the shape run(id, params) is typed by), a request class per algorithm (native.Buffer, native.Centroids, … - frozen dataclasses whose fields are the parameters in snake case, destinations defaulting to None) and a REGISTRY of AlgorithmInfo. They come from a snapshot of the registry of the QGIS the SDK release was qualified with (qgis_sdk/processing/catalogue.json; QGIS_VERSION says which), kept fresh by scripts/generate_processing_snapshot.py and a CI drift check. A host whose registry differs is what describe reports; the host’s description, not the snapshot, is what a run is validated against.

Over the fakes, FakeAlgorithms.list/describe answer from that snapshot (or a Catalogue the test passes as catalogue=), and run answers from what the test scripted with script_run(id, outputs) - a mapping of output name to a Path (becomes the registered layer or a FileRef, by the output’s kind) or a scalar, or a callable of the AlgorithmRun - and records every run in .runs. A run nobody scripted fails with QgisSdkAlgorithmFailedError, never with an invented layer.

A project that depends on a provider the runtime does not ship generates its own modules with the developer tools:

Terminal window
qgis-sdk processing generate --provider grass --provider saga # reads the project's QGIS
qgis-sdk processing generate --check # CI: fail on drift

The reader runs inside the project’s QGIS interpreter (the tools never import QGIS); the output is <plugin package>/processing/ in Python (--out) and web/src/processing.schema.ts for the page (--web-out), each skippable (--no-python, --no-web). Pass --catalogue <json> to render from a catalogue a reader already wrote.