[{"data":1,"prerenderedAt":2377},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Fwrite-processing-script-with-alg-decorator":3},{"id":4,"title":5,"body":6,"description":2366,"extension":2367,"meta":2368,"navigation":304,"path":2373,"seo":2374,"stem":2375,"__hash__":2376},"docs\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Fwrite-processing-script-with-alg-decorator\u002Findex.md","Write a Processing Script with the @alg Decorator",{"type":7,"value":8,"toc":2354},"minimark",[9,13,53,62,207,212,234,238,246,944,978,982,992,1102,1759,1791,1795,1806,2067,2084,2159,2163,2191,2195,2213,2217,2261,2265,2274,2278,2288,2302,2311,2317,2321,2350],[10,11,5],"h1",{"id":12},"write-a-processing-script-with-the-alg-decorator",[14,15,16,17,21,22,25,26,25,29,25,32,25,35,25,38,25,41,44,45,48,49,52],"p",{},"A full ",[18,19,20],"code",{},"QgsProcessingAlgorithm"," subclass needs a dozen methods before it does anything: ",[18,23,24],{},"name",", ",[18,27,28],{},"displayName",[18,30,31],{},"group",[18,33,34],{},"groupId",[18,36,37],{},"createInstance",[18,39,40],{},"initAlgorithm",[18,42,43],{},"processAlgorithm"," and more. For a one-off tool that a team drops into the Processing Toolbox — reproject and clip to the project boundary, flag parcels below a minimum area, count features per category — that is more ceremony than logic. The ",[18,46,47],{},"@alg"," decorator writes the boilerplate for you: decorate a function, declare its inputs and outputs, and it becomes a script algorithm with a dialog, batch mode, model designer support and ",[18,50,51],{},"processing.run"," access.",[14,54,55,56,61],{},"This recipe belongs to ",[57,58,60],"a",{"href":59},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002F","Processing Provider Plugins for QGIS",". It writes a decorated script, covers the common parameter types, writes features to a sink with progress and cancellation, and explains when to move to a full class.",[14,63,64],{},[65,66,71,75,79,86,103,112,122,128,135,140,144,148,153,157,161,168,175,180,186,190,194,198,202],"svg",{"viewBox":67,"role":68,"ariaLabel":69,"xmlns":70},"0 0 760 300","img","What the alg decorator generates: a decorated function with input and output declarations becomes a script algorithm with metadata methods, a parameter dialog, batch processing, model designer support and a processing.run identifier","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[72,73,74],"title",{},"A function in, a full algorithm out",[76,77,78],"desc",{},"On the left, a Python function decorated with alg for name, label and group, plus alg.input decorators for a source, a distance and a sink, and an alg.output for a number. The decorator builds a QgsProcessingAlgorithm with name, displayName, group, initAlgorithm and processAlgorithm filled in. On the right, what users get: a Toolbox entry under Scripts, a generated dialog, batch mode, availability in the model designer, and the identifier script:flag_small_parcels for processing.run.",[80,81],"rect",{"x":82,"y":82,"width":83,"height":84,"fill":85},"0","760","300","#f6f3ea",[87,88,89],"defs",{},[90,91,98],"marker",{"id":92,"viewBox":93,"refX":94,"refY":95,"markerWidth":96,"markerHeight":96,"orient":97},"algGenArrow","0 0 10 10","8","5","7","auto-start-reverse",[99,100],"path",{"d":101,"fill":102},"M0 0 L10 5 L0 10 z","#2f3b35",[104,105,111],"text",{"x":106,"y":107,"style":108,"fill":109,"textAnchor":110},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Declare inputs and outputs, get the rest free",[80,113],{"x":114,"y":115,"width":116,"height":117,"rx":118,"fill":119,"stroke":120,"style":121},"24","50","280","226","10","#fffdf7","#59645f","stroke-width:2",[104,123,127],{"x":124,"y":125,"style":126,"fill":109,"textAnchor":110},"164","74","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","your script",[104,129,134],{"x":130,"y":131,"style":132,"fill":133},"44","102","font-size:9.5px;font-family:monospace","#0f766e","@alg(name=…, label=…)",[104,136,139],{"x":130,"y":137,"style":132,"fill":138},"124","#2563eb","@alg.input(SOURCE …)",[104,141,143],{"x":130,"y":142,"style":132,"fill":138},"146","@alg.input(NUMBER …)",[104,145,147],{"x":130,"y":146,"style":132,"fill":138},"168","@alg.input(SINK …)",[104,149,152],{"x":130,"y":150,"style":132,"fill":151},"190","#b45309","@alg.output(NUMBER …)",[104,154,156],{"x":130,"y":155,"style":132,"fill":102},"218","def run(instance, parameters,",[104,158,160],{"x":130,"y":159,"style":132,"fill":102},"238","        context, feedback, inputs):",[162,163],"line",{"x1":164,"y1":165,"x2":166,"y2":165,"stroke":102,"style":167},"304","162","352","stroke-width:2;marker-end:url(#algGenArrow)",[80,169],{"x":170,"y":115,"width":171,"height":117,"rx":118,"fill":172,"stroke":173,"style":174},"360","376","#e8efe6","#15803d","stroke-width:2.5",[104,176,179],{"x":177,"y":125,"style":126,"fill":178,"textAnchor":110},"548","#166534","what users get",[104,181,185],{"x":182,"y":183,"style":184,"fill":102},"384","106","font-size:10.5px;font-family:sans-serif","✓ Toolbox entry under Scripts",[104,187,189],{"x":182,"y":188,"style":184,"fill":102},"132","✓ generated parameter dialog",[104,191,193],{"x":182,"y":192,"style":184,"fill":102},"158","✓ batch processing mode",[104,195,197],{"x":182,"y":196,"style":184,"fill":102},"184","✓ usable in the model designer",[104,199,201],{"x":182,"y":200,"style":184,"fill":102},"210","✓ help panel from the docstring",[104,203,206],{"x":182,"y":204,"style":205,"fill":102},"244","font-size:10px;font-family:monospace","processing.run(\"script:flag_small_parcels\", …)",[208,209,211],"h2",{"id":210},"prerequisites","Prerequisites",[213,214,215,223,226],"ul",{},[216,217,218,222],"li",{},[219,220,221],"strong",{},"QGIS 3.40 LTR"," or newer, or the QGIS 4 series. The decorator has been available since 3.6.",[216,224,225],{},"A task that fits in one function. The decorator suits scripts with a few inputs and outputs; see the last section for when to use a class.",[216,227,228,229,233],{},"Familiarity with running algorithms from Python, as in ",[57,230,232],{"href":231},"\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002Frun-processing-algorithm-from-script\u002F","running a Processing algorithm from a script",".",[208,235,237],{"id":236},"a-complete-decorated-script","A complete decorated script",[14,239,240,241,245],{},"Save a script like this in the Processing scripts folder — ",[242,243,244],"em",{},"Processing Toolbox → Scripts → Create New Script"," opens an editor that saves there — and it appears in the Toolbox immediately.",[247,248,253],"pre",{"className":249,"code":250,"language":251,"meta":252,"style":252},"language-python shiki shiki-themes github-dark","from qgis.core import QgsFeatureSink, QgsProcessingException, QgsField, QgsFields\nfrom qgis.PyQt.QtCore import QMetaType\nfrom qgis.processing import alg\n\n\n@alg(name=\"flag_small_parcels\", label=\"Flag small parcels\",\n     group=\"parcel_qa\", group_label=\"Parcel quality checks\")\n@alg.input(type=alg.SOURCE, name=\"INPUT\", label=\"Parcels\", types=[alg.TYPE_VECTOR_POLYGON])\n@alg.input(type=alg.NUMBER, name=\"MIN_AREA\", label=\"Minimum area (m²)\",\n           default=50.0, minValue=0.0)\n@alg.input(type=alg.SINK, name=\"OUTPUT\", label=\"Flagged parcels\")\n@alg.output(type=alg.NUMBER, name=\"FLAGGED\", label=\"Number flagged\")\ndef flag_small_parcels(instance, parameters, context, feedback, inputs):\n    \"\"\"\n    Copies every parcel and adds a small_parcel field that is true where the\n    area is below the minimum. Area is planar, in the layer's CRS units.\n    \"\"\"\n    source = instance.parameterAsSource(parameters, \"INPUT\", context)\n    if source is None:\n        raise QgsProcessingException(instance.invalidSourceError(parameters, \"INPUT\"))\n    min_area = instance.parameterAsDouble(parameters, \"MIN_AREA\", context)\n\n    fields = QgsFields(source.fields())\n    fields.append(QgsField(\"small_parcel\", QMetaType.Type.Bool))\n    sink, dest_id = instance.parameterAsSink(\n        parameters, \"OUTPUT\", context, fields, source.wkbType(), source.sourceCrs())\n    if sink is None:\n        raise QgsProcessingException(instance.invalidSinkError(parameters, \"OUTPUT\"))\n\n    total = source.featureCount() or 1\n    flagged = 0\n    for i, feature in enumerate(source.getFeatures()):\n        if feedback.isCanceled():\n            break\n        is_small = feature.geometry().area() \u003C min_area\n        flagged += int(is_small)\n        feature.setFields(fields, False)\n        feature.setAttributes(feature.attributes() + [is_small])\n        sink.addFeature(feature, QgsFeatureSink.FastInsert)\n        feedback.setProgress(100 * i \u002F total)\n\n    feedback.pushInfo(f\"{flagged} of {total} parcels below {min_area} m²\")\n    return {\"OUTPUT\": dest_id, \"FLAGGED\": flagged}\n","python","",[18,254,255,273,286,299,306,311,343,367,421,457,480,516,552,564,570,576,582,587,603,621,635,650,655,666,678,689,700,714,726,731,748,759,777,786,792,809,824,835,847,853,874,879,925],{"__ignoreMap":252},[256,257,259,263,267,270],"span",{"class":162,"line":258},1,[256,260,262],{"class":261},"snl16","from",[256,264,266],{"class":265},"s95oV"," qgis.core ",[256,268,269],{"class":261},"import",[256,271,272],{"class":265}," QgsFeatureSink, QgsProcessingException, QgsField, QgsFields\n",[256,274,276,278,281,283],{"class":162,"line":275},2,[256,277,262],{"class":261},[256,279,280],{"class":265}," qgis.PyQt.QtCore ",[256,282,269],{"class":261},[256,284,285],{"class":265}," QMetaType\n",[256,287,289,291,294,296],{"class":162,"line":288},3,[256,290,262],{"class":261},[256,292,293],{"class":265}," qgis.processing ",[256,295,269],{"class":261},[256,297,298],{"class":265}," alg\n",[256,300,302],{"class":162,"line":301},4,[256,303,305],{"emptyLinePlaceholder":304},true,"\n",[256,307,309],{"class":162,"line":308},5,[256,310,305],{"emptyLinePlaceholder":304},[256,312,314,317,320,323,326,330,332,335,337,340],{"class":162,"line":313},6,[256,315,47],{"class":316},"svObZ",[256,318,319],{"class":265},"(",[256,321,24],{"class":322},"s9osk",[256,324,325],{"class":261},"=",[256,327,329],{"class":328},"sU2Wk","\"flag_small_parcels\"",[256,331,25],{"class":265},[256,333,334],{"class":322},"label",[256,336,325],{"class":261},[256,338,339],{"class":328},"\"Flag small parcels\"",[256,341,342],{"class":265},",\n",[256,344,346,349,351,354,356,359,361,364],{"class":162,"line":345},7,[256,347,348],{"class":322},"     group",[256,350,325],{"class":261},[256,352,353],{"class":328},"\"parcel_qa\"",[256,355,25],{"class":265},[256,357,358],{"class":322},"group_label",[256,360,325],{"class":261},[256,362,363],{"class":328},"\"Parcel quality checks\"",[256,365,366],{"class":265},")\n",[256,368,370,373,375,378,380,383,387,389,391,393,396,398,400,402,405,407,410,412,415,418],{"class":162,"line":369},8,[256,371,372],{"class":316},"@alg.input",[256,374,319],{"class":265},[256,376,377],{"class":322},"type",[256,379,325],{"class":261},[256,381,382],{"class":265},"alg.",[256,384,386],{"class":385},"sDLfK","SOURCE",[256,388,25],{"class":265},[256,390,24],{"class":322},[256,392,325],{"class":261},[256,394,395],{"class":328},"\"INPUT\"",[256,397,25],{"class":265},[256,399,334],{"class":322},[256,401,325],{"class":261},[256,403,404],{"class":328},"\"Parcels\"",[256,406,25],{"class":265},[256,408,409],{"class":322},"types",[256,411,325],{"class":261},[256,413,414],{"class":265},"[alg.",[256,416,417],{"class":385},"TYPE_VECTOR_POLYGON",[256,419,420],{"class":265},"])\n",[256,422,424,426,428,430,432,434,437,439,441,443,446,448,450,452,455],{"class":162,"line":423},9,[256,425,372],{"class":316},[256,427,319],{"class":265},[256,429,377],{"class":322},[256,431,325],{"class":261},[256,433,382],{"class":265},[256,435,436],{"class":385},"NUMBER",[256,438,25],{"class":265},[256,440,24],{"class":322},[256,442,325],{"class":261},[256,444,445],{"class":328},"\"MIN_AREA\"",[256,447,25],{"class":265},[256,449,334],{"class":322},[256,451,325],{"class":261},[256,453,454],{"class":328},"\"Minimum area (m²)\"",[256,456,342],{"class":265},[256,458,460,463,465,468,470,473,475,478],{"class":162,"line":459},10,[256,461,462],{"class":322},"           default",[256,464,325],{"class":261},[256,466,467],{"class":385},"50.0",[256,469,25],{"class":265},[256,471,472],{"class":322},"minValue",[256,474,325],{"class":261},[256,476,477],{"class":385},"0.0",[256,479,366],{"class":265},[256,481,483,485,487,489,491,493,496,498,500,502,505,507,509,511,514],{"class":162,"line":482},11,[256,484,372],{"class":316},[256,486,319],{"class":265},[256,488,377],{"class":322},[256,490,325],{"class":261},[256,492,382],{"class":265},[256,494,495],{"class":385},"SINK",[256,497,25],{"class":265},[256,499,24],{"class":322},[256,501,325],{"class":261},[256,503,504],{"class":328},"\"OUTPUT\"",[256,506,25],{"class":265},[256,508,334],{"class":322},[256,510,325],{"class":261},[256,512,513],{"class":328},"\"Flagged parcels\"",[256,515,366],{"class":265},[256,517,519,522,524,526,528,530,532,534,536,538,541,543,545,547,550],{"class":162,"line":518},12,[256,520,521],{"class":316},"@alg.output",[256,523,319],{"class":265},[256,525,377],{"class":322},[256,527,325],{"class":261},[256,529,382],{"class":265},[256,531,436],{"class":385},[256,533,25],{"class":265},[256,535,24],{"class":322},[256,537,325],{"class":261},[256,539,540],{"class":328},"\"FLAGGED\"",[256,542,25],{"class":265},[256,544,334],{"class":322},[256,546,325],{"class":261},[256,548,549],{"class":328},"\"Number flagged\"",[256,551,366],{"class":265},[256,553,555,558,561],{"class":162,"line":554},13,[256,556,557],{"class":261},"def",[256,559,560],{"class":316}," flag_small_parcels",[256,562,563],{"class":265},"(instance, parameters, context, feedback, inputs):\n",[256,565,567],{"class":162,"line":566},14,[256,568,569],{"class":328},"    \"\"\"\n",[256,571,573],{"class":162,"line":572},15,[256,574,575],{"class":328},"    Copies every parcel and adds a small_parcel field that is true where the\n",[256,577,579],{"class":162,"line":578},16,[256,580,581],{"class":328},"    area is below the minimum. Area is planar, in the layer's CRS units.\n",[256,583,585],{"class":162,"line":584},17,[256,586,569],{"class":328},[256,588,590,593,595,598,600],{"class":162,"line":589},18,[256,591,592],{"class":265},"    source ",[256,594,325],{"class":261},[256,596,597],{"class":265}," instance.parameterAsSource(parameters, ",[256,599,395],{"class":328},[256,601,602],{"class":265},", context)\n",[256,604,606,609,612,615,618],{"class":162,"line":605},19,[256,607,608],{"class":261},"    if",[256,610,611],{"class":265}," source ",[256,613,614],{"class":261},"is",[256,616,617],{"class":385}," None",[256,619,620],{"class":265},":\n",[256,622,624,627,630,632],{"class":162,"line":623},20,[256,625,626],{"class":261},"        raise",[256,628,629],{"class":265}," QgsProcessingException(instance.invalidSourceError(parameters, ",[256,631,395],{"class":328},[256,633,634],{"class":265},"))\n",[256,636,638,641,643,646,648],{"class":162,"line":637},21,[256,639,640],{"class":265},"    min_area ",[256,642,325],{"class":261},[256,644,645],{"class":265}," instance.parameterAsDouble(parameters, ",[256,647,445],{"class":328},[256,649,602],{"class":265},[256,651,653],{"class":162,"line":652},22,[256,654,305],{"emptyLinePlaceholder":304},[256,656,658,661,663],{"class":162,"line":657},23,[256,659,660],{"class":265},"    fields ",[256,662,325],{"class":261},[256,664,665],{"class":265}," QgsFields(source.fields())\n",[256,667,669,672,675],{"class":162,"line":668},24,[256,670,671],{"class":265},"    fields.append(QgsField(",[256,673,674],{"class":328},"\"small_parcel\"",[256,676,677],{"class":265},", QMetaType.Type.Bool))\n",[256,679,681,684,686],{"class":162,"line":680},25,[256,682,683],{"class":265},"    sink, dest_id ",[256,685,325],{"class":261},[256,687,688],{"class":265}," instance.parameterAsSink(\n",[256,690,692,695,697],{"class":162,"line":691},26,[256,693,694],{"class":265},"        parameters, ",[256,696,504],{"class":328},[256,698,699],{"class":265},", context, fields, source.wkbType(), source.sourceCrs())\n",[256,701,703,705,708,710,712],{"class":162,"line":702},27,[256,704,608],{"class":261},[256,706,707],{"class":265}," sink ",[256,709,614],{"class":261},[256,711,617],{"class":385},[256,713,620],{"class":265},[256,715,717,719,722,724],{"class":162,"line":716},28,[256,718,626],{"class":261},[256,720,721],{"class":265}," QgsProcessingException(instance.invalidSinkError(parameters, ",[256,723,504],{"class":328},[256,725,634],{"class":265},[256,727,729],{"class":162,"line":728},29,[256,730,305],{"emptyLinePlaceholder":304},[256,732,734,737,739,742,745],{"class":162,"line":733},30,[256,735,736],{"class":265},"    total ",[256,738,325],{"class":261},[256,740,741],{"class":265}," source.featureCount() ",[256,743,744],{"class":261},"or",[256,746,747],{"class":385}," 1\n",[256,749,751,754,756],{"class":162,"line":750},31,[256,752,753],{"class":265},"    flagged ",[256,755,325],{"class":261},[256,757,758],{"class":385}," 0\n",[256,760,762,765,768,771,774],{"class":162,"line":761},32,[256,763,764],{"class":261},"    for",[256,766,767],{"class":265}," i, feature ",[256,769,770],{"class":261},"in",[256,772,773],{"class":385}," enumerate",[256,775,776],{"class":265},"(source.getFeatures()):\n",[256,778,780,783],{"class":162,"line":779},33,[256,781,782],{"class":261},"        if",[256,784,785],{"class":265}," feedback.isCanceled():\n",[256,787,789],{"class":162,"line":788},34,[256,790,791],{"class":261},"            break\n",[256,793,795,798,800,803,806],{"class":162,"line":794},35,[256,796,797],{"class":265},"        is_small ",[256,799,325],{"class":261},[256,801,802],{"class":265}," feature.geometry().area() ",[256,804,805],{"class":261},"\u003C",[256,807,808],{"class":265}," min_area\n",[256,810,812,815,818,821],{"class":162,"line":811},36,[256,813,814],{"class":265},"        flagged ",[256,816,817],{"class":261},"+=",[256,819,820],{"class":385}," int",[256,822,823],{"class":265},"(is_small)\n",[256,825,827,830,833],{"class":162,"line":826},37,[256,828,829],{"class":265},"        feature.setFields(fields, ",[256,831,832],{"class":385},"False",[256,834,366],{"class":265},[256,836,838,841,844],{"class":162,"line":837},38,[256,839,840],{"class":265},"        feature.setAttributes(feature.attributes() ",[256,842,843],{"class":261},"+",[256,845,846],{"class":265}," [is_small])\n",[256,848,850],{"class":162,"line":849},39,[256,851,852],{"class":265},"        sink.addFeature(feature, QgsFeatureSink.FastInsert)\n",[256,854,856,859,862,865,868,871],{"class":162,"line":855},40,[256,857,858],{"class":265},"        feedback.setProgress(",[256,860,861],{"class":385},"100",[256,863,864],{"class":261}," *",[256,866,867],{"class":265}," i ",[256,869,870],{"class":261},"\u002F",[256,872,873],{"class":265}," total)\n",[256,875,877],{"class":162,"line":876},41,[256,878,305],{"emptyLinePlaceholder":304},[256,880,882,885,888,891,894,897,900,903,905,908,910,913,915,918,920,923],{"class":162,"line":881},42,[256,883,884],{"class":265},"    feedback.pushInfo(",[256,886,887],{"class":261},"f",[256,889,890],{"class":328},"\"",[256,892,893],{"class":385},"{",[256,895,896],{"class":265},"flagged",[256,898,899],{"class":385},"}",[256,901,902],{"class":328}," of ",[256,904,893],{"class":385},[256,906,907],{"class":265},"total",[256,909,899],{"class":385},[256,911,912],{"class":328}," parcels below ",[256,914,893],{"class":385},[256,916,917],{"class":265},"min_area",[256,919,899],{"class":385},[256,921,922],{"class":328}," m²\"",[256,924,366],{"class":265},[256,926,928,931,934,936,939,941],{"class":162,"line":927},43,[256,929,930],{"class":261},"    return",[256,932,933],{"class":265}," {",[256,935,504],{"class":328},[256,937,938],{"class":265},": dest_id, ",[256,940,540],{"class":328},[256,942,943],{"class":265},": flagged}\n",[14,945,946,949,950,953,954,957,958,960,961,963,964,966,967,970,971,974,975,977],{},[219,947,948],{},"Breakdown:"," Decorators are applied bottom-up, but read top-down they describe the algorithm: identity and group first, then inputs, then outputs. The function signature is fixed — ",[18,951,952],{},"instance"," is the generated algorithm object, which is where the ",[18,955,956],{},"parameterAs…"," helpers live. ",[18,959,386],{}," accepts any vector layer or file and respects \"selected features only\"; ",[18,962,409],{}," restricts it to polygons in the dialog. A ",[18,965,495],{}," is an output the user chooses — a temporary layer, a file, a GeoPackage table — and ",[18,968,969],{},"parameterAsSink"," creates it with the fields, geometry type and CRS you give. The docstring becomes the help text shown beside the dialog. Returning a dictionary whose keys match the declared outputs is what makes ",[18,972,973],{},"FLAGGED"," available to models and to ",[18,976,51],{}," callers.",[208,979,981],{"id":980},"parameter-types-you-will-use-most","Parameter types you will use most",[14,983,984,985,988,989,991],{},"The decorator exposes the common Processing parameter types as constants on ",[18,986,987],{},"alg",". Each maps to a widget in the dialog and a ",[18,990,956],{}," method in the function.",[14,993,994],{},[65,995,998,1001,1004,1007,1011,1016,1021,1025,1029,1034,1038,1042,1045,1049,1052,1055,1058,1061,1064,1067,1070,1073,1076,1079,1082,1086,1089,1092,1096,1099],{"viewBox":996,"role":68,"ariaLabel":997,"xmlns":70},"0 0 760 270","Common alg parameter types with their dialog widget and the method that reads them: SOURCE, VECTOR_LAYER, RASTER_LAYER, NUMBER, DISTANCE, ENUM, FIELD, BOOL, STRING, SINK and FILE_DEST",[72,999,1000],{},"Declare the type, read it with the matching method",[76,1002,1003],{},"A table. SOURCE, a layer combo with selected-features option, read with parameterAsSource. RASTER_LAYER, a raster combo, parameterAsRasterLayer. NUMBER, a spin box, parameterAsDouble or parameterAsInt. DISTANCE, a spin box with units linked to a layer, parameterAsDouble. ENUM, a drop-down, parameterAsEnum. FIELD, a field combo linked to a layer, parameterAsFields. BOOL, a checkbox, parameterAsBool. SINK, an output layer chooser, parameterAsSink. FILE_DEST, a file save chooser, parameterAsFileOutput.",[80,1005],{"x":82,"y":82,"width":83,"height":1006,"fill":85},"270",[104,1008,1010],{"x":106,"y":1009,"style":108,"fill":109,"textAnchor":110},"26","alg type → dialog widget → reader",[80,1012],{"x":114,"y":1013,"width":1014,"height":1015,"rx":118,"fill":119,"stroke":120,"style":121},"40","712","214",[104,1017,377],{"x":1018,"y":1019,"style":1020,"fill":109,"textAnchor":110},"140","64","text-anchor:middle;font-size:10.5px;font-weight:bold;font-family:sans-serif",[104,1022,1024],{"x":1023,"y":1019,"style":1020,"fill":109,"textAnchor":110},"370","widget",[104,1026,1028],{"x":1027,"y":1019,"style":1020,"fill":109,"textAnchor":110},"600","read with",[162,1030],{"x1":1013,"y1":125,"x2":1031,"y2":125,"stroke":1032,"style":1033},"720","#d9d3c4","stroke-width:1.5",[104,1035,386],{"x":1018,"y":1036,"style":1037,"fill":138,"textAnchor":110},"96","text-anchor:middle;font-size:10px;font-family:monospace",[104,1039,1041],{"x":1023,"y":1036,"style":1040,"fill":102,"textAnchor":110},"text-anchor:middle;font-size:10px;font-family:sans-serif","layer + selected only",[104,1043,1044],{"x":1027,"y":1036,"style":1037,"fill":102,"textAnchor":110},"parameterAsSource",[104,1046,1048],{"x":1018,"y":1047,"style":1037,"fill":138,"textAnchor":110},"118","RASTER_LAYER",[104,1050,1051],{"x":1023,"y":1047,"style":1040,"fill":102,"textAnchor":110},"raster combo",[104,1053,1054],{"x":1027,"y":1047,"style":1037,"fill":102,"textAnchor":110},"parameterAsRasterLayer",[104,1056,1057],{"x":1018,"y":1018,"style":1037,"fill":138,"textAnchor":110},"NUMBER \u002F DISTANCE",[104,1059,1060],{"x":1023,"y":1018,"style":1040,"fill":102,"textAnchor":110},"spin box (with units)",[104,1062,1063],{"x":1027,"y":1018,"style":1037,"fill":102,"textAnchor":110},"parameterAsDouble",[104,1065,1066],{"x":1018,"y":165,"style":1037,"fill":138,"textAnchor":110},"ENUM",[104,1068,1069],{"x":1023,"y":165,"style":1040,"fill":102,"textAnchor":110},"drop-down",[104,1071,1072],{"x":1027,"y":165,"style":1037,"fill":102,"textAnchor":110},"parameterAsEnum",[104,1074,1075],{"x":1018,"y":196,"style":1037,"fill":138,"textAnchor":110},"FIELD",[104,1077,1078],{"x":1023,"y":196,"style":1040,"fill":102,"textAnchor":110},"field combo from a layer",[104,1080,1081],{"x":1027,"y":196,"style":1037,"fill":102,"textAnchor":110},"parameterAsFields",[104,1083,1085],{"x":1018,"y":1084,"style":1037,"fill":138,"textAnchor":110},"206","BOOL \u002F STRING",[104,1087,1088],{"x":1023,"y":1084,"style":1040,"fill":102,"textAnchor":110},"checkbox \u002F text box",[104,1090,1091],{"x":1027,"y":1084,"style":1037,"fill":102,"textAnchor":110},"parameterAsBool \u002F String",[104,1093,1095],{"x":1018,"y":1094,"style":1037,"fill":151,"textAnchor":110},"228","SINK \u002F FILE_DEST",[104,1097,1098],{"x":1023,"y":1094,"style":1040,"fill":102,"textAnchor":110},"output chooser",[104,1100,1101],{"x":1027,"y":1094,"style":1037,"fill":102,"textAnchor":110},"parameterAsSink \u002F FileOutput",[247,1103,1105],{"className":249,"code":1104,"language":251,"meta":252,"style":252},"@alg(name=\"summarise_by_category\", label=\"Summarise by category\",\n     group=\"parcel_qa\", group_label=\"Parcel quality checks\")\n@alg.input(type=alg.SOURCE, name=\"INPUT\", label=\"Input layer\")\n@alg.input(type=alg.FIELD, name=\"CATEGORY\", label=\"Category field\",\n           parentLayerParameterName=\"INPUT\")\n@alg.input(type=alg.ENUM, name=\"STAT\", label=\"Statistic\",\n           options=[\"count\", \"total area\", \"mean area\"], default=0)\n@alg.input(type=alg.DISTANCE, name=\"TOLERANCE\", label=\"Simplify tolerance\",\n           default=0.0, parentParameterName=\"INPUT\", optional=True)\n@alg.input(type=alg.FILE_DEST, name=\"CSV\", label=\"Summary CSV\",\n           fileFilter=\"CSV files (*.csv)\")\n@alg.output(type=alg.FILE, name=\"CSV\", label=\"Summary CSV\")\ndef summarise_by_category(instance, parameters, context, feedback, inputs):\n    \"\"\"Writes one row per category with the chosen statistic.\"\"\"\n    source = instance.parameterAsSource(parameters, \"INPUT\", context)\n    category = instance.parameterAsFields(parameters, \"CATEGORY\", context)[0]\n    stat = instance.parameterAsEnum(parameters, \"STAT\", context)\n    csv_path = instance.parameterAsFileOutput(parameters, \"CSV\", context)\n\n    totals = {}\n    for f in source.getFeatures():\n        if feedback.isCanceled():\n            break\n        key = f[category]\n        count, area = totals.get(key, (0, 0.0))\n        totals[key] = (count + 1, area + f.geometry().area())\n\n    with open(csv_path, \"w\", encoding=\"utf-8\") as fh:\n        fh.write(f\"{category},value\\n\")\n        for key, (count, area) in sorted(totals.items(), key=lambda kv: str(kv[0])):\n            value = [count, area, area \u002F count][stat]\n            fh.write(f\"{key},{value:.2f}\\n\")\n    return {\"CSV\": csv_path}\n",[18,1106,1107,1131,1149,1182,1216,1227,1261,1296,1331,1360,1395,1407,1440,1449,1454,1466,1486,1500,1514,1518,1528,1540,1546,1550,1560,1578,1601,1605,1638,1664,1700,1715,1748],{"__ignoreMap":252},[256,1108,1109,1111,1113,1115,1117,1120,1122,1124,1126,1129],{"class":162,"line":258},[256,1110,47],{"class":316},[256,1112,319],{"class":265},[256,1114,24],{"class":322},[256,1116,325],{"class":261},[256,1118,1119],{"class":328},"\"summarise_by_category\"",[256,1121,25],{"class":265},[256,1123,334],{"class":322},[256,1125,325],{"class":261},[256,1127,1128],{"class":328},"\"Summarise by category\"",[256,1130,342],{"class":265},[256,1132,1133,1135,1137,1139,1141,1143,1145,1147],{"class":162,"line":275},[256,1134,348],{"class":322},[256,1136,325],{"class":261},[256,1138,353],{"class":328},[256,1140,25],{"class":265},[256,1142,358],{"class":322},[256,1144,325],{"class":261},[256,1146,363],{"class":328},[256,1148,366],{"class":265},[256,1150,1151,1153,1155,1157,1159,1161,1163,1165,1167,1169,1171,1173,1175,1177,1180],{"class":162,"line":288},[256,1152,372],{"class":316},[256,1154,319],{"class":265},[256,1156,377],{"class":322},[256,1158,325],{"class":261},[256,1160,382],{"class":265},[256,1162,386],{"class":385},[256,1164,25],{"class":265},[256,1166,24],{"class":322},[256,1168,325],{"class":261},[256,1170,395],{"class":328},[256,1172,25],{"class":265},[256,1174,334],{"class":322},[256,1176,325],{"class":261},[256,1178,1179],{"class":328},"\"Input layer\"",[256,1181,366],{"class":265},[256,1183,1184,1186,1188,1190,1192,1194,1196,1198,1200,1202,1205,1207,1209,1211,1214],{"class":162,"line":301},[256,1185,372],{"class":316},[256,1187,319],{"class":265},[256,1189,377],{"class":322},[256,1191,325],{"class":261},[256,1193,382],{"class":265},[256,1195,1075],{"class":385},[256,1197,25],{"class":265},[256,1199,24],{"class":322},[256,1201,325],{"class":261},[256,1203,1204],{"class":328},"\"CATEGORY\"",[256,1206,25],{"class":265},[256,1208,334],{"class":322},[256,1210,325],{"class":261},[256,1212,1213],{"class":328},"\"Category field\"",[256,1215,342],{"class":265},[256,1217,1218,1221,1223,1225],{"class":162,"line":308},[256,1219,1220],{"class":322},"           parentLayerParameterName",[256,1222,325],{"class":261},[256,1224,395],{"class":328},[256,1226,366],{"class":265},[256,1228,1229,1231,1233,1235,1237,1239,1241,1243,1245,1247,1250,1252,1254,1256,1259],{"class":162,"line":313},[256,1230,372],{"class":316},[256,1232,319],{"class":265},[256,1234,377],{"class":322},[256,1236,325],{"class":261},[256,1238,382],{"class":265},[256,1240,1066],{"class":385},[256,1242,25],{"class":265},[256,1244,24],{"class":322},[256,1246,325],{"class":261},[256,1248,1249],{"class":328},"\"STAT\"",[256,1251,25],{"class":265},[256,1253,334],{"class":322},[256,1255,325],{"class":261},[256,1257,1258],{"class":328},"\"Statistic\"",[256,1260,342],{"class":265},[256,1262,1263,1266,1268,1271,1274,1276,1279,1281,1284,1287,1290,1292,1294],{"class":162,"line":345},[256,1264,1265],{"class":322},"           options",[256,1267,325],{"class":261},[256,1269,1270],{"class":265},"[",[256,1272,1273],{"class":328},"\"count\"",[256,1275,25],{"class":265},[256,1277,1278],{"class":328},"\"total area\"",[256,1280,25],{"class":265},[256,1282,1283],{"class":328},"\"mean area\"",[256,1285,1286],{"class":265},"], ",[256,1288,1289],{"class":322},"default",[256,1291,325],{"class":261},[256,1293,82],{"class":385},[256,1295,366],{"class":265},[256,1297,1298,1300,1302,1304,1306,1308,1311,1313,1315,1317,1320,1322,1324,1326,1329],{"class":162,"line":369},[256,1299,372],{"class":316},[256,1301,319],{"class":265},[256,1303,377],{"class":322},[256,1305,325],{"class":261},[256,1307,382],{"class":265},[256,1309,1310],{"class":385},"DISTANCE",[256,1312,25],{"class":265},[256,1314,24],{"class":322},[256,1316,325],{"class":261},[256,1318,1319],{"class":328},"\"TOLERANCE\"",[256,1321,25],{"class":265},[256,1323,334],{"class":322},[256,1325,325],{"class":261},[256,1327,1328],{"class":328},"\"Simplify tolerance\"",[256,1330,342],{"class":265},[256,1332,1333,1335,1337,1339,1341,1344,1346,1348,1350,1353,1355,1358],{"class":162,"line":423},[256,1334,462],{"class":322},[256,1336,325],{"class":261},[256,1338,477],{"class":385},[256,1340,25],{"class":265},[256,1342,1343],{"class":322},"parentParameterName",[256,1345,325],{"class":261},[256,1347,395],{"class":328},[256,1349,25],{"class":265},[256,1351,1352],{"class":322},"optional",[256,1354,325],{"class":261},[256,1356,1357],{"class":385},"True",[256,1359,366],{"class":265},[256,1361,1362,1364,1366,1368,1370,1372,1375,1377,1379,1381,1384,1386,1388,1390,1393],{"class":162,"line":459},[256,1363,372],{"class":316},[256,1365,319],{"class":265},[256,1367,377],{"class":322},[256,1369,325],{"class":261},[256,1371,382],{"class":265},[256,1373,1374],{"class":385},"FILE_DEST",[256,1376,25],{"class":265},[256,1378,24],{"class":322},[256,1380,325],{"class":261},[256,1382,1383],{"class":328},"\"CSV\"",[256,1385,25],{"class":265},[256,1387,334],{"class":322},[256,1389,325],{"class":261},[256,1391,1392],{"class":328},"\"Summary CSV\"",[256,1394,342],{"class":265},[256,1396,1397,1400,1402,1405],{"class":162,"line":482},[256,1398,1399],{"class":322},"           fileFilter",[256,1401,325],{"class":261},[256,1403,1404],{"class":328},"\"CSV files (*.csv)\"",[256,1406,366],{"class":265},[256,1408,1409,1411,1413,1415,1417,1419,1422,1424,1426,1428,1430,1432,1434,1436,1438],{"class":162,"line":518},[256,1410,521],{"class":316},[256,1412,319],{"class":265},[256,1414,377],{"class":322},[256,1416,325],{"class":261},[256,1418,382],{"class":265},[256,1420,1421],{"class":385},"FILE",[256,1423,25],{"class":265},[256,1425,24],{"class":322},[256,1427,325],{"class":261},[256,1429,1383],{"class":328},[256,1431,25],{"class":265},[256,1433,334],{"class":322},[256,1435,325],{"class":261},[256,1437,1392],{"class":328},[256,1439,366],{"class":265},[256,1441,1442,1444,1447],{"class":162,"line":554},[256,1443,557],{"class":261},[256,1445,1446],{"class":316}," summarise_by_category",[256,1448,563],{"class":265},[256,1450,1451],{"class":162,"line":566},[256,1452,1453],{"class":328},"    \"\"\"Writes one row per category with the chosen statistic.\"\"\"\n",[256,1455,1456,1458,1460,1462,1464],{"class":162,"line":572},[256,1457,592],{"class":265},[256,1459,325],{"class":261},[256,1461,597],{"class":265},[256,1463,395],{"class":328},[256,1465,602],{"class":265},[256,1467,1468,1471,1473,1476,1478,1481,1483],{"class":162,"line":578},[256,1469,1470],{"class":265},"    category ",[256,1472,325],{"class":261},[256,1474,1475],{"class":265}," instance.parameterAsFields(parameters, ",[256,1477,1204],{"class":328},[256,1479,1480],{"class":265},", context)[",[256,1482,82],{"class":385},[256,1484,1485],{"class":265},"]\n",[256,1487,1488,1491,1493,1496,1498],{"class":162,"line":584},[256,1489,1490],{"class":265},"    stat ",[256,1492,325],{"class":261},[256,1494,1495],{"class":265}," instance.parameterAsEnum(parameters, ",[256,1497,1249],{"class":328},[256,1499,602],{"class":265},[256,1501,1502,1505,1507,1510,1512],{"class":162,"line":589},[256,1503,1504],{"class":265},"    csv_path ",[256,1506,325],{"class":261},[256,1508,1509],{"class":265}," instance.parameterAsFileOutput(parameters, ",[256,1511,1383],{"class":328},[256,1513,602],{"class":265},[256,1515,1516],{"class":162,"line":605},[256,1517,305],{"emptyLinePlaceholder":304},[256,1519,1520,1523,1525],{"class":162,"line":623},[256,1521,1522],{"class":265},"    totals ",[256,1524,325],{"class":261},[256,1526,1527],{"class":265}," {}\n",[256,1529,1530,1532,1535,1537],{"class":162,"line":637},[256,1531,764],{"class":261},[256,1533,1534],{"class":265}," f ",[256,1536,770],{"class":261},[256,1538,1539],{"class":265}," source.getFeatures():\n",[256,1541,1542,1544],{"class":162,"line":652},[256,1543,782],{"class":261},[256,1545,785],{"class":265},[256,1547,1548],{"class":162,"line":657},[256,1549,791],{"class":261},[256,1551,1552,1555,1557],{"class":162,"line":668},[256,1553,1554],{"class":265},"        key ",[256,1556,325],{"class":261},[256,1558,1559],{"class":265}," f[category]\n",[256,1561,1562,1565,1567,1570,1572,1574,1576],{"class":162,"line":680},[256,1563,1564],{"class":265},"        count, area ",[256,1566,325],{"class":261},[256,1568,1569],{"class":265}," totals.get(key, (",[256,1571,82],{"class":385},[256,1573,25],{"class":265},[256,1575,477],{"class":385},[256,1577,634],{"class":265},[256,1579,1580,1583,1585,1588,1590,1593,1596,1598],{"class":162,"line":691},[256,1581,1582],{"class":265},"        totals[key] ",[256,1584,325],{"class":261},[256,1586,1587],{"class":265}," (count ",[256,1589,843],{"class":261},[256,1591,1592],{"class":385}," 1",[256,1594,1595],{"class":265},", area ",[256,1597,843],{"class":261},[256,1599,1600],{"class":265}," f.geometry().area())\n",[256,1602,1603],{"class":162,"line":702},[256,1604,305],{"emptyLinePlaceholder":304},[256,1606,1607,1610,1613,1616,1619,1621,1624,1626,1629,1632,1635],{"class":162,"line":716},[256,1608,1609],{"class":261},"    with",[256,1611,1612],{"class":385}," open",[256,1614,1615],{"class":265},"(csv_path, ",[256,1617,1618],{"class":328},"\"w\"",[256,1620,25],{"class":265},[256,1622,1623],{"class":322},"encoding",[256,1625,325],{"class":261},[256,1627,1628],{"class":328},"\"utf-8\"",[256,1630,1631],{"class":265},") ",[256,1633,1634],{"class":261},"as",[256,1636,1637],{"class":265}," fh:\n",[256,1639,1640,1643,1645,1647,1649,1652,1654,1657,1660,1662],{"class":162,"line":728},[256,1641,1642],{"class":265},"        fh.write(",[256,1644,887],{"class":261},[256,1646,890],{"class":328},[256,1648,893],{"class":385},[256,1650,1651],{"class":265},"category",[256,1653,899],{"class":385},[256,1655,1656],{"class":328},",value",[256,1658,1659],{"class":385},"\\n",[256,1661,890],{"class":328},[256,1663,366],{"class":265},[256,1665,1666,1669,1672,1674,1677,1680,1683,1686,1689,1692,1695,1697],{"class":162,"line":733},[256,1667,1668],{"class":261},"        for",[256,1670,1671],{"class":265}," key, (count, area) ",[256,1673,770],{"class":261},[256,1675,1676],{"class":385}," sorted",[256,1678,1679],{"class":265},"(totals.items(), ",[256,1681,1682],{"class":322},"key",[256,1684,1685],{"class":261},"=lambda",[256,1687,1688],{"class":265}," kv: ",[256,1690,1691],{"class":385},"str",[256,1693,1694],{"class":265},"(kv[",[256,1696,82],{"class":385},[256,1698,1699],{"class":265},"])):\n",[256,1701,1702,1705,1707,1710,1712],{"class":162,"line":750},[256,1703,1704],{"class":265},"            value ",[256,1706,325],{"class":261},[256,1708,1709],{"class":265}," [count, area, area ",[256,1711,870],{"class":261},[256,1713,1714],{"class":265}," count][stat]\n",[256,1716,1717,1720,1722,1724,1726,1728,1730,1733,1735,1738,1741,1744,1746],{"class":162,"line":761},[256,1718,1719],{"class":265},"            fh.write(",[256,1721,887],{"class":261},[256,1723,890],{"class":328},[256,1725,893],{"class":385},[256,1727,1682],{"class":265},[256,1729,899],{"class":385},[256,1731,1732],{"class":328},",",[256,1734,893],{"class":385},[256,1736,1737],{"class":265},"value",[256,1739,1740],{"class":261},":.2f",[256,1742,1743],{"class":385},"}\\n",[256,1745,890],{"class":328},[256,1747,366],{"class":265},[256,1749,1750,1752,1754,1756],{"class":162,"line":779},[256,1751,930],{"class":261},[256,1753,933],{"class":265},[256,1755,1383],{"class":328},[256,1757,1758],{"class":265},": csv_path}\n",[14,1760,1761,1763,1764,1767,1768,1771,1772,1774,1775,1777,1778,1780,1781,1783,1784,1787,1788,233],{},[219,1762,948],{}," Save this as its own ",[18,1765,1766],{},".py"," file with the same imports as the first script — each script file defines one decorated algorithm. ",[18,1769,1770],{},"parentLayerParameterName"," links the field combo to the chosen layer, so it lists that layer's fields. ",[18,1773,1066],{}," returns the index of the chosen option, which is why the statistic is picked by indexing a list. ",[18,1776,1310],{}," shows the linked layer's units next to the spin box, which prevents the classic metres-versus-degrees confusion. ",[18,1779,1374],{}," produces a file path chosen in the dialog, and declaring a matching ",[18,1782,1421],{}," output passes that path on to models. ",[18,1785,1786],{},"optional=True"," lets a parameter be left empty; read it with a check for ",[18,1789,1790],{},"None",[208,1792,1794],{"id":1793},"run-it-test-it-and-share-it","Run it, test it, and share it",[14,1796,1797,1798,1801,1802,1805],{},"Script algorithms live in the ",[18,1799,1800],{},"script"," provider, so their id is ",[18,1803,1804],{},"script:"," plus the name. That is enough to call them from code and to write a test.",[247,1807,1809],{"className":249,"code":1808,"language":251,"meta":252,"style":252},"import processing\nfrom qgis.core import QgsVectorLayer, QgsFeature, QgsGeometry\n\nparcels = QgsVectorLayer(\"Polygon?crs=EPSG:27700&field=id:integer\", \"test\", \"memory\")\nfeatures = []\nfor i, size in enumerate((5, 20, 200)):\n    f = QgsFeature(parcels.fields())\n    f.setAttributes([i])\n    f.setGeometry(QgsGeometry.fromWkt(f\"POLYGON((0 0,{size} 0,{size} {size},0 {size},0 0))\"))\n    features.append(f)\nparcels.dataProvider().addFeatures(features)\n\nresult = processing.run(\"script:flag_small_parcels\", {\n    \"INPUT\": parcels, \"MIN_AREA\": 50, \"OUTPUT\": \"TEMPORARY_OUTPUT\",\n})\nassert result[\"FLAGGED\"] == 2, result\nprint([f[\"small_parcel\"] for f in result[\"OUTPUT\"].getFeatures()])\n",[18,1810,1811,1818,1829,1833,1858,1868,1898,1908,1913,1959,1964,1969,1973,1989,2015,2020,2042],{"__ignoreMap":252},[256,1812,1813,1815],{"class":162,"line":258},[256,1814,269],{"class":261},[256,1816,1817],{"class":265}," processing\n",[256,1819,1820,1822,1824,1826],{"class":162,"line":275},[256,1821,262],{"class":261},[256,1823,266],{"class":265},[256,1825,269],{"class":261},[256,1827,1828],{"class":265}," QgsVectorLayer, QgsFeature, QgsGeometry\n",[256,1830,1831],{"class":162,"line":288},[256,1832,305],{"emptyLinePlaceholder":304},[256,1834,1835,1838,1840,1843,1846,1848,1851,1853,1856],{"class":162,"line":301},[256,1836,1837],{"class":265},"parcels ",[256,1839,325],{"class":261},[256,1841,1842],{"class":265}," QgsVectorLayer(",[256,1844,1845],{"class":328},"\"Polygon?crs=EPSG:27700&field=id:integer\"",[256,1847,25],{"class":265},[256,1849,1850],{"class":328},"\"test\"",[256,1852,25],{"class":265},[256,1854,1855],{"class":328},"\"memory\"",[256,1857,366],{"class":265},[256,1859,1860,1863,1865],{"class":162,"line":308},[256,1861,1862],{"class":265},"features ",[256,1864,325],{"class":261},[256,1866,1867],{"class":265}," []\n",[256,1869,1870,1873,1876,1878,1880,1883,1885,1887,1890,1892,1895],{"class":162,"line":313},[256,1871,1872],{"class":261},"for",[256,1874,1875],{"class":265}," i, size ",[256,1877,770],{"class":261},[256,1879,773],{"class":385},[256,1881,1882],{"class":265},"((",[256,1884,95],{"class":385},[256,1886,25],{"class":265},[256,1888,1889],{"class":385},"20",[256,1891,25],{"class":265},[256,1893,1894],{"class":385},"200",[256,1896,1897],{"class":265},")):\n",[256,1899,1900,1903,1905],{"class":162,"line":345},[256,1901,1902],{"class":265},"    f ",[256,1904,325],{"class":261},[256,1906,1907],{"class":265}," QgsFeature(parcels.fields())\n",[256,1909,1910],{"class":162,"line":369},[256,1911,1912],{"class":265},"    f.setAttributes([i])\n",[256,1914,1915,1918,1920,1923,1925,1928,1930,1933,1935,1937,1939,1941,1943,1945,1948,1950,1952,1954,1957],{"class":162,"line":423},[256,1916,1917],{"class":265},"    f.setGeometry(QgsGeometry.fromWkt(",[256,1919,887],{"class":261},[256,1921,1922],{"class":328},"\"POLYGON((0 0,",[256,1924,893],{"class":385},[256,1926,1927],{"class":265},"size",[256,1929,899],{"class":385},[256,1931,1932],{"class":328}," 0,",[256,1934,893],{"class":385},[256,1936,1927],{"class":265},[256,1938,899],{"class":385},[256,1940,933],{"class":385},[256,1942,1927],{"class":265},[256,1944,899],{"class":385},[256,1946,1947],{"class":328},",0 ",[256,1949,893],{"class":385},[256,1951,1927],{"class":265},[256,1953,899],{"class":385},[256,1955,1956],{"class":328},",0 0))\"",[256,1958,634],{"class":265},[256,1960,1961],{"class":162,"line":459},[256,1962,1963],{"class":265},"    features.append(f)\n",[256,1965,1966],{"class":162,"line":482},[256,1967,1968],{"class":265},"parcels.dataProvider().addFeatures(features)\n",[256,1970,1971],{"class":162,"line":518},[256,1972,305],{"emptyLinePlaceholder":304},[256,1974,1975,1978,1980,1983,1986],{"class":162,"line":554},[256,1976,1977],{"class":265},"result ",[256,1979,325],{"class":261},[256,1981,1982],{"class":265}," processing.run(",[256,1984,1985],{"class":328},"\"script:flag_small_parcels\"",[256,1987,1988],{"class":265},", {\n",[256,1990,1991,1994,1997,1999,2002,2004,2006,2008,2010,2013],{"class":162,"line":566},[256,1992,1993],{"class":328},"    \"INPUT\"",[256,1995,1996],{"class":265},": parcels, ",[256,1998,445],{"class":328},[256,2000,2001],{"class":265},": ",[256,2003,115],{"class":385},[256,2005,25],{"class":265},[256,2007,504],{"class":328},[256,2009,2001],{"class":265},[256,2011,2012],{"class":328},"\"TEMPORARY_OUTPUT\"",[256,2014,342],{"class":265},[256,2016,2017],{"class":162,"line":572},[256,2018,2019],{"class":265},"})\n",[256,2021,2022,2025,2028,2030,2033,2036,2039],{"class":162,"line":578},[256,2023,2024],{"class":261},"assert",[256,2026,2027],{"class":265}," result[",[256,2029,540],{"class":328},[256,2031,2032],{"class":265},"] ",[256,2034,2035],{"class":261},"==",[256,2037,2038],{"class":385}," 2",[256,2040,2041],{"class":265},", result\n",[256,2043,2044,2047,2050,2052,2054,2056,2058,2060,2062,2064],{"class":162,"line":584},[256,2045,2046],{"class":385},"print",[256,2048,2049],{"class":265},"([f[",[256,2051,674],{"class":328},[256,2053,2032],{"class":265},[256,2055,1872],{"class":261},[256,2057,1534],{"class":265},[256,2059,770],{"class":261},[256,2061,2027],{"class":265},[256,2063,504],{"class":328},[256,2065,2066],{"class":265},"].getFeatures()])\n",[14,2068,2069,2071,2072,2076,2077,2079,2080,2083],{},[219,2070,948],{}," A memory layer with three squares of known area is a complete test fixture: 25 m² and 400 m² are below and above the threshold in an obvious way, and 40,000 m² confirms nothing odd happens with large values. Asserting on the declared output number rather than on printed text makes the test robust to message wording. The same test runs under pytest with the approach in ",[57,2073,2075],{"href":2074},"\u002Fqgis-plugin-development\u002Ftesting-and-ci-for-plugins\u002Ftest-processing-algorithms-with-pytest-qgis\u002F","testing Processing algorithms with pytest-qgis",". To share a script, copy the ",[18,2078,1766],{}," file into colleagues' Processing scripts folders, or point ",[242,2081,2082],{},"Processing → Options → Scripts folders"," at a shared network directory so everyone loads the same version.",[14,2085,2086],{},[65,2087,2090,2093,2096,2099,2106,2109,2113,2119,2123,2126,2130,2133,2139,2143,2147,2150,2153,2156],{"viewBox":2088,"role":68,"ariaLabel":2089,"xmlns":70},"0 0 760 236","When to graduate from an alg script to a class-based algorithm in a provider plugin: a single script for personal and team tools, a class when you need custom validation, dynamic parameters, several related algorithms, translations or distribution through the plugin repository",[72,2091,2092],{},"Script today, provider plugin tomorrow",[76,2094,2095],{},"Left column, stay with alg: one or two tools, fixed parameters, shared through a scripts folder, no translations needed. Right column, move to a QgsProcessingAlgorithm class in a provider plugin: custom checkParameterValues validation, flags such as no-threading, many related algorithms under one provider with an icon, translations, versioned releases through the plugin repository, and full unit testing as a package.",[80,2097],{"x":82,"y":82,"width":83,"height":2098,"fill":85},"236",[87,2100,2101],{},[90,2102,2104],{"id":2103,"viewBox":93,"refX":94,"refY":95,"markerWidth":96,"markerHeight":96,"orient":97},"algGradArrow",[99,2105],{"d":101,"fill":102},[104,2107,2108],{"x":106,"y":1009,"style":108,"fill":109,"textAnchor":110},"The decorator is a starting point, not a ceiling",[80,2110],{"x":114,"y":130,"width":84,"height":2111,"rx":118,"fill":2112,"stroke":133,"style":174},"172","#eef7f4",[104,2114,2118],{"x":2115,"y":2116,"style":2117,"fill":133,"textAnchor":110},"174","70","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","@alg script",[104,2120,2122],{"x":2115,"y":861,"style":2121,"fill":102,"textAnchor":110},"text-anchor:middle;font-size:10.5px;font-family:sans-serif","one or two tools",[104,2124,2125],{"x":2115,"y":137,"style":2121,"fill":102,"textAnchor":110},"fixed parameters",[104,2127,2129],{"x":2115,"y":2128,"style":2121,"fill":102,"textAnchor":110},"148","shared scripts folder",[104,2131,2132],{"x":2115,"y":150,"style":1040,"fill":120,"textAnchor":110},"minutes to write",[162,2134],{"x1":2135,"y1":2136,"x2":2137,"y2":2136,"stroke":102,"style":2138},"324","130","428","stroke-width:2;marker-end:url(#algGradArrow)",[80,2140],{"x":2141,"y":130,"width":84,"height":2111,"rx":118,"fill":2142,"stroke":138,"style":174},"436","#eff3ff",[104,2144,2146],{"x":2145,"y":2116,"style":2117,"fill":138,"textAnchor":110},"586","class in a provider plugin",[104,2148,2149],{"x":2145,"y":861,"style":2121,"fill":102,"textAnchor":110},"custom validation, flags",[104,2151,2152],{"x":2145,"y":137,"style":2121,"fill":102,"textAnchor":110},"many algorithms, one provider",[104,2154,2155],{"x":2145,"y":2128,"style":2121,"fill":102,"textAnchor":110},"translations, releases",[104,2157,2158],{"x":2145,"y":150,"style":1040,"fill":120,"textAnchor":110},"distributable and versioned",[208,2160,2162],{"id":2161},"when-to-write-a-class-instead","When to write a class instead",[14,2164,2165,2166,2168,2169,2172,2173,2175,2176,2180,2181,2185,2186,2190],{},"The decorator generates a sensible default for everything it hides. Move to a ",[18,2167,20],{}," subclass, registered by a provider plugin, when you need any of the things it does not expose: validation across parameters in ",[18,2170,2171],{},"checkParameterValues",", algorithm flags such as disabling background threads for code that touches the GUI, a custom provider with its own icon and grouping for a family of tools, translation of labels, or distribution through the plugin repository with versioned releases. The conversion is mechanical — the decorator arguments become method return values and the function body becomes ",[18,2174,43],{}," — and it is walked through in ",[57,2177,2179],{"href":2178},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Fwrite-custom-processing-algorithm-pyqgis\u002F","writing a custom Processing algorithm"," and ",[57,2182,2184],{"href":2183},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Fregister-processing-provider-in-plugin\u002F","registering a Processing provider in a plugin",". For algorithms that transform features one at a time, ",[57,2187,2189],{"href":2188},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Fwrite-feature-based-processing-algorithm-pyqgis\u002F","a feature-based algorithm"," removes even more boilerplate.",[208,2192,2194],{"id":2193},"qgis-version-compatibility","QGIS version compatibility",[14,2196,2197,2198,2200,2201,2204,2205,2208,2209,2212],{},"The ",[18,2199,47],{}," decorator arrived in QGIS 3.6 and its parameter constants have stayed stable. ",[18,2202,2203],{},"QMetaType.Type"," for field types is required on the QGIS 4 series; on releases before 3.38 use ",[18,2206,2207],{},"QVariant.Bool",". Processing runs algorithms in a background thread by default, so decorated scripts must not touch ",[18,2210,2211],{},"iface"," or widgets. Scripts saved on 3.x load on later versions as long as their Qt code uses scoped enums.",[208,2214,2216],{"id":2215},"troubleshooting","Troubleshooting",[213,2218,2219,2229,2238,2247,2253],{},[216,2220,2221,2224,2225,2228],{},[219,2222,2223],{},"The script does not appear in the Toolbox."," It is not in a configured scripts folder, or it raised an error on import — check the ",[242,2226,2227],{},"Processing"," tab of the log.",[216,2230,2231,2234,2235,2237],{},[219,2232,2233],{},"\"Wrong number of decorators\"."," ",[18,2236,47],{}," must be the top decorator and every function needs one.",[216,2239,2240,2243,2244,2246],{},[219,2241,2242],{},"Output key missing in results."," The returned dictionary key does not match an ",[18,2245,521],{}," name.",[216,2248,2249,2252],{},[219,2250,2251],{},"QGIS crashes when the script runs."," The code touches GUI objects from the background thread.",[216,2254,2255,2234,2258,2260],{},[219,2256,2257],{},"Field combo is empty.",[18,2259,1770],{}," does not match the source parameter's name.",[208,2262,2264],{"id":2263},"conclusion","Conclusion",[14,2266,2267,2268,2270,2271,2273],{},"Use ",[18,2269,47],{}," for focused tools: declare identity, inputs and outputs as decorators, read parameters through the instance, write features to a sink with progress and cancellation checks, and return every declared output. Test it with ",[18,2272,51],{}," and a memory-layer fixture, share it through a scripts folder, and graduate to a class in a provider plugin when you need validation, flags, translation or releases.",[208,2275,2277],{"id":2276},"frequently-asked-questions","Frequently Asked Questions",[14,2279,2280,2283,2284,2287],{},[219,2281,2282],{},"Can a decorated script call other algorithms?","\nYes. Use ",[18,2285,2286],{},"processing.run(…, context=context, feedback=feedback, is_child_algorithm=True)"," so progress and temporary outputs are handled correctly.",[14,2289,2290,2293,2294,2297,2298,2301],{},[219,2291,2292],{},"Where is the scripts folder?","\nIn the user profile under ",[18,2295,2296],{},"processing\u002Fscripts","; ",[242,2299,2300],{},"Processing → Options"," shows the exact path and lets you add more.",[14,2303,2304,2307,2308,233],{},[219,2305,2306],{},"Can the script load its output into the project?","\nProcessing adds sink outputs automatically when run from the dialog. From code, use ",[18,2309,2310],{},"processing.runAndLoadResults",[14,2312,2313,2316],{},[219,2314,2315],{},"How do I add an icon?","\nScripts use the generic script icon. Custom icons require a provider plugin.",[208,2318,2320],{"id":2319},"related","Related",[213,2322,2323,2328,2333,2338,2344],{},[216,2324,2325,2327],{},[57,2326,60],{"href":59}," — the guide this recipe belongs to",[216,2329,2330],{},[57,2331,2332],{"href":2178},"Write a Custom Processing Algorithm in PyQGIS",[216,2334,2335],{},[57,2336,2337],{"href":2188},"Write a Feature-Based Processing Algorithm in PyQGIS",[216,2339,2340],{},[57,2341,2343],{"href":2342},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Fadd-parameters-to-processing-algorithm-pyqgis\u002F","Add Parameters to a Processing Algorithm",[216,2345,2346],{},[57,2347,2349],{"href":2348},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Freport-progress-and-cancel-processing-algorithm\u002F","Report Progress and Cancellation in a Processing Algorithm",[2351,2352,2353],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":252,"searchDepth":275,"depth":275,"links":2355},[2356,2357,2358,2359,2360,2361,2362,2363,2364,2365],{"id":210,"depth":275,"text":211},{"id":236,"depth":275,"text":237},{"id":980,"depth":275,"text":981},{"id":1793,"depth":275,"text":1794},{"id":2161,"depth":275,"text":2162},{"id":2193,"depth":275,"text":2194},{"id":2215,"depth":275,"text":2216},{"id":2263,"depth":275,"text":2264},{"id":2276,"depth":275,"text":2277},{"id":2319,"depth":275,"text":2320},"Turn a Python function into a Processing algorithm with the @alg decorator — declaring inputs and outputs, reading parameters, writing to a feature sink, reporting progress, and deciding when to graduate to a full QgsProcessingAlgorithm class in a provider plugin.","md",{"slug":2369,"type":2370,"breadcrumb":2371,"datePublished":2372,"dateModified":2372},"write-processing-script-with-alg-decorator","article","Processing Script with @alg","2026-09-17","\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Fwrite-processing-script-with-alg-decorator",{"title":5,"description":2366},"qgis-plugin-development\u002Fprocessing-provider-plugins\u002Fwrite-processing-script-with-alg-decorator\u002Findex","ayIjszwSlGk5fQ-2ldpffF3MetMa2RqIqfHtoCcbPuw",1789632907590]