[{"data":1,"prerenderedAt":1767},["ShallowReactive",2],{"doc:\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Frun-graphical-model-from-python-pyqgis":3},{"id":4,"title":5,"body":6,"description":1756,"extension":1757,"meta":1758,"navigation":256,"path":1763,"seo":1764,"stem":1765,"__hash__":1766},"docs\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Frun-graphical-model-from-python-pyqgis\u002Findex.md","Run a Graphical Model from Python in PyQGIS",{"type":7,"value":8,"toc":1742},"minimark",[9,13,17,31,180,185,205,209,216,320,335,338,402,415,419,422,572,601,605,608,764,787,790,806,811,887,891,894,897,1166,1171,1178,1182,1185,1463,1477,1481,1489,1498,1501,1505,1511,1596,1600,1656,1660,1663,1667,1680,1686,1696,1706,1710,1738],[10,11,5],"h1",{"id":12},"run-a-graphical-model-from-python-in-pyqgis",[14,15,16],"p",{},"The Processing modeller lets somebody build a workflow by dragging boxes, and the result is an algorithm like any other — which means Python can run it, schedule it, loop it over a folder and wrap it in error handling. That combination is genuinely useful: the analyst who understands the workflow maintains the model, and the developer who understands automation drives it.",[14,18,19,20,25,26,30],{},"This recipe belongs to ",[21,22,24],"a",{"href":23},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002F","Chaining Processing Algorithms in PyQGIS",". It covers running an installed model by id, loading a ",[27,28,29],"code",{},".model3"," file that is not installed, discovering its parameter names without opening the modeller, and the trade-off against writing the chain in Python.",[14,32,33],{},[34,35,40,44,48,55,72,81,91,97,102,106,110,116,122,125,128,133,136,140,145,150,153,156,159,166,171,176],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 320","img","A model file registered into the Processing registry appears as an algorithm with an id, which Python calls exactly like a built-in algorithm","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"A model is just another algorithm",[45,46,47],"desc",{},"A model file in the user's profile is loaded by the model provider at startup and registered with an id in the model provider's namespace. From Python it is indistinguishable from a native algorithm: the same run call, the same parameter dictionary, the same feedback object.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","320","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"modArrow","0 0 10 10","8","5","7","auto-start-reverse",[68,69],"path",{"d":70,"fill":71},"M0 0 L10 5 L0 10 z","#2f3b35",[73,74,80],"text",{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"380","28","text-anchor:middle;font-size:14px;font-weight:bold;font-family:sans-serif","#17211d","middle","Built in the modeller, driven from Python",[49,82],{"x":83,"y":84,"width":85,"height":86,"rx":87,"fill":88,"stroke":89,"style":90},"24","56","200","106","10","#fdf2e2","#b45309","stroke-width:2.5",[73,92,96],{"x":93,"y":94,"style":95,"fill":89,"textAnchor":79},"124","84","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","catchment.model3",[73,98,101],{"x":93,"y":99,"style":100,"fill":71,"textAnchor":79},"108","text-anchor:middle;font-size:10px;font-family:sans-serif","in the user profile",[73,103,105],{"x":93,"y":104,"style":100,"fill":71,"textAnchor":79},"128","processing\u002Fmodels\u002F",[73,107,109],{"x":93,"y":108,"style":100,"fill":71,"textAnchor":79},"150","edited by dragging boxes",[111,112],"line",{"x1":113,"y1":99,"x2":114,"y2":99,"stroke":71,"style":115},"228","266","stroke-width:2;marker-end:url(#modArrow)",[49,117],{"x":118,"y":84,"width":119,"height":86,"rx":87,"fill":120,"stroke":121,"style":90},"274","212","#eef7f4","#0f766e",[73,123,124],{"x":75,"y":94,"style":95,"fill":121,"textAnchor":79},"model provider",[73,126,127],{"x":75,"y":99,"style":100,"fill":71,"textAnchor":79},"loads at startup",[73,129,132],{"x":75,"y":130,"style":131,"fill":71,"textAnchor":79},"130","text-anchor:middle;font-size:10px;font-family:monospace","model:catchment",[73,134,135],{"x":75,"y":108,"style":100,"fill":71,"textAnchor":79},"an id like any other",[111,137],{"x1":138,"y1":99,"x2":139,"y2":99,"stroke":71,"style":115},"490","528",[49,141],{"x":142,"y":84,"width":85,"height":86,"rx":87,"fill":143,"stroke":144,"style":90},"536","#edf8e9","#15803d",[73,146,149],{"x":147,"y":94,"style":148,"fill":144,"textAnchor":79},"636","text-anchor:middle;font-size:11px;font-weight:bold;font-family:monospace","processing.run()",[73,151,152],{"x":147,"y":99,"style":100,"fill":71,"textAnchor":79},"same call as native:buffer",[73,154,155],{"x":147,"y":130,"style":100,"fill":71,"textAnchor":79},"same feedback object",[73,157,158],{"x":147,"y":108,"style":100,"fill":71,"textAnchor":79},"same return dictionary",[49,160],{"x":161,"y":85,"width":162,"height":163,"rx":87,"fill":164,"stroke":165,"style":90},"120","520","94","#fffdf7","#2563eb",[73,167,170],{"x":75,"y":168,"style":169,"fill":165,"textAnchor":79},"226","text-anchor:middle;font-size:11.5px;font-weight:bold;font-family:sans-serif","the division of labour this enables",[73,172,175],{"x":75,"y":173,"style":174,"fill":71,"textAnchor":79},"250","text-anchor:middle;font-size:10.5px;font-family:sans-serif","the analyst maintains the workflow in the modeller",[73,177,179],{"x":75,"y":178,"style":174,"fill":71,"textAnchor":79},"272","the script loops it, schedules it and handles the errors",[181,182,184],"h2",{"id":183},"prerequisites","Prerequisites",[186,187,188,196,202],"ul",{},[189,190,191,195],"li",{},[192,193,194],"strong",{},"QGIS 3.34 LTR"," (bundled Python 3.12) or newer with Processing initialised.",[189,197,198,199,201],{},"A model saved from the Processing modeller as a ",[27,200,29],{}," file, either installed in the profile or somewhere readable.",[189,203,204],{},"The algorithms the model uses must be available — a model built with SAGA steps needs the SAGA provider on the machine running it.",[181,206,208],{"id":207},"run-an-installed-model","Run an installed model",[14,210,211,212,215],{},"A model saved into the user profile is registered automatically and has an id in the ",[27,213,214],{},"model"," namespace.",[217,218,223],"pre",{"className":219,"code":220,"language":221,"meta":222,"style":222},"language-python shiki shiki-themes github-dark","import processing\nfrom qgis.core import QgsApplication\n\nregistry = QgsApplication.processingRegistry()\nfor alg in registry.algorithms():\n    if alg.provider().id() == \"model\":\n        print(alg.id(), \"|\", alg.displayName())\n","python","",[27,224,225,237,251,258,270,285,304],{"__ignoreMap":222},[226,227,229,233],"span",{"class":111,"line":228},1,[226,230,232],{"class":231},"snl16","import",[226,234,236],{"class":235},"s95oV"," processing\n",[226,238,240,243,246,248],{"class":111,"line":239},2,[226,241,242],{"class":231},"from",[226,244,245],{"class":235}," qgis.core ",[226,247,232],{"class":231},[226,249,250],{"class":235}," QgsApplication\n",[226,252,254],{"class":111,"line":253},3,[226,255,257],{"emptyLinePlaceholder":256},true,"\n",[226,259,261,264,267],{"class":111,"line":260},4,[226,262,263],{"class":235},"registry ",[226,265,266],{"class":231},"=",[226,268,269],{"class":235}," QgsApplication.processingRegistry()\n",[226,271,273,276,279,282],{"class":111,"line":272},5,[226,274,275],{"class":231},"for",[226,277,278],{"class":235}," alg ",[226,280,281],{"class":231},"in",[226,283,284],{"class":235}," registry.algorithms():\n",[226,286,288,291,294,297,301],{"class":111,"line":287},6,[226,289,290],{"class":231},"    if",[226,292,293],{"class":235}," alg.provider().id() ",[226,295,296],{"class":231},"==",[226,298,300],{"class":299},"sU2Wk"," \"model\"",[226,302,303],{"class":235},":\n",[226,305,307,311,314,317],{"class":111,"line":306},7,[226,308,310],{"class":309},"sDLfK","        print",[226,312,313],{"class":235},"(alg.id(), ",[226,315,316],{"class":299},"\"|\"",[226,318,319],{"class":235},", alg.displayName())\n",[14,321,322,325,326,330,331,334],{},[192,323,324],{},"Breakdown:"," Listing the registry is the reliable way to find the id, because the id is derived from the model's ",[327,328,329],"em",{},"name"," at save time and is not always what the display name suggests — spaces, capitals and punctuation are normalised. Running this once and pasting the exact id into the script beats guessing. Models saved into a project rather than the profile appear under the ",[27,332,333],{},"project"," provider instead.",[14,336,337],{},"With the id in hand it is an ordinary call:",[217,339,341],{"className":219,"code":340,"language":221,"meta":222,"style":222},"result = processing.run(\"model:catchment_summary\", {\n    \"dem\": \"\u002Fdata\u002Fdem_27700.tif\",\n    \"catchments\": \"\u002Fdata\u002Fcatchments.gpkg\",\n    \"native:zonalstatisticsfb_1:summary\": \"\u002Fdata\u002Foutput\u002Fsummary.gpkg\",\n})\n",[27,342,343,359,373,385,397],{"__ignoreMap":222},[226,344,345,348,350,353,356],{"class":111,"line":228},[226,346,347],{"class":235},"result ",[226,349,266],{"class":231},[226,351,352],{"class":235}," processing.run(",[226,354,355],{"class":299},"\"model:catchment_summary\"",[226,357,358],{"class":235},", {\n",[226,360,361,364,367,370],{"class":111,"line":239},[226,362,363],{"class":299},"    \"dem\"",[226,365,366],{"class":235},": ",[226,368,369],{"class":299},"\"\u002Fdata\u002Fdem_27700.tif\"",[226,371,372],{"class":235},",\n",[226,374,375,378,380,383],{"class":111,"line":253},[226,376,377],{"class":299},"    \"catchments\"",[226,379,366],{"class":235},[226,381,382],{"class":299},"\"\u002Fdata\u002Fcatchments.gpkg\"",[226,384,372],{"class":235},[226,386,387,390,392,395],{"class":111,"line":260},[226,388,389],{"class":299},"    \"native:zonalstatisticsfb_1:summary\"",[226,391,366],{"class":235},[226,393,394],{"class":299},"\"\u002Fdata\u002Foutput\u002Fsummary.gpkg\"",[226,396,372],{"class":235},[226,398,399],{"class":111,"line":272},[226,400,401],{"class":235},"})\n",[14,403,404,406,407,410,411,414],{},[192,405,324],{}," Input parameter names are whatever the model author typed in the modeller's parameter definitions, lower-cased and normalised — hence ",[27,408,409],{},"dem"," rather than ",[27,412,413],{},"INPUT",". Output names are uglier: the modeller composes them from the algorithm id, an index and the output name the author gave. They are stable as long as the model is not restructured, and they are the reason a script driving a model should be re-checked whenever the model changes.",[181,416,418],{"id":417},"discover-the-parameters-without-opening-the-modeller","Discover the parameters without opening the modeller",[14,420,421],{},"Rather than reading the ids off a diagram, ask the algorithm.",[217,423,425],{"className":219,"code":424,"language":221,"meta":222,"style":222},"alg = QgsApplication.processingRegistry().algorithmById(\"model:catchment_summary\")\nif alg is None:\n    raise LookupError(\"model not registered — check the id and the profile\")\n\nfor param in alg.parameterDefinitions():\n    print(f\"in  {param.name():40} {type(param).__name__}\")\nfor output in alg.outputDefinitions():\n    print(f\"out {output.name():40} {type(output).__name__}\")\n",[27,426,427,442,457,473,477,489,528,540],{"__ignoreMap":222},[226,428,429,432,434,437,439],{"class":111,"line":228},[226,430,431],{"class":235},"alg ",[226,433,266],{"class":231},[226,435,436],{"class":235}," QgsApplication.processingRegistry().algorithmById(",[226,438,355],{"class":299},[226,440,441],{"class":235},")\n",[226,443,444,447,449,452,455],{"class":111,"line":239},[226,445,446],{"class":231},"if",[226,448,278],{"class":235},[226,450,451],{"class":231},"is",[226,453,454],{"class":309}," None",[226,456,303],{"class":235},[226,458,459,462,465,468,471],{"class":111,"line":253},[226,460,461],{"class":231},"    raise",[226,463,464],{"class":309}," LookupError",[226,466,467],{"class":235},"(",[226,469,470],{"class":299},"\"model not registered — check the id and the profile\"",[226,472,441],{"class":235},[226,474,475],{"class":111,"line":260},[226,476,257],{"emptyLinePlaceholder":256},[226,478,479,481,484,486],{"class":111,"line":272},[226,480,275],{"class":231},[226,482,483],{"class":235}," param ",[226,485,281],{"class":231},[226,487,488],{"class":235}," alg.parameterDefinitions():\n",[226,490,491,494,496,499,502,505,508,511,514,517,520,523,526],{"class":111,"line":287},[226,492,493],{"class":309},"    print",[226,495,467],{"class":235},[226,497,498],{"class":231},"f",[226,500,501],{"class":299},"\"in  ",[226,503,504],{"class":309},"{",[226,506,507],{"class":235},"param.name()",[226,509,510],{"class":231},":40",[226,512,513],{"class":309},"}",[226,515,516],{"class":309}," {type",[226,518,519],{"class":235},"(param).",[226,521,522],{"class":309},"__name__}",[226,524,525],{"class":299},"\"",[226,527,441],{"class":235},[226,529,530,532,535,537],{"class":111,"line":306},[226,531,275],{"class":231},[226,533,534],{"class":235}," output ",[226,536,281],{"class":231},[226,538,539],{"class":235}," alg.outputDefinitions():\n",[226,541,543,545,547,549,552,554,557,559,561,563,566,568,570],{"class":111,"line":542},8,[226,544,493],{"class":309},[226,546,467],{"class":235},[226,548,498],{"class":231},[226,550,551],{"class":299},"\"out ",[226,553,504],{"class":309},[226,555,556],{"class":235},"output.name()",[226,558,510],{"class":231},[226,560,513],{"class":309},[226,562,516],{"class":309},[226,564,565],{"class":235},"(output).",[226,567,522],{"class":309},[226,569,525],{"class":299},[226,571,441],{"class":235},[14,573,574,576,577,580,581,584,585,588,589,592,593,596,597,600],{},[192,575,324],{}," ",[27,578,579],{},"parameterDefinitions()"," and ",[27,582,583],{},"outputDefinitions()"," give the exact strings the parameter dictionary needs, plus the type each expects — which distinguishes a ",[27,586,587],{},"QgsProcessingParameterFeatureSource"," that accepts a layer or a path from a ",[27,590,591],{},"QgsProcessingParameterNumber"," that does not. ",[27,594,595],{},"algorithmById()"," returning ",[27,598,599],{},"None"," is the usual symptom of a model that is present as a file but not registered, which happens when it was dropped into the wrong directory or added after QGIS started.",[181,602,604],{"id":603},"load-a-model-that-is-not-installed","Load a model that is not installed",[14,606,607],{},"For a model kept in a repository alongside the script — the arrangement that makes it reviewable — load it directly.",[217,609,611],{"className":219,"code":610,"language":221,"meta":222,"style":222},"from qgis.core import QgsProcessingModelAlgorithm, QgsProcessingContext, QgsProcessingFeedback\n\nmodel = QgsProcessingModelAlgorithm()\nif not model.fromFile(\"\u002Fdata\u002Fmodels\u002Fcatchment_summary.model3\"):\n    raise RuntimeError(\"could not read the model file\")\n\ncontext = QgsProcessingContext()\nfeedback = QgsProcessingFeedback()\nresults, ok = model.run(\n    {\"dem\": \"\u002Fdata\u002Fdem_27700.tif\", \"catchments\": \"\u002Fdata\u002Fcatchments.gpkg\"},\n    context, feedback,\n)\nif not ok:\n    raise RuntimeError(\"model run failed\")\n",[27,612,613,624,628,638,654,668,672,682,692,703,729,735,740,750],{"__ignoreMap":222},[226,614,615,617,619,621],{"class":111,"line":228},[226,616,242],{"class":231},[226,618,245],{"class":235},[226,620,232],{"class":231},[226,622,623],{"class":235}," QgsProcessingModelAlgorithm, QgsProcessingContext, QgsProcessingFeedback\n",[226,625,626],{"class":111,"line":239},[226,627,257],{"emptyLinePlaceholder":256},[226,629,630,633,635],{"class":111,"line":253},[226,631,632],{"class":235},"model ",[226,634,266],{"class":231},[226,636,637],{"class":235}," QgsProcessingModelAlgorithm()\n",[226,639,640,642,645,648,651],{"class":111,"line":260},[226,641,446],{"class":231},[226,643,644],{"class":231}," not",[226,646,647],{"class":235}," model.fromFile(",[226,649,650],{"class":299},"\"\u002Fdata\u002Fmodels\u002Fcatchment_summary.model3\"",[226,652,653],{"class":235},"):\n",[226,655,656,658,661,663,666],{"class":111,"line":272},[226,657,461],{"class":231},[226,659,660],{"class":309}," RuntimeError",[226,662,467],{"class":235},[226,664,665],{"class":299},"\"could not read the model file\"",[226,667,441],{"class":235},[226,669,670],{"class":111,"line":287},[226,671,257],{"emptyLinePlaceholder":256},[226,673,674,677,679],{"class":111,"line":306},[226,675,676],{"class":235},"context ",[226,678,266],{"class":231},[226,680,681],{"class":235}," QgsProcessingContext()\n",[226,683,684,687,689],{"class":111,"line":542},[226,685,686],{"class":235},"feedback ",[226,688,266],{"class":231},[226,690,691],{"class":235}," QgsProcessingFeedback()\n",[226,693,695,698,700],{"class":111,"line":694},9,[226,696,697],{"class":235},"results, ok ",[226,699,266],{"class":231},[226,701,702],{"class":235}," model.run(\n",[226,704,706,709,712,714,716,719,722,724,726],{"class":111,"line":705},10,[226,707,708],{"class":235},"    {",[226,710,711],{"class":299},"\"dem\"",[226,713,366],{"class":235},[226,715,369],{"class":299},[226,717,718],{"class":235},", ",[226,720,721],{"class":299},"\"catchments\"",[226,723,366],{"class":235},[226,725,382],{"class":299},[226,727,728],{"class":235},"},\n",[226,730,732],{"class":111,"line":731},11,[226,733,734],{"class":235},"    context, feedback,\n",[226,736,738],{"class":111,"line":737},12,[226,739,441],{"class":235},[226,741,743,745,747],{"class":111,"line":742},13,[226,744,446],{"class":231},[226,746,644],{"class":231},[226,748,749],{"class":235}," ok:\n",[226,751,753,755,757,759,762],{"class":111,"line":752},14,[226,754,461],{"class":231},[226,756,660],{"class":309},[226,758,467],{"class":235},[226,760,761],{"class":299},"\"model run failed\"",[226,763,441],{"class":235},[14,765,766,576,768,771,772,775,776,779,780,783,784,786],{},[192,767,324],{},[27,769,770],{},"fromFile()"," returns a boolean rather than raising, and a ",[27,773,774],{},"False"," usually means a version mismatch — a model saved by a newer QGIS can fail to load in an older one. ",[27,777,778],{},"run()"," on the algorithm object takes an explicit context and feedback and returns a ",[27,781,782],{},"(results, ok)"," tuple, unlike ",[27,785,149],{}," which raises. The context matters more here than usual: temporary outputs are owned by it, so letting it go out of scope while the results are still needed loses them.",[14,788,789],{},"Registering the loaded model instead makes it available by id for the rest of the session, which is tidier when several scripts share it:",[217,791,793],{"className":219,"code":792,"language":221,"meta":222,"style":222},"QgsApplication.processingRegistry().providerById(\"model\").refreshAlgorithms()\n",[27,794,795],{"__ignoreMap":222},[226,796,797,800,803],{"class":111,"line":228},[226,798,799],{"class":235},"QgsApplication.processingRegistry().providerById(",[226,801,802],{"class":299},"\"model\"",[226,804,805],{"class":235},").refreshAlgorithms()\n",[14,807,808,810],{},[192,809,324],{}," Refreshing the model provider re-scans the models directory, which picks up a file copied into place after startup. It is the programmatic equivalent of the modeller's reload, and worth calling in a plugin that ships models of its own.",[14,812,813],{},[34,814,817,820,823,826,829,835,841,844,847,851,855,859,862,865,869,872,875,878,881,884],{"viewBox":815,"role":37,"ariaLabel":816,"xmlns":39},"0 0 760 288","A decision between maintaining a workflow as a graphical model and writing it as a Python chain, based on who edits it and how much control flow it needs",[41,818,819],{},"Model or Python chain",[45,821,822],{},"A graphical model suits a linear workflow maintained by someone who does not write Python, and is reviewable as a diagram. A Python chain suits anything needing loops, conditionals, error handling or per feature logic, and is reviewable as a diff. Mixing them is common: a model for the analysis, a script for the orchestration.",[49,824],{"x":51,"y":51,"width":52,"height":825,"fill":54},"288",[73,827,828],{"x":75,"y":76,"style":77,"fill":78,"textAnchor":79},"The question is who maintains it, not which is more powerful",[49,830],{"x":831,"y":832,"width":833,"height":834,"rx":87,"fill":88,"stroke":89,"style":90},"26","52","330","204",[73,836,840],{"x":837,"y":838,"style":839,"fill":89,"textAnchor":79},"191","80","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","graphical model",[73,842,843],{"x":837,"y":99,"style":174,"fill":71,"textAnchor":79},"linear or lightly branching",[73,845,846],{"x":837,"y":130,"style":174,"fill":71,"textAnchor":79},"edited without Python",[73,848,850],{"x":837,"y":849,"style":174,"fill":71,"textAnchor":79},"152","runs in the toolbox and in batch",[73,852,854],{"x":837,"y":853,"style":174,"fill":71,"textAnchor":79},"174","reviewable as a diagram",[73,856,858],{"x":837,"y":834,"style":174,"fill":857,"textAnchor":79},"#b91c1c","no loops, no conditionals",[73,860,861],{"x":837,"y":168,"style":174,"fill":857,"textAnchor":79},"diffs are unreadable XML",[49,863],{"x":864,"y":832,"width":833,"height":834,"rx":87,"fill":143,"stroke":144,"style":90},"404",[73,866,868],{"x":867,"y":838,"style":839,"fill":144,"textAnchor":79},"569","Python chain",[73,870,871],{"x":867,"y":99,"style":174,"fill":71,"textAnchor":79},"loops, branches, retries",[73,873,874],{"x":867,"y":130,"style":174,"fill":71,"textAnchor":79},"real error handling",[73,876,877],{"x":867,"y":849,"style":174,"fill":71,"textAnchor":79},"testable, diffable",[73,879,880],{"x":867,"y":853,"style":174,"fill":71,"textAnchor":79},"schedulable anywhere",[73,882,883],{"x":867,"y":834,"style":174,"fill":89,"textAnchor":79},"needs a Python author",[73,885,886],{"x":867,"y":168,"style":174,"fill":89,"textAnchor":79},"not visible in the toolbox",[181,888,890],{"id":889},"keeping-a-model-and-its-driver-in-step","Keeping a model and its driver in step",[14,892,893],{},"The awkward part of this arrangement is that a model's output parameter ids are generated, and restructuring the model changes them. A script written against yesterday's ids fails today with an error that names a string nobody chose.",[14,895,896],{},"The defence is a validation step that runs before the work does.",[217,898,900],{"className":219,"code":899,"language":221,"meta":222,"style":222},"from qgis.core import QgsApplication\n\nEXPECTED_INPUTS = {\"dem\", \"catchments\"}\nEXPECTED_OUTPUTS = {\"native:zonalstatisticsfb_1:summary\"}\n\n\ndef check_model(model_id):\n    alg = QgsApplication.processingRegistry().algorithmById(model_id)\n    if alg is None:\n        raise LookupError(f\"{model_id} is not registered\")\n\n    inputs = {p.name() for p in alg.parameterDefinitions()}\n    outputs = {o.name() for o in alg.outputDefinitions()}\n\n    if not EXPECTED_INPUTS \u003C= inputs:\n        raise RuntimeError(f\"model inputs changed: expected {EXPECTED_INPUTS}, found {inputs}\")\n    if not EXPECTED_OUTPUTS \u003C= outputs:\n        raise RuntimeError(f\"model outputs changed: expected {EXPECTED_OUTPUTS}, found {outputs}\")\n    return alg\n",[27,901,902,912,916,936,950,954,958,970,980,992,1017,1021,1041,1061,1065,1081,1112,1127,1157],{"__ignoreMap":222},[226,903,904,906,908,910],{"class":111,"line":228},[226,905,242],{"class":231},[226,907,245],{"class":235},[226,909,232],{"class":231},[226,911,250],{"class":235},[226,913,914],{"class":111,"line":239},[226,915,257],{"emptyLinePlaceholder":256},[226,917,918,921,924,927,929,931,933],{"class":111,"line":253},[226,919,920],{"class":309},"EXPECTED_INPUTS",[226,922,923],{"class":231}," =",[226,925,926],{"class":235}," {",[226,928,711],{"class":299},[226,930,718],{"class":235},[226,932,721],{"class":299},[226,934,935],{"class":235},"}\n",[226,937,938,941,943,945,948],{"class":111,"line":260},[226,939,940],{"class":309},"EXPECTED_OUTPUTS",[226,942,923],{"class":231},[226,944,926],{"class":235},[226,946,947],{"class":299},"\"native:zonalstatisticsfb_1:summary\"",[226,949,935],{"class":235},[226,951,952],{"class":111,"line":272},[226,953,257],{"emptyLinePlaceholder":256},[226,955,956],{"class":111,"line":287},[226,957,257],{"emptyLinePlaceholder":256},[226,959,960,963,967],{"class":111,"line":306},[226,961,962],{"class":231},"def",[226,964,966],{"class":965},"svObZ"," check_model",[226,968,969],{"class":235},"(model_id):\n",[226,971,972,975,977],{"class":111,"line":542},[226,973,974],{"class":235},"    alg ",[226,976,266],{"class":231},[226,978,979],{"class":235}," QgsApplication.processingRegistry().algorithmById(model_id)\n",[226,981,982,984,986,988,990],{"class":111,"line":694},[226,983,290],{"class":231},[226,985,278],{"class":235},[226,987,451],{"class":231},[226,989,454],{"class":309},[226,991,303],{"class":235},[226,993,994,997,999,1001,1003,1005,1007,1010,1012,1015],{"class":111,"line":705},[226,995,996],{"class":231},"        raise",[226,998,464],{"class":309},[226,1000,467],{"class":235},[226,1002,498],{"class":231},[226,1004,525],{"class":299},[226,1006,504],{"class":309},[226,1008,1009],{"class":235},"model_id",[226,1011,513],{"class":309},[226,1013,1014],{"class":299}," is not registered\"",[226,1016,441],{"class":235},[226,1018,1019],{"class":111,"line":731},[226,1020,257],{"emptyLinePlaceholder":256},[226,1022,1023,1026,1028,1031,1033,1036,1038],{"class":111,"line":737},[226,1024,1025],{"class":235},"    inputs ",[226,1027,266],{"class":231},[226,1029,1030],{"class":235}," {p.name() ",[226,1032,275],{"class":231},[226,1034,1035],{"class":235}," p ",[226,1037,281],{"class":231},[226,1039,1040],{"class":235}," alg.parameterDefinitions()}\n",[226,1042,1043,1046,1048,1051,1053,1056,1058],{"class":111,"line":742},[226,1044,1045],{"class":235},"    outputs ",[226,1047,266],{"class":231},[226,1049,1050],{"class":235}," {o.name() ",[226,1052,275],{"class":231},[226,1054,1055],{"class":235}," o ",[226,1057,281],{"class":231},[226,1059,1060],{"class":235}," alg.outputDefinitions()}\n",[226,1062,1063],{"class":111,"line":752},[226,1064,257],{"emptyLinePlaceholder":256},[226,1066,1068,1070,1072,1075,1078],{"class":111,"line":1067},15,[226,1069,290],{"class":231},[226,1071,644],{"class":231},[226,1073,1074],{"class":309}," EXPECTED_INPUTS",[226,1076,1077],{"class":231}," \u003C=",[226,1079,1080],{"class":235}," inputs:\n",[226,1082,1084,1086,1088,1090,1092,1095,1098,1101,1103,1106,1108,1110],{"class":111,"line":1083},16,[226,1085,996],{"class":231},[226,1087,660],{"class":309},[226,1089,467],{"class":235},[226,1091,498],{"class":231},[226,1093,1094],{"class":299},"\"model inputs changed: expected ",[226,1096,1097],{"class":309},"{EXPECTED_INPUTS}",[226,1099,1100],{"class":299},", found ",[226,1102,504],{"class":309},[226,1104,1105],{"class":235},"inputs",[226,1107,513],{"class":309},[226,1109,525],{"class":299},[226,1111,441],{"class":235},[226,1113,1115,1117,1119,1122,1124],{"class":111,"line":1114},17,[226,1116,290],{"class":231},[226,1118,644],{"class":231},[226,1120,1121],{"class":309}," EXPECTED_OUTPUTS",[226,1123,1077],{"class":231},[226,1125,1126],{"class":235}," outputs:\n",[226,1128,1130,1132,1134,1136,1138,1141,1144,1146,1148,1151,1153,1155],{"class":111,"line":1129},18,[226,1131,996],{"class":231},[226,1133,660],{"class":309},[226,1135,467],{"class":235},[226,1137,498],{"class":231},[226,1139,1140],{"class":299},"\"model outputs changed: expected ",[226,1142,1143],{"class":309},"{EXPECTED_OUTPUTS}",[226,1145,1100],{"class":299},[226,1147,504],{"class":309},[226,1149,1150],{"class":235},"outputs",[226,1152,513],{"class":309},[226,1154,525],{"class":299},[226,1156,441],{"class":235},[226,1158,1160,1163],{"class":111,"line":1159},19,[226,1161,1162],{"class":231},"    return",[226,1164,1165],{"class":235}," alg\n",[14,1167,1168,1170],{},[192,1169,324],{}," Using subset comparison rather than equality lets the model gain parameters without breaking the driver, which is the common and harmless change; it still catches a rename or a removal, which are the breaking ones. Raising with both the expected and the actual sets means the fix is visible in the error message rather than requiring somebody to open the modeller. Calling this once at the top of a scheduled job converts a mid-run failure into a startup failure, which is far cheaper when the run takes an hour.",[14,1172,1173,1174,1177],{},"Pairing the check with a note in the model's own description — \"driven by ",[27,1175,1176],{},"nightly_catchments.py","; renaming outputs will break it\" — closes the loop from the other direction, so the analyst editing the model has some warning that something depends on it.",[181,1179,1181],{"id":1180},"loop-a-model-over-many-inputs","Loop a model over many inputs",[14,1183,1184],{},"The combination that justifies the whole approach: the model holds the analysis, the script holds the iteration.",[217,1186,1188],{"className":219,"code":1187,"language":221,"meta":222,"style":222},"from pathlib import Path\nimport processing\n\nfeedback = QgsProcessingFeedback()\nfailures = []\n\nfor dem in sorted(Path(\"\u002Fdata\u002Ftiles\").glob(\"*.tif\")):\n    out = Path(\"\u002Fdata\u002Foutput\") \u002F f\"{dem.stem}_summary.gpkg\"\n    try:\n        processing.run(\"model:catchment_summary\", {\n            \"dem\": str(dem),\n            \"catchments\": \"\u002Fdata\u002Fcatchments.gpkg\",\n            \"native:zonalstatisticsfb_1:summary\": str(out),\n        }, feedback=feedback)\n    except Exception as exc:\n        failures.append((dem.name, str(exc)))\n\nif failures:\n    print(f\"{len(failures)} tile(s) failed:\")\n    for name, message in failures:\n        print(f\"  {name}: {message}\")\n",[27,1189,1190,1202,1208,1212,1220,1230,1234,1261,1295,1302,1311,1324,1335,1347,1361,1375,1385,1389,1396,1419,1432],{"__ignoreMap":222},[226,1191,1192,1194,1197,1199],{"class":111,"line":228},[226,1193,242],{"class":231},[226,1195,1196],{"class":235}," pathlib ",[226,1198,232],{"class":231},[226,1200,1201],{"class":235}," Path\n",[226,1203,1204,1206],{"class":111,"line":239},[226,1205,232],{"class":231},[226,1207,236],{"class":235},[226,1209,1210],{"class":111,"line":253},[226,1211,257],{"emptyLinePlaceholder":256},[226,1213,1214,1216,1218],{"class":111,"line":260},[226,1215,686],{"class":235},[226,1217,266],{"class":231},[226,1219,691],{"class":235},[226,1221,1222,1225,1227],{"class":111,"line":272},[226,1223,1224],{"class":235},"failures ",[226,1226,266],{"class":231},[226,1228,1229],{"class":235}," []\n",[226,1231,1232],{"class":111,"line":287},[226,1233,257],{"emptyLinePlaceholder":256},[226,1235,1236,1238,1241,1243,1246,1249,1252,1255,1258],{"class":111,"line":306},[226,1237,275],{"class":231},[226,1239,1240],{"class":235}," dem ",[226,1242,281],{"class":231},[226,1244,1245],{"class":309}," sorted",[226,1247,1248],{"class":235},"(Path(",[226,1250,1251],{"class":299},"\"\u002Fdata\u002Ftiles\"",[226,1253,1254],{"class":235},").glob(",[226,1256,1257],{"class":299},"\"*.tif\"",[226,1259,1260],{"class":235},")):\n",[226,1262,1263,1266,1268,1271,1274,1277,1280,1283,1285,1287,1290,1292],{"class":111,"line":542},[226,1264,1265],{"class":235},"    out ",[226,1267,266],{"class":231},[226,1269,1270],{"class":235}," Path(",[226,1272,1273],{"class":299},"\"\u002Fdata\u002Foutput\"",[226,1275,1276],{"class":235},") ",[226,1278,1279],{"class":231},"\u002F",[226,1281,1282],{"class":231}," f",[226,1284,525],{"class":299},[226,1286,504],{"class":309},[226,1288,1289],{"class":235},"dem.stem",[226,1291,513],{"class":309},[226,1293,1294],{"class":299},"_summary.gpkg\"\n",[226,1296,1297,1300],{"class":111,"line":694},[226,1298,1299],{"class":231},"    try",[226,1301,303],{"class":235},[226,1303,1304,1307,1309],{"class":111,"line":705},[226,1305,1306],{"class":235},"        processing.run(",[226,1308,355],{"class":299},[226,1310,358],{"class":235},[226,1312,1313,1316,1318,1321],{"class":111,"line":731},[226,1314,1315],{"class":299},"            \"dem\"",[226,1317,366],{"class":235},[226,1319,1320],{"class":309},"str",[226,1322,1323],{"class":235},"(dem),\n",[226,1325,1326,1329,1331,1333],{"class":111,"line":737},[226,1327,1328],{"class":299},"            \"catchments\"",[226,1330,366],{"class":235},[226,1332,382],{"class":299},[226,1334,372],{"class":235},[226,1336,1337,1340,1342,1344],{"class":111,"line":742},[226,1338,1339],{"class":299},"            \"native:zonalstatisticsfb_1:summary\"",[226,1341,366],{"class":235},[226,1343,1320],{"class":309},[226,1345,1346],{"class":235},"(out),\n",[226,1348,1349,1352,1356,1358],{"class":111,"line":752},[226,1350,1351],{"class":235},"        }, ",[226,1353,1355],{"class":1354},"s9osk","feedback",[226,1357,266],{"class":231},[226,1359,1360],{"class":235},"feedback)\n",[226,1362,1363,1366,1369,1372],{"class":111,"line":1067},[226,1364,1365],{"class":231},"    except",[226,1367,1368],{"class":309}," Exception",[226,1370,1371],{"class":231}," as",[226,1373,1374],{"class":235}," exc:\n",[226,1376,1377,1380,1382],{"class":111,"line":1083},[226,1378,1379],{"class":235},"        failures.append((dem.name, ",[226,1381,1320],{"class":309},[226,1383,1384],{"class":235},"(exc)))\n",[226,1386,1387],{"class":111,"line":1114},[226,1388,257],{"emptyLinePlaceholder":256},[226,1390,1391,1393],{"class":111,"line":1129},[226,1392,446],{"class":231},[226,1394,1395],{"class":235}," failures:\n",[226,1397,1398,1400,1402,1404,1406,1409,1412,1414,1417],{"class":111,"line":1159},[226,1399,493],{"class":309},[226,1401,467],{"class":235},[226,1403,498],{"class":231},[226,1405,525],{"class":299},[226,1407,1408],{"class":309},"{len",[226,1410,1411],{"class":235},"(failures)",[226,1413,513],{"class":309},[226,1415,1416],{"class":299}," tile(s) failed:\"",[226,1418,441],{"class":235},[226,1420,1422,1425,1428,1430],{"class":111,"line":1421},20,[226,1423,1424],{"class":231},"    for",[226,1426,1427],{"class":235}," name, message ",[226,1429,281],{"class":231},[226,1431,1395],{"class":235},[226,1433,1435,1437,1439,1441,1444,1446,1448,1450,1452,1454,1457,1459,1461],{"class":111,"line":1434},21,[226,1436,310],{"class":309},[226,1438,467],{"class":235},[226,1440,498],{"class":231},[226,1442,1443],{"class":299},"\"  ",[226,1445,504],{"class":309},[226,1447,329],{"class":235},[226,1449,513],{"class":309},[226,1451,366],{"class":299},[226,1453,504],{"class":309},[226,1455,1456],{"class":235},"message",[226,1458,513],{"class":309},[226,1460,525],{"class":299},[226,1462,441],{"class":235},[14,1464,1465,1467,1468,1471,1472,1476],{},[192,1466,324],{}," Collecting failures rather than stopping on the first is right for a tile sweep, where one corrupt input should not abandon two hundred good ones — but the summary at the end is what stops the failures being invisible. Catching broad ",[27,1469,1470],{},"Exception"," is defensible in this specific shape because the loop's purpose is resilience; the printed message preserves what went wrong. Pairing this with the ",[21,1473,1475],{"href":1474},"\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Fhandle-processing-feedback-and-errors-pyqgis\u002F","feedback object"," gives warnings as well as failures.",[181,1478,1480],{"id":1479},"model-or-python","Model or Python",[14,1482,1483,1484,1488],{},"Models win when the workflow is linear, the person who owns it does not write Python, and it needs to appear in the toolbox for interactive use. They also get batch mode and the ",[21,1485,1487],{"href":1486},"\u002Fpyqgis-fundamentals-environment-setup\u002Fheadless-qgis-and-server-automation\u002Fuse-qgis-process-command-line-runner\u002F","qgis_process runner"," for free.",[14,1490,1491,1492,1494,1495,1497],{},"Python wins the moment control flow appears — a loop, a conditional, a retry, anything that depends on a value computed halfway through. It also wins on review: a ",[27,1493,29],{}," is XML that diffs unreadably, while a chain of ",[27,1496,149],{}," calls is a diff anybody can read.",[14,1499,1500],{},"The productive arrangement is usually both: keep the analysis in a model so the domain expert can adjust it, and drive it from a script that handles inputs, iteration, errors and outputs.",[181,1502,1504],{"id":1503},"qgis-version-compatibility","QGIS version compatibility",[14,1506,1507,1508,1510],{},"The examples target ",[192,1509,194],{}," (Python 3.12).",[1512,1513,1514,1530],"table",{},[1515,1516,1517],"thead",{},[1518,1519,1520,1524,1527],"tr",{},[1521,1522,1523],"th",{},"QGIS version",[1521,1525,1526],{},"Python",[1521,1528,1529],{},"Notes",[1531,1532,1533,1551,1565,1575,1586],"tbody",{},[1518,1534,1535,1539,1542],{},[1536,1537,1538],"td",{},"3.16 LTR",[1536,1540,1541],{},"3.7",[1536,1543,1544,1546,1547,1550],{},[27,1545,29],{}," format and ",[27,1548,1549],{},"model:"," provider namespace in use.",[1518,1552,1553,1556,1559],{},[1536,1554,1555],{},"3.22 LTR",[1536,1557,1558],{},"3.9",[1536,1560,1561,1562,1564],{},"Models can be embedded in a project and appear under the ",[27,1563,333],{}," provider.",[1518,1566,1567,1570,1572],{},[1536,1568,1569],{},"3.28 LTR",[1536,1571,1558],{},[1536,1573,1574],{},"Model output parameter naming stabilised.",[1518,1576,1577,1580,1583],{},[1536,1578,1579],{},"3.34 LTR",[1536,1581,1582],{},"3.12",[1536,1584,1585],{},"Baseline for this page.",[1518,1587,1588,1591,1593],{},[1536,1589,1590],{},"3.40+",[1536,1592,1582],{},[1536,1594,1595],{},"Modeller gains conditional branches, narrowing the gap with scripted chains.",[181,1597,1599],{"id":1598},"troubleshooting","Troubleshooting",[186,1601,1602,1614,1623,1631,1639,1645],{},[189,1603,1604,1609,1610,1613],{},[192,1605,1606,1608],{},[27,1607,595],{}," returns None."," The model is not registered. Check the profile directory and call ",[27,1611,1612],{},"refreshAlgorithms()",".",[189,1615,1616,1619,1620,1622],{},[192,1617,1618],{},"A parameter name is rejected."," Names are normalised from the modeller's labels. List ",[27,1621,579],{}," rather than guessing.",[189,1624,1625,1630],{},[192,1626,1627,1629],{},[27,1628,770],{}," returns False."," The model was saved by a newer QGIS, or the file is corrupt. Check the version it was authored in.",[189,1632,1633,1636,1637,1613],{},[192,1634,1635],{},"The model runs but writes nothing."," The output parameter id is wrong, so the result went to a temporary file. Compare against ",[27,1638,583],{},[189,1640,1641,1644],{},[192,1642,1643],{},"It works interactively and fails in a script."," A provider the model depends on is not initialised headlessly. Initialise Processing fully.",[189,1646,1647,1650,1651,1655],{},[192,1648,1649],{},"Results differ from the modeller run."," The script's ",[21,1652,1654],{"href":1653},"\u002Fspatial-data-processing-automation\u002Fcoordinate-reference-systems\u002Fset-project-crs-and-datum-transform-pyqgis\u002F","transform context"," differs from the project's. Pass the project.",[181,1657,1659],{"id":1658},"conclusion","Conclusion",[14,1661,1662],{},"List the registry to get the exact model id, ask the algorithm for its parameter names rather than reading them off a diagram, and drive the model from a script that owns the iteration and the error handling. Keep the analysis in the model where a domain expert can maintain it, and everything with control flow in Python.",[181,1664,1666],{"id":1665},"frequently-asked-questions","Frequently Asked Questions",[14,1668,1669,1672,1673,1675,1676,1679],{},[192,1670,1671],{},"Where are models stored?","\nIn ",[27,1674,105],{}," inside the active user profile directory, which ",[27,1677,1678],{},"QgsApplication.qgisSettingsDirPath()"," reports. Models embedded in a project live in the project file instead.",[14,1681,1682,1685],{},[192,1683,1684],{},"Can a plugin ship models?","\nYes — install them into the profile's models directory on first run, or add a custom provider. Refreshing the model provider afterwards makes them available without a restart.",[14,1687,1688,1691,1692,1613],{},[192,1689,1690],{},"Can I convert a model into Python?","\nThe modeller offers an export to a Processing script, which produces a class-based algorithm rather than a flat chain. It is a reasonable starting point when a model has outgrown the modeller — see ",[21,1693,1695],{"href":1694},"\u002Fqgis-plugin-development\u002Fprocessing-provider-plugins\u002Fwrite-custom-processing-algorithm-pyqgis\u002F","writing a custom processing algorithm",[14,1697,1698,1705],{},[192,1699,1700,1701,1704],{},"Do models work with ",[27,1702,1703],{},"qgis_process","?","\nYes, by id, exactly like a native algorithm. That is often the simplest way to schedule one without writing any Python at all.",[181,1707,1709],{"id":1708},"related","Related",[186,1711,1712,1717,1722,1728,1733],{},[189,1713,1714,1716],{},[21,1715,24],{"href":23}," — the guide this recipe belongs to",[189,1718,1719],{},[21,1720,1721],{"href":1474},"Handle Processing Feedback and Errors in PyQGIS",[189,1723,1724],{},[21,1725,1727],{"href":1726},"\u002Fspatial-data-processing-automation\u002Fbatch-processing-with-pyqgis\u002Frun-algorithm-over-folder-of-files-pyqgis\u002F","Run an Algorithm Over a Folder of Files in PyQGIS",[189,1729,1730],{},[21,1731,1732],{"href":1694},"Write a Custom Processing Algorithm in PyQGIS",[189,1734,1735],{},[21,1736,1737],{"href":1486},"Use the qgis_process Command Line Runner",[1739,1740,1741],"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 .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);}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}",{"title":222,"searchDepth":239,"depth":239,"links":1743},[1744,1745,1746,1747,1748,1749,1750,1751,1752,1753,1754,1755],{"id":183,"depth":239,"text":184},{"id":207,"depth":239,"text":208},{"id":417,"depth":239,"text":418},{"id":603,"depth":239,"text":604},{"id":889,"depth":239,"text":890},{"id":1180,"depth":239,"text":1181},{"id":1479,"depth":239,"text":1480},{"id":1503,"depth":239,"text":1504},{"id":1598,"depth":239,"text":1599},{"id":1658,"depth":239,"text":1659},{"id":1665,"depth":239,"text":1666},{"id":1708,"depth":239,"text":1709},"Execute a QGIS Processing model from a script — loading a .model3 file, calling it by id, inspecting its parameters, and deciding when a model beats a hand-written chain.","md",{"slug":1759,"type":1760,"breadcrumb":1761,"datePublished":1762,"dateModified":1762},"run-graphical-model-from-python-pyqgis","article","Run a Model from Python","2026-08-27","\u002Fspatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Frun-graphical-model-from-python-pyqgis",{"title":5,"description":1756},"spatial-data-processing-automation\u002Fchaining-processing-algorithms\u002Frun-graphical-model-from-python-pyqgis\u002Findex","9jx73qZb5m2t-ZyGTiY2GUYMam6Ch8XOJq207_x1P8U",1787823363563]