[{"data":1,"prerenderedAt":1753},["ShallowReactive",2],{"doc:\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fcreate-layer-actions-pyqgis":3},{"id":4,"title":5,"body":6,"description":1742,"extension":1743,"meta":1744,"navigation":270,"path":1749,"seo":1750,"stem":1751,"__hash__":1752},"docs\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fcreate-layer-actions-pyqgis\u002Findex.md","Create Layer Actions in PyQGIS",{"type":7,"value":8,"toc":1729},"minimark",[9,13,22,31,195,200,222,226,238,416,434,438,441,519,705,736,740,743,831,1098,1120,1124,1127,1318,1334,1338,1345,1543,1565,1569,1594,1598,1647,1651,1654,1658,1668,1678,1684,1690,1694,1725],[10,11,5],"h1",{"id":12},"create-layer-actions-in-pyqgis",[14,15,16,17,21],"p",{},"A layer action is a small command attached to a layer that runs against one feature: open the asset's page in the maintenance system, zoom to every parcel with the same owner, copy a formatted grid reference to the clipboard, email the inspection record. Users run it from the ",[18,19,20],"em",{},"Run Feature Action"," tool on the map, from a button column in the attribute table, or from the feature form. It is the lightest way to connect a map to the rest of an organisation's systems — no plugin to install, because the action is stored with the layer.",[14,23,24,25,30],{},"This recipe belongs to ",[26,27,29],"a",{"href":28},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002F","Attribute Forms & Layer Actions in PyQGIS",". It builds URL and Python actions from code, explains how attribute values are substituted into them, controls where each action appears, and runs actions from scripts.",[14,32,33],{},[34,35,40,44,48,55,72,81,90,96,99,103,106,110,118,121,125,135,141,146,151,155,161,164,172,177,182,187,191],"svg",{"viewBox":36,"role":37,"ariaLabel":38,"xmlns":39},"0 0 760 300","img","How a layer action runs: the user picks a feature from the map, attribute table or form, QGIS substitutes that feature's values into the action text, and the action either opens a URL or runs Python","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg",[41,42,43],"title",{},"One feature in, one command out",[45,46,47],"desc",{},"Three entry points on the left: the Run Feature Action map tool, the actions column in the attribute table, and the actions bar in a feature form. Each passes the chosen feature to the layer's action list. QGIS evaluates the expression placeholders in the action text with that feature's values, then either opens the resulting URL in a browser or executes the resulting Python code inside QGIS.",[49,50],"rect",{"x":51,"y":51,"width":52,"height":53,"fill":54},"0","760","300","#f6f3ea",[56,57,58],"defs",{},[59,60,67],"marker",{"id":61,"viewBox":62,"refX":63,"refY":64,"markerWidth":65,"markerHeight":65,"orient":66},"laRunArrow","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","Pick a feature, get a command built from it",[49,82],{"x":83,"y":84,"width":85,"height":86,"rx":63,"fill":87,"stroke":88,"style":89},"20","54","170","48","#fffdf7","#59645f","stroke-width:1.8",[73,91,95],{"x":92,"y":93,"style":94,"fill":71,"textAnchor":79},"105","83","text-anchor:middle;font-size:10.5px;font-family:sans-serif","map: action tool",[49,97],{"x":83,"y":98,"width":85,"height":86,"rx":63,"fill":87,"stroke":88,"style":89},"126",[73,100,102],{"x":92,"y":101,"style":94,"fill":71,"textAnchor":79},"155","attribute table column",[49,104],{"x":83,"y":105,"width":85,"height":86,"rx":63,"fill":87,"stroke":88,"style":89},"198",[73,107,109],{"x":92,"y":108,"style":94,"fill":71,"textAnchor":79},"227","feature form bar",[111,112],"line",{"x1":113,"y1":114,"x2":115,"y2":116,"stroke":71,"style":117},"190","78","244","138","stroke-width:1.6;marker-end:url(#laRunArrow)",[111,119],{"x1":113,"y1":120,"x2":115,"y2":120,"stroke":71,"style":117},"150",[111,122],{"x1":113,"y1":123,"x2":115,"y2":124,"stroke":71,"style":117},"222","162",[49,126],{"x":127,"y":128,"width":129,"height":130,"rx":131,"fill":132,"stroke":133,"style":134},"252","96","220","108","10","#eef7f4","#0f766e","stroke-width:2.5",[73,136,140],{"x":137,"y":138,"style":139,"fill":133,"textAnchor":79},"362","122","text-anchor:middle;font-size:11px;font-weight:bold;font-family:sans-serif","substitute values",[73,142,145],{"x":137,"y":143,"style":144,"fill":71,"textAnchor":79},"148","text-anchor:middle;font-size:9.5px;font-family:monospace","…\u002Fassets\u002F{asset_id}",[73,147,150],{"x":137,"y":148,"style":149,"fill":71,"textAnchor":79},"166","text-anchor:middle;font-size:9.5px;font-family:sans-serif","becomes",[73,152,154],{"x":137,"y":153,"style":144,"fill":71,"textAnchor":79},"184","…\u002Fassets\u002FTR-10432",[111,156],{"x1":157,"y1":158,"x2":159,"y2":160,"stroke":71,"style":117},"472","130","530","92",[111,162],{"x1":157,"y1":85,"x2":159,"y2":163,"stroke":71,"style":117},"208",[49,165],{"x":166,"y":167,"width":105,"height":168,"rx":131,"fill":169,"stroke":170,"style":171},"538","56","70","#eff3ff","#2563eb","stroke-width:2",[73,173,176],{"x":174,"y":175,"style":139,"fill":170,"textAnchor":79},"637","84","OpenUrl",[73,178,181],{"x":174,"y":179,"style":180,"fill":71,"textAnchor":79},"106","text-anchor:middle;font-size:10px;font-family:sans-serif","system browser opens",[49,183],{"x":166,"y":184,"width":105,"height":168,"rx":131,"fill":185,"stroke":186,"style":171},"174","#fdf2e2","#b45309",[73,188,190],{"x":174,"y":189,"style":139,"fill":186,"textAnchor":79},"202","GenericPython",[73,192,194],{"x":174,"y":193,"style":180,"fill":71,"textAnchor":79},"224","code runs inside QGIS",[196,197,199],"h2",{"id":198},"prerequisites","Prerequisites",[201,202,203,211,214],"ul",{},[204,205,206,210],"li",{},[207,208,209],"strong",{},"QGIS 3.40 LTR"," or newer, or the QGIS 4 series.",[204,212,213],{},"A vector layer with an attribute that identifies each feature in some other system, if you want URL actions.",[204,215,216,217,221],{},"An understanding of ",[26,218,220],{"href":219},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fevaluate-qgis-expression-in-pyqgis\u002F","QGIS expressions",", because action text is expanded with them.",[196,223,225],{"id":224},"a-url-action-built-from-attributes","A URL action built from attributes",[14,227,228,229,233,234,237],{},"The simplest useful action opens a web page for the feature. The action text contains expression placeholders — an expression between ",[230,231,232],"code",{},"[%"," and ",[230,235,236],{},"%]"," — which QGIS evaluates against the chosen feature before opening the result.",[239,240,245],"pre",{"className":241,"code":242,"language":243,"meta":244,"style":244},"language-python shiki shiki-themes github-dark","from qgis.core import Qgis, QgsProject, QgsAction\n\nassets = QgsProject.instance().mapLayersByName(\"street_trees\")[0]\n\nopen_record = QgsAction(\n    Qgis.AttributeActionType.OpenUrl,\n    \"Open asset record\",\n    \"https:\u002F\u002Fassets.example.gov\u002Ftrees\u002F[% url_encode(map('id', \\\"asset_id\\\")) %]\",\n    \"\",\n    False,\n    \"Record\",\n    {\"Feature\", \"Canvas\"},\n)\nassets.actions().addAction(open_record)\nprint(open_record.id())\n","python","",[230,246,247,265,272,297,302,313,319,328,353,361,369,377,395,401,407],{"__ignoreMap":244},[248,249,251,255,259,262],"span",{"class":111,"line":250},1,[248,252,254],{"class":253},"snl16","from",[248,256,258],{"class":257},"s95oV"," qgis.core ",[248,260,261],{"class":253},"import",[248,263,264],{"class":257}," Qgis, QgsProject, QgsAction\n",[248,266,268],{"class":111,"line":267},2,[248,269,271],{"emptyLinePlaceholder":270},true,"\n",[248,273,275,278,281,284,288,291,294],{"class":111,"line":274},3,[248,276,277],{"class":257},"assets ",[248,279,280],{"class":253},"=",[248,282,283],{"class":257}," QgsProject.instance().mapLayersByName(",[248,285,287],{"class":286},"sU2Wk","\"street_trees\"",[248,289,290],{"class":257},")[",[248,292,51],{"class":293},"sDLfK",[248,295,296],{"class":257},"]\n",[248,298,300],{"class":111,"line":299},4,[248,301,271],{"emptyLinePlaceholder":270},[248,303,305,308,310],{"class":111,"line":304},5,[248,306,307],{"class":257},"open_record ",[248,309,280],{"class":253},[248,311,312],{"class":257}," QgsAction(\n",[248,314,316],{"class":111,"line":315},6,[248,317,318],{"class":257},"    Qgis.AttributeActionType.OpenUrl,\n",[248,320,322,325],{"class":111,"line":321},7,[248,323,324],{"class":286},"    \"Open asset record\"",[248,326,327],{"class":257},",\n",[248,329,331,334,337,340,343,346,348,351],{"class":111,"line":330},8,[248,332,333],{"class":286},"    \"https:\u002F\u002Fassets.example.gov\u002Ftrees\u002F[",[248,335,336],{"class":293},"% u",[248,338,339],{"class":286},"rl_encode(map('id', ",[248,341,342],{"class":293},"\\\"",[248,344,345],{"class":286},"asset_id",[248,347,342],{"class":293},[248,349,350],{"class":286},")) %]\"",[248,352,327],{"class":257},[248,354,356,359],{"class":111,"line":355},9,[248,357,358],{"class":286},"    \"\"",[248,360,327],{"class":257},[248,362,364,367],{"class":111,"line":363},10,[248,365,366],{"class":293},"    False",[248,368,327],{"class":257},[248,370,372,375],{"class":111,"line":371},11,[248,373,374],{"class":286},"    \"Record\"",[248,376,327],{"class":257},[248,378,380,383,386,389,392],{"class":111,"line":379},12,[248,381,382],{"class":257},"    {",[248,384,385],{"class":286},"\"Feature\"",[248,387,388],{"class":257},", ",[248,390,391],{"class":286},"\"Canvas\"",[248,393,394],{"class":257},"},\n",[248,396,398],{"class":111,"line":397},13,[248,399,400],{"class":257},")\n",[248,402,404],{"class":111,"line":403},14,[248,405,406],{"class":257},"assets.actions().addAction(open_record)\n",[248,408,410,413],{"class":111,"line":409},15,[248,411,412],{"class":293},"print",[248,414,415],{"class":257},"(open_record.id())\n",[14,417,418,421,422,425,426,429,430,433],{},[207,419,420],{},"Breakdown:"," The constructor takes the action type, a description shown in menus, the action text, an icon path, a capture flag, a short title used on buttons, and the set of scopes where the action appears. ",[230,423,424],{},"url_encode(map(…))"," produces a correctly escaped ",[230,427,428],{},"id=TR-10432"," query string; for a value inserted into a path segment, ",[230,431,432],{},"url_encode"," is not needed when ids contain only safe characters, but any user-entered text — a street name with an ampersand — needs escaping or the link breaks. The action gets a generated id, which is how it is removed or run later. Nothing needs saving explicitly: actions are part of the layer, stored in the project and in the layer's QML style.",[196,435,437],{"id":436},"a-python-action","A Python action",[14,439,440],{},"Python actions run inside QGIS with the same placeholders expanded first. That makes them powerful — anything PyQGIS can do, an action can do to the clicked feature — and it is also why the substituted values must never be pasted into code unquoted.",[14,442,443],{},[34,444,447,450,453,456,460,467,472,477,481,485,489,492,497,502,505,509,513,516],{"viewBox":445,"role":37,"ariaLabel":446,"xmlns":39},"0 0 760 262","Safe and unsafe substitution in a Python action: a raw attribute value pasted into code can break or inject code, while passing the feature id and reading attributes inside the code keeps user data out of the source",[41,448,449],{},"Substitute ids, not free text",[45,451,452],{},"Unsafe: the action text contains a print call with the owner name placeholder inside quotes; an owner called O'Brien closes the string early and breaks the code, and crafted text could run arbitrary commands. Safe: the action substitutes only the layer id and the numeric feature id, then fetches the feature in Python and reads the owner attribute as data, so no attribute text ever becomes code.",[49,454],{"x":51,"y":51,"width":52,"height":455,"fill":54},"262",[73,457,459],{"x":75,"y":458,"style":77,"fill":78,"textAnchor":79},"26","Attribute text must never become source code",[49,461],{"x":462,"y":463,"width":464,"height":465,"rx":131,"fill":185,"stroke":466,"style":134},"24","46","344","196","#b91c1c",[73,468,471],{"x":465,"y":469,"style":470,"fill":466,"textAnchor":79},"72","text-anchor:middle;font-size:12px;font-weight:bold;font-family:sans-serif","pasting the value in",[73,473,476],{"x":465,"y":474,"style":475,"fill":71,"textAnchor":79},"104","text-anchor:middle;font-size:10px;font-family:monospace","print('owner: {owner}')",[73,478,480],{"x":465,"y":479,"style":94,"fill":71,"textAnchor":79},"136","owner = O'Brien",[73,482,484],{"x":465,"y":483,"style":475,"fill":466,"textAnchor":79},"160","print('owner: O'Brien')",[73,486,488],{"x":465,"y":487,"style":94,"fill":466,"textAnchor":79},"200","syntax error — or worse,",[73,490,491],{"x":465,"y":129,"style":94,"fill":466,"textAnchor":79},"code chosen by whoever typed it",[49,493],{"x":494,"y":463,"width":464,"height":465,"rx":131,"fill":495,"stroke":496,"style":134},"392","#e8efe6","#15803d",[73,498,501],{"x":499,"y":469,"style":470,"fill":500,"textAnchor":79},"564","#166534","passing the id in",[73,503,504],{"x":499,"y":474,"style":475,"fill":71,"textAnchor":79},"fid = {feature id}",[73,506,508],{"x":499,"y":507,"style":475,"fill":71,"textAnchor":79},"128","f = layer.getFeature(fid)",[73,510,512],{"x":499,"y":511,"style":475,"fill":71,"textAnchor":79},"152","print('owner:', f['owner'])",[73,514,515],{"x":499,"y":487,"style":94,"fill":500,"textAnchor":79},"values stay data,",[73,517,518],{"x":499,"y":129,"style":94,"fill":500,"textAnchor":79},"whatever they contain",[239,520,522],{"className":241,"code":521,"language":243,"meta":244,"style":244},"SELECT_SAME_OWNER = \"\"\"\nfrom qgis.core import QgsProject, QgsFeatureRequest, QgsExpression\nfrom qgis.utils import iface\n\nlayer = QgsProject.instance().mapLayer('[% @layer_id %]')\nfeature = layer.getFeature([% $id %])\nowner = feature['owner_ref']\n\nexpr = f'\"owner_ref\" = {QgsExpression.quotedValue(owner)}'\nids = [f.id() for f in layer.getFeatures(\n    QgsFeatureRequest().setFilterExpression(expr).setNoAttributes())]\nlayer.selectByIds(ids)\niface.mapCanvas().zoomToSelected(layer)\niface.messageBar().pushInfo('Same owner', f'{len(ids)} parcels selected')\n\"\"\"\n\nselect_same_owner = QgsAction(\n    Qgis.AttributeActionType.GenericPython,\n    \"Select all parcels with this owner\",\n    SELECT_SAME_OWNER,\n    \"\",\n    False,\n    \"Same owner\",\n    {\"Feature\", \"Canvas\"},\n)\nparcels = QgsProject.instance().mapLayersByName(\"parcels\")[0]\nparcels.actions().addAction(select_same_owner)\n",[230,523,524,535,540,545,549,554,559,564,568,573,578,583,588,593,598,603,608,618,624,632,640,647,654,662,675,680,699],{"__ignoreMap":244},[248,525,526,529,532],{"class":111,"line":250},[248,527,528],{"class":293},"SELECT_SAME_OWNER",[248,530,531],{"class":253}," =",[248,533,534],{"class":286}," \"\"\"\n",[248,536,537],{"class":111,"line":267},[248,538,539],{"class":286},"from qgis.core import QgsProject, QgsFeatureRequest, QgsExpression\n",[248,541,542],{"class":111,"line":274},[248,543,544],{"class":286},"from qgis.utils import iface\n",[248,546,547],{"class":111,"line":299},[248,548,271],{"emptyLinePlaceholder":270},[248,550,551],{"class":111,"line":304},[248,552,553],{"class":286},"layer = QgsProject.instance().mapLayer('[% @layer_id %]')\n",[248,555,556],{"class":111,"line":315},[248,557,558],{"class":286},"feature = layer.getFeature([% $id %])\n",[248,560,561],{"class":111,"line":321},[248,562,563],{"class":286},"owner = feature['owner_ref']\n",[248,565,566],{"class":111,"line":330},[248,567,271],{"emptyLinePlaceholder":270},[248,569,570],{"class":111,"line":355},[248,571,572],{"class":286},"expr = f'\"owner_ref\" = {QgsExpression.quotedValue(owner)}'\n",[248,574,575],{"class":111,"line":363},[248,576,577],{"class":286},"ids = [f.id() for f in layer.getFeatures(\n",[248,579,580],{"class":111,"line":371},[248,581,582],{"class":286},"    QgsFeatureRequest().setFilterExpression(expr).setNoAttributes())]\n",[248,584,585],{"class":111,"line":379},[248,586,587],{"class":286},"layer.selectByIds(ids)\n",[248,589,590],{"class":111,"line":397},[248,591,592],{"class":286},"iface.mapCanvas().zoomToSelected(layer)\n",[248,594,595],{"class":111,"line":403},[248,596,597],{"class":286},"iface.messageBar().pushInfo('Same owner', f'{len(ids)} parcels selected')\n",[248,599,600],{"class":111,"line":409},[248,601,602],{"class":286},"\"\"\"\n",[248,604,606],{"class":111,"line":605},16,[248,607,271],{"emptyLinePlaceholder":270},[248,609,611,614,616],{"class":111,"line":610},17,[248,612,613],{"class":257},"select_same_owner ",[248,615,280],{"class":253},[248,617,312],{"class":257},[248,619,621],{"class":111,"line":620},18,[248,622,623],{"class":257},"    Qgis.AttributeActionType.GenericPython,\n",[248,625,627,630],{"class":111,"line":626},19,[248,628,629],{"class":286},"    \"Select all parcels with this owner\"",[248,631,327],{"class":257},[248,633,635,638],{"class":111,"line":634},20,[248,636,637],{"class":293},"    SELECT_SAME_OWNER",[248,639,327],{"class":257},[248,641,643,645],{"class":111,"line":642},21,[248,644,358],{"class":286},[248,646,327],{"class":257},[248,648,650,652],{"class":111,"line":649},22,[248,651,366],{"class":293},[248,653,327],{"class":257},[248,655,657,660],{"class":111,"line":656},23,[248,658,659],{"class":286},"    \"Same owner\"",[248,661,327],{"class":257},[248,663,665,667,669,671,673],{"class":111,"line":664},24,[248,666,382],{"class":257},[248,668,385],{"class":286},[248,670,388],{"class":257},[248,672,391],{"class":286},[248,674,394],{"class":257},[248,676,678],{"class":111,"line":677},25,[248,679,400],{"class":257},[248,681,683,686,688,690,693,695,697],{"class":111,"line":682},26,[248,684,685],{"class":257},"parcels ",[248,687,280],{"class":253},[248,689,283],{"class":257},[248,691,692],{"class":286},"\"parcels\"",[248,694,290],{"class":257},[248,696,51],{"class":293},[248,698,296],{"class":257},[248,700,702],{"class":111,"line":701},27,[248,703,704],{"class":257},"parcels.actions().addAction(select_same_owner)\n",[14,706,707,709,710,713,714,717,718,721,722,725,726,730,731,735],{},[207,708,420],{}," Only two placeholders are substituted: ",[230,711,712],{},"@layer_id",", an internal identifier, and ",[230,715,716],{},"$id",", an integer. Everything else is read from the feature inside the code, so an owner reference containing quotes, newlines or anything hostile stays a Python string. ",[230,719,720],{},"QgsExpression.quotedValue"," then quotes it safely for the filter expression. ",[230,723,724],{},"setNoAttributes()"," keeps the selection query fast on a large layer, as in ",[26,727,729],{"href":728},"\u002Fqgis-plugin-development\u002Fbackground-tasks-and-plugin-performance\u002Fspeed-up-feature-iteration-with-qgsfeaturerequest-pyqgis\u002F","speeding up feature iteration",". The message bar gives feedback without a modal dialog, the pattern from ",[26,732,734],{"href":733},"\u002Fqgis-plugin-development\u002Fqt-designer-for-gis-interfaces\u002Fshow-messages-with-qgsmessagebar-pyqgis\u002F","showing messages with QgsMessageBar",".",[196,737,739],{"id":738},"scopes-where-an-action-appears","Scopes: where an action appears",[14,741,742],{},"An action's scopes decide which user interfaces offer it. The same action can appear in several places, and a well-chosen set keeps menus short.",[14,744,745],{},[34,746,749,752,755,758,761,764,769,771,774,777,780,784,787,790,793,796,800,803,806,809,812,815,818,822,825,828],{"viewBox":747,"role":37,"ariaLabel":748,"xmlns":39},"0 0 760 250","The four action scopes and where each shows the action: Canvas in the Run Feature Action map tool, Feature in forms, identify results and the attribute table, Field on a single attribute cell, Layer in the attribute table toolbar without a feature",[41,750,751],{},"Four scopes, four places",[45,753,754],{},"Canvas scope shows the action in the Run Feature Action tool on the map. Feature scope shows it in feature forms, identify results and the per-row actions column of the attribute table. Field scope shows it on a right-click of a single attribute value and adds the field name and value as variables. Layer scope shows it in the attribute table toolbar and runs without any feature.",[49,756],{"x":51,"y":51,"width":52,"height":757,"fill":54},"250",[73,759,760],{"x":75,"y":458,"style":77,"fill":78,"textAnchor":79},"Choose scopes so each action shows up where it is needed",[49,762],{"x":462,"y":86,"width":148,"height":763,"rx":131,"fill":132,"stroke":133,"style":134},"182",[73,765,768],{"x":766,"y":767,"style":470,"fill":133,"textAnchor":79},"107","74","Canvas",[73,770,20],{"x":766,"y":474,"style":180,"fill":71,"textAnchor":79},[73,772,773],{"x":766,"y":138,"style":180,"fill":71,"textAnchor":79},"map tool",[73,775,776],{"x":766,"y":487,"style":149,"fill":88,"textAnchor":79},"click on the map",[49,778],{"x":779,"y":86,"width":148,"height":763,"rx":131,"fill":169,"stroke":170,"style":134},"206",[73,781,783],{"x":782,"y":767,"style":470,"fill":170,"textAnchor":79},"289","Feature",[73,785,786],{"x":782,"y":474,"style":180,"fill":71,"textAnchor":79},"forms, identify,",[73,788,789],{"x":782,"y":138,"style":180,"fill":71,"textAnchor":79},"table row column",[73,791,792],{"x":782,"y":487,"style":149,"fill":88,"textAnchor":79},"most common choice",[49,794],{"x":795,"y":86,"width":148,"height":763,"rx":131,"fill":185,"stroke":186,"style":134},"388",[73,797,799],{"x":798,"y":767,"style":470,"fill":186,"textAnchor":79},"471","Field",[73,801,802],{"x":798,"y":474,"style":180,"fill":71,"textAnchor":79},"right-click one",[73,804,805],{"x":798,"y":138,"style":180,"fill":71,"textAnchor":79},"attribute value",[73,807,808],{"x":798,"y":143,"style":144,"fill":71,"textAnchor":79},"@field_name",[73,810,811],{"x":798,"y":148,"style":144,"fill":71,"textAnchor":79},"@field_value",[73,813,814],{"x":798,"y":487,"style":149,"fill":88,"textAnchor":79},"generic per-value tools",[49,816],{"x":817,"y":86,"width":148,"height":763,"rx":131,"fill":87,"stroke":88,"style":134},"570",[73,819,821],{"x":820,"y":767,"style":470,"fill":78,"textAnchor":79},"653","Layer",[73,823,824],{"x":820,"y":474,"style":180,"fill":71,"textAnchor":79},"attribute table",[73,826,827],{"x":820,"y":138,"style":180,"fill":71,"textAnchor":79},"toolbar",[73,829,830],{"x":820,"y":487,"style":149,"fill":88,"textAnchor":79},"no feature involved",[239,832,834],{"className":241,"code":833,"language":243,"meta":244,"style":244},"copy_value = QgsAction(\n    Qgis.AttributeActionType.GenericPython,\n    \"Copy value to clipboard\",\n    \"from qgis.PyQt.QtWidgets import QApplication\\n\"\n    \"from qgis.core import QgsProject\\n\"\n    \"lyr = QgsProject.instance().mapLayer('[% @layer_id %]')\\n\"\n    \"value = lyr.getFeature([% $id %])[[% @field_index %]]\\n\"\n    \"QApplication.clipboard().setText('' if value is None else str(value))\",\n    \"\",\n    False,\n    \"Copy\",\n    {\"Field\"},\n)\n\nexport_selection = QgsAction(\n    Qgis.AttributeActionType.GenericPython,\n    \"Export selected features to the review folder\",\n    \"import processing\\n\"\n    \"from qgis.core import QgsProject, QgsProcessingFeatureSourceDefinition\\n\"\n    \"lyr = QgsProject.instance().mapLayer('[% @layer_id %]')\\n\"\n    \"processing.run('native:savefeatures', {\"\n    \"'INPUT': QgsProcessingFeatureSourceDefinition(lyr.id(), selectedFeaturesOnly=True),\"\n    \"'OUTPUT': '\u002Fsrv\u002Freview\u002F' + lyr.name() + '.gpkg'})\",\n    \"\",\n    False,\n    \"Export selection\",\n    {\"Layer\"},\n)\n\nfor action in (copy_value, export_selection):\n    assets.actions().addAction(action)\n\ntable_config = assets.attributeTableConfig()\ntable_config.setActionWidgetVisible(True)\ntable_config.setActionWidgetStyle(table_config.ButtonList)\nassets.setAttributeTableConfig(table_config)\n",[230,835,836,845,849,856,867,876,885,894,901,907,913,920,929,933,937,946,950,957,966,975,983,988,993,1000,1006,1012,1019,1028,1033,1038,1053,1059,1064,1075,1086,1092],{"__ignoreMap":244},[248,837,838,841,843],{"class":111,"line":250},[248,839,840],{"class":257},"copy_value ",[248,842,280],{"class":253},[248,844,312],{"class":257},[248,846,847],{"class":111,"line":267},[248,848,623],{"class":257},[248,850,851,854],{"class":111,"line":274},[248,852,853],{"class":286},"    \"Copy value to clipboard\"",[248,855,327],{"class":257},[248,857,858,861,864],{"class":111,"line":299},[248,859,860],{"class":286},"    \"from qgis.PyQt.QtWidgets import QApplication",[248,862,863],{"class":293},"\\n",[248,865,866],{"class":286},"\"\n",[248,868,869,872,874],{"class":111,"line":304},[248,870,871],{"class":286},"    \"from qgis.core import QgsProject",[248,873,863],{"class":293},[248,875,866],{"class":286},[248,877,878,881,883],{"class":111,"line":315},[248,879,880],{"class":286},"    \"lyr = QgsProject.instance().mapLayer('[% @layer_id %]')",[248,882,863],{"class":293},[248,884,866],{"class":286},[248,886,887,890,892],{"class":111,"line":321},[248,888,889],{"class":286},"    \"value = lyr.getFeature([% $id %])[[% @field_index %]]",[248,891,863],{"class":293},[248,893,866],{"class":286},[248,895,896,899],{"class":111,"line":330},[248,897,898],{"class":286},"    \"QApplication.clipboard().setText('' if value is None else str(value))\"",[248,900,327],{"class":257},[248,902,903,905],{"class":111,"line":355},[248,904,358],{"class":286},[248,906,327],{"class":257},[248,908,909,911],{"class":111,"line":363},[248,910,366],{"class":293},[248,912,327],{"class":257},[248,914,915,918],{"class":111,"line":371},[248,916,917],{"class":286},"    \"Copy\"",[248,919,327],{"class":257},[248,921,922,924,927],{"class":111,"line":379},[248,923,382],{"class":257},[248,925,926],{"class":286},"\"Field\"",[248,928,394],{"class":257},[248,930,931],{"class":111,"line":397},[248,932,400],{"class":257},[248,934,935],{"class":111,"line":403},[248,936,271],{"emptyLinePlaceholder":270},[248,938,939,942,944],{"class":111,"line":409},[248,940,941],{"class":257},"export_selection ",[248,943,280],{"class":253},[248,945,312],{"class":257},[248,947,948],{"class":111,"line":605},[248,949,623],{"class":257},[248,951,952,955],{"class":111,"line":610},[248,953,954],{"class":286},"    \"Export selected features to the review folder\"",[248,956,327],{"class":257},[248,958,959,962,964],{"class":111,"line":620},[248,960,961],{"class":286},"    \"import processing",[248,963,863],{"class":293},[248,965,866],{"class":286},[248,967,968,971,973],{"class":111,"line":626},[248,969,970],{"class":286},"    \"from qgis.core import QgsProject, QgsProcessingFeatureSourceDefinition",[248,972,863],{"class":293},[248,974,866],{"class":286},[248,976,977,979,981],{"class":111,"line":634},[248,978,880],{"class":286},[248,980,863],{"class":293},[248,982,866],{"class":286},[248,984,985],{"class":111,"line":642},[248,986,987],{"class":286},"    \"processing.run('native:savefeatures', {\"\n",[248,989,990],{"class":111,"line":649},[248,991,992],{"class":286},"    \"'INPUT': QgsProcessingFeatureSourceDefinition(lyr.id(), selectedFeaturesOnly=True),\"\n",[248,994,995,998],{"class":111,"line":656},[248,996,997],{"class":286},"    \"'OUTPUT': '\u002Fsrv\u002Freview\u002F' + lyr.name() + '.gpkg'})\"",[248,999,327],{"class":257},[248,1001,1002,1004],{"class":111,"line":664},[248,1003,358],{"class":286},[248,1005,327],{"class":257},[248,1007,1008,1010],{"class":111,"line":677},[248,1009,366],{"class":293},[248,1011,327],{"class":257},[248,1013,1014,1017],{"class":111,"line":682},[248,1015,1016],{"class":286},"    \"Export selection\"",[248,1018,327],{"class":257},[248,1020,1021,1023,1026],{"class":111,"line":701},[248,1022,382],{"class":257},[248,1024,1025],{"class":286},"\"Layer\"",[248,1027,394],{"class":257},[248,1029,1031],{"class":111,"line":1030},28,[248,1032,400],{"class":257},[248,1034,1036],{"class":111,"line":1035},29,[248,1037,271],{"emptyLinePlaceholder":270},[248,1039,1041,1044,1047,1050],{"class":111,"line":1040},30,[248,1042,1043],{"class":253},"for",[248,1045,1046],{"class":257}," action ",[248,1048,1049],{"class":253},"in",[248,1051,1052],{"class":257}," (copy_value, export_selection):\n",[248,1054,1056],{"class":111,"line":1055},31,[248,1057,1058],{"class":257},"    assets.actions().addAction(action)\n",[248,1060,1062],{"class":111,"line":1061},32,[248,1063,271],{"emptyLinePlaceholder":270},[248,1065,1067,1070,1072],{"class":111,"line":1066},33,[248,1068,1069],{"class":257},"table_config ",[248,1071,280],{"class":253},[248,1073,1074],{"class":257}," assets.attributeTableConfig()\n",[248,1076,1078,1081,1084],{"class":111,"line":1077},34,[248,1079,1080],{"class":257},"table_config.setActionWidgetVisible(",[248,1082,1083],{"class":293},"True",[248,1085,400],{"class":257},[248,1087,1089],{"class":111,"line":1088},35,[248,1090,1091],{"class":257},"table_config.setActionWidgetStyle(table_config.ButtonList)\n",[248,1093,1095],{"class":111,"line":1094},36,[248,1096,1097],{"class":257},"assets.setAttributeTableConfig(table_config)\n",[14,1099,1100,1102,1103,1105,1106,388,1108,233,1110,1113,1114,1116,1117,1119],{},[207,1101,420],{}," The ",[18,1104,799],{}," scope adds ",[230,1107,808],{},[230,1109,811],{},[230,1111,1112],{},"@field_index"," to the expression context, so one action serves every column. It would be tempting to substitute ",[230,1115,811],{}," straight into the code, but a placeholder is replaced by the bare text of the value, not a quoted Python literal — the same trap as before. Substituting the integer field index and the feature id, then reading the value inside the code, copies any value correctly, including ones with quotes and line breaks. The ",[18,1118,821],{}," scope runs without a feature and suits bulk operations on the current selection. Making the action widget visible in the attribute table config adds a column of buttons, one per feature-scoped action, which is the most discoverable place for them.",[196,1121,1123],{"id":1122},"run-actions-from-a-script","Run actions from a script",[14,1125,1126],{},"Actions are not only for clicking. A script can list a layer's actions and run one against a feature with a proper expression context — useful for testing an action, or for reusing logic already maintained as an action.",[239,1128,1130],{"className":241,"code":1129,"language":243,"meta":244,"style":244},"from qgis.core import QgsExpressionContext, QgsExpressionContextUtils\n\nlayer = parcels\nmanager = layer.actions()\nby_name = {a.name(): a for a in manager.actions()}\naction = by_name[\"Select all parcels with this owner\"]\n\nfeature = next(layer.getFeatures())\ncontext = QgsExpressionContext()\ncontext.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(layer))\ncontext.setFeature(feature)\n\nmanager.doAction(action.id(), feature, context)\n\nfor a in manager.actions():\n    print(f\"{a.shortTitle() or a.name():\u003C20} {a.type()} scopes={sorted(a.actionScopes())}\")\n",[230,1131,1132,1143,1147,1157,1167,1187,1202,1206,1219,1229,1234,1239,1243,1248,1252,1263],{"__ignoreMap":244},[248,1133,1134,1136,1138,1140],{"class":111,"line":250},[248,1135,254],{"class":253},[248,1137,258],{"class":257},[248,1139,261],{"class":253},[248,1141,1142],{"class":257}," QgsExpressionContext, QgsExpressionContextUtils\n",[248,1144,1145],{"class":111,"line":267},[248,1146,271],{"emptyLinePlaceholder":270},[248,1148,1149,1152,1154],{"class":111,"line":274},[248,1150,1151],{"class":257},"layer ",[248,1153,280],{"class":253},[248,1155,1156],{"class":257}," parcels\n",[248,1158,1159,1162,1164],{"class":111,"line":299},[248,1160,1161],{"class":257},"manager ",[248,1163,280],{"class":253},[248,1165,1166],{"class":257}," layer.actions()\n",[248,1168,1169,1172,1174,1177,1179,1182,1184],{"class":111,"line":304},[248,1170,1171],{"class":257},"by_name ",[248,1173,280],{"class":253},[248,1175,1176],{"class":257}," {a.name(): a ",[248,1178,1043],{"class":253},[248,1180,1181],{"class":257}," a ",[248,1183,1049],{"class":253},[248,1185,1186],{"class":257}," manager.actions()}\n",[248,1188,1189,1192,1194,1197,1200],{"class":111,"line":315},[248,1190,1191],{"class":257},"action ",[248,1193,280],{"class":253},[248,1195,1196],{"class":257}," by_name[",[248,1198,1199],{"class":286},"\"Select all parcels with this owner\"",[248,1201,296],{"class":257},[248,1203,1204],{"class":111,"line":321},[248,1205,271],{"emptyLinePlaceholder":270},[248,1207,1208,1211,1213,1216],{"class":111,"line":330},[248,1209,1210],{"class":257},"feature ",[248,1212,280],{"class":253},[248,1214,1215],{"class":293}," next",[248,1217,1218],{"class":257},"(layer.getFeatures())\n",[248,1220,1221,1224,1226],{"class":111,"line":355},[248,1222,1223],{"class":257},"context ",[248,1225,280],{"class":253},[248,1227,1228],{"class":257}," QgsExpressionContext()\n",[248,1230,1231],{"class":111,"line":363},[248,1232,1233],{"class":257},"context.appendScopes(QgsExpressionContextUtils.globalProjectLayerScopes(layer))\n",[248,1235,1236],{"class":111,"line":371},[248,1237,1238],{"class":257},"context.setFeature(feature)\n",[248,1240,1241],{"class":111,"line":379},[248,1242,271],{"emptyLinePlaceholder":270},[248,1244,1245],{"class":111,"line":397},[248,1246,1247],{"class":257},"manager.doAction(action.id(), feature, context)\n",[248,1249,1250],{"class":111,"line":403},[248,1251,271],{"emptyLinePlaceholder":270},[248,1253,1254,1256,1258,1260],{"class":111,"line":409},[248,1255,1043],{"class":253},[248,1257,1181],{"class":257},[248,1259,1049],{"class":253},[248,1261,1262],{"class":257}," manager.actions():\n",[248,1264,1265,1268,1271,1274,1277,1280,1283,1286,1289,1292,1295,1298,1301,1303,1306,1309,1312,1314,1316],{"class":111,"line":605},[248,1266,1267],{"class":293},"    print",[248,1269,1270],{"class":257},"(",[248,1272,1273],{"class":253},"f",[248,1275,1276],{"class":286},"\"",[248,1278,1279],{"class":293},"{",[248,1281,1282],{"class":257},"a.shortTitle() ",[248,1284,1285],{"class":253},"or",[248,1287,1288],{"class":257}," a.name()",[248,1290,1291],{"class":253},":\u003C20",[248,1293,1294],{"class":293},"}",[248,1296,1297],{"class":293}," {",[248,1299,1300],{"class":257},"a.type()",[248,1302,1294],{"class":293},[248,1304,1305],{"class":286}," scopes=",[248,1307,1308],{"class":293},"{sorted",[248,1310,1311],{"class":257},"(a.actionScopes())",[248,1313,1294],{"class":293},[248,1315,1276],{"class":286},[248,1317,400],{"class":257},[14,1319,1320,1322,1323,1326,1327,1329,1330,1333],{},[207,1321,420],{}," ",[230,1324,1325],{},"globalProjectLayerScopes"," supplies the same variables the user interface provides — project and layer variables, including ",[230,1328,712],{}," — so placeholders expand identically. ",[230,1331,1332],{},"doAction"," evaluates the placeholders and runs the action synchronously; a Python action that shows a message bar needs QGIS Desktop, so guard such actions if the script might run headless. Listing actions with their scopes is also a quick audit of what a shared project will let users run.",[196,1335,1337],{"id":1336},"replace-actions-idempotently-and-ship-them-with-the-data","Replace actions idempotently and ship them with the data",[14,1339,1340,1341,1344],{},"A script that adds actions will be run again — after a fix to the Python, after a URL changes — and ",[230,1342,1343],{},"addAction"," does not check for duplicates. Remove any previous version by name first, then save the actions into the data source so users who add the layer from the file get them too.",[239,1346,1348],{"className":241,"code":1347,"language":243,"meta":244,"style":244},"from qgis.core import QgsMapLayer\n\ndef install_actions(layer, actions):\n    manager = layer.actions()\n    wanted = {a.name() for a in actions}\n    for existing in manager.actions():\n        if existing.name() in wanted:\n            manager.removeAction(existing.id())\n    for action in actions:\n        manager.addAction(action)\n\n    error = layer.saveStyleToDatabase(\n        \"actions\", \"layer actions\", False, \"\",\n        QgsMapLayer.StyleCategory.Actions,\n    )\n    return error\n\nerr = install_actions(parcels, [select_same_owner, export_selection])\nprint(\"saved\" if not err else err)\n",[230,1349,1350,1361,1365,1377,1386,1405,1417,1430,1435,1446,1451,1455,1465,1487,1492,1497,1505,1509,1519],{"__ignoreMap":244},[248,1351,1352,1354,1356,1358],{"class":111,"line":250},[248,1353,254],{"class":253},[248,1355,258],{"class":257},[248,1357,261],{"class":253},[248,1359,1360],{"class":257}," QgsMapLayer\n",[248,1362,1363],{"class":111,"line":267},[248,1364,271],{"emptyLinePlaceholder":270},[248,1366,1367,1370,1374],{"class":111,"line":274},[248,1368,1369],{"class":253},"def",[248,1371,1373],{"class":1372},"svObZ"," install_actions",[248,1375,1376],{"class":257},"(layer, actions):\n",[248,1378,1379,1382,1384],{"class":111,"line":299},[248,1380,1381],{"class":257},"    manager ",[248,1383,280],{"class":253},[248,1385,1166],{"class":257},[248,1387,1388,1391,1393,1396,1398,1400,1402],{"class":111,"line":304},[248,1389,1390],{"class":257},"    wanted ",[248,1392,280],{"class":253},[248,1394,1395],{"class":257}," {a.name() ",[248,1397,1043],{"class":253},[248,1399,1181],{"class":257},[248,1401,1049],{"class":253},[248,1403,1404],{"class":257}," actions}\n",[248,1406,1407,1410,1413,1415],{"class":111,"line":315},[248,1408,1409],{"class":253},"    for",[248,1411,1412],{"class":257}," existing ",[248,1414,1049],{"class":253},[248,1416,1262],{"class":257},[248,1418,1419,1422,1425,1427],{"class":111,"line":321},[248,1420,1421],{"class":253},"        if",[248,1423,1424],{"class":257}," existing.name() ",[248,1426,1049],{"class":253},[248,1428,1429],{"class":257}," wanted:\n",[248,1431,1432],{"class":111,"line":330},[248,1433,1434],{"class":257},"            manager.removeAction(existing.id())\n",[248,1436,1437,1439,1441,1443],{"class":111,"line":355},[248,1438,1409],{"class":253},[248,1440,1046],{"class":257},[248,1442,1049],{"class":253},[248,1444,1445],{"class":257}," actions:\n",[248,1447,1448],{"class":111,"line":363},[248,1449,1450],{"class":257},"        manager.addAction(action)\n",[248,1452,1453],{"class":111,"line":371},[248,1454,271],{"emptyLinePlaceholder":270},[248,1456,1457,1460,1462],{"class":111,"line":379},[248,1458,1459],{"class":257},"    error ",[248,1461,280],{"class":253},[248,1463,1464],{"class":257}," layer.saveStyleToDatabase(\n",[248,1466,1467,1470,1472,1475,1477,1480,1482,1485],{"class":111,"line":397},[248,1468,1469],{"class":286},"        \"actions\"",[248,1471,388],{"class":257},[248,1473,1474],{"class":286},"\"layer actions\"",[248,1476,388],{"class":257},[248,1478,1479],{"class":293},"False",[248,1481,388],{"class":257},[248,1483,1484],{"class":286},"\"\"",[248,1486,327],{"class":257},[248,1488,1489],{"class":111,"line":403},[248,1490,1491],{"class":257},"        QgsMapLayer.StyleCategory.Actions,\n",[248,1493,1494],{"class":111,"line":409},[248,1495,1496],{"class":257},"    )\n",[248,1498,1499,1502],{"class":111,"line":605},[248,1500,1501],{"class":253},"    return",[248,1503,1504],{"class":257}," error\n",[248,1506,1507],{"class":111,"line":610},[248,1508,271],{"emptyLinePlaceholder":270},[248,1510,1511,1514,1516],{"class":111,"line":620},[248,1512,1513],{"class":257},"err ",[248,1515,280],{"class":253},[248,1517,1518],{"class":257}," install_actions(parcels, [select_same_owner, export_selection])\n",[248,1520,1521,1523,1525,1528,1531,1534,1537,1540],{"class":111,"line":626},[248,1522,412],{"class":293},[248,1524,1270],{"class":257},[248,1526,1527],{"class":286},"\"saved\"",[248,1529,1530],{"class":253}," if",[248,1532,1533],{"class":253}," not",[248,1535,1536],{"class":257}," err ",[248,1538,1539],{"class":253},"else",[248,1541,1542],{"class":257}," err)\n",[14,1544,1545,1547,1548,1551,1552,1555,1556,1559,1560,1564],{},[207,1546,420],{}," Matching on the action name makes the install repeatable: the second run replaces rather than duplicates, and actions added by hand under other names are left alone. Saving with only the ",[230,1549,1550],{},"Actions"," category, and not as the default style, stores the actions alongside any existing default style instead of replacing it; users can load them from the layer's style menu, or a plugin can apply them on load. Where the actions are part of the layer's standard configuration, save them into the default style together with the ",[230,1553,1554],{},"Forms"," category so a single style carries both. Keeping ",[230,1557,1558],{},"install_actions"," in a module next to the scripts that build ",[26,1561,1563],{"href":1562},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fbuild-tabbed-attribute-form-layout-pyqgis\u002F","forms"," means one command configures a layer completely.",[196,1566,1568],{"id":1567},"qgis-version-compatibility","QGIS version compatibility",[14,1570,1571,1574,1575,233,1578,1581,1582,1585,1586,1589,1590,1593],{},[230,1572,1573],{},"Qgis.AttributeActionType"," is the 3.36+ spelling; earlier releases use ",[230,1576,1577],{},"QgsAction.GenericPython",[230,1579,1580],{},"QgsAction.OpenUrl",", which the QGIS 4 series no longer accepts. The seven-argument ",[230,1583,1584],{},"QgsAction"," constructor with scopes has been available since 3.0. ",[230,1587,1588],{},"setActionWidgetStyle"," takes ",[230,1591,1592],{},"QgsAttributeTableConfig.ActionWidgetStyle.ButtonList"," in its scoped form on QGIS 4. Mobile apps do not run Python actions; URL actions generally work.",[196,1595,1597],{"id":1596},"troubleshooting","Troubleshooting",[201,1599,1600,1612,1622,1628,1637],{},[204,1601,1602,1605,1606,1608,1609,1611],{},[207,1603,1604],{},"The action does not appear on the map."," It lacks the ",[230,1607,768],{}," scope, or the ",[18,1610,20],{}," tool is not selected.",[204,1613,1614,1617,1618,1621],{},[207,1615,1616],{},"Placeholders appear literally in the URL."," The expression inside ",[230,1619,1620],{},"[% %]"," has a syntax error; test it in the expression builder.",[204,1623,1624,1627],{},[207,1625,1626],{},"A Python action fails for some features only."," A substituted text value contains a quote; substitute the feature id instead.",[204,1629,1630,1633,1634,1636],{},[207,1631,1632],{},"Actions disappear when the layer is loaded in another project."," Save the layer style with the ",[230,1635,1550],{}," category to the data source.",[204,1638,1639,1642,1643,1646],{},[207,1640,1641],{},"Nothing happens in QGIS Server or a script."," Python actions that use ",[230,1644,1645],{},"iface"," need QGIS Desktop.",[196,1648,1650],{"id":1649},"conclusion","Conclusion",[14,1652,1653],{},"Use URL actions to link features to other systems and Python actions for anything that must act inside QGIS. Substitute only identifiers into Python action text and read everything else inside the code. Choose scopes deliberately, show feature actions as buttons in the attribute table, and save actions with the layer's style so they follow the data.",[196,1655,1657],{"id":1656},"frequently-asked-questions","Frequently Asked Questions",[14,1659,1660,1663,1664,1667],{},[207,1661,1662],{},"Can an action ask the user for input?","\nYes, a Python action can open a ",[230,1665,1666],{},"QInputDialog",". Keep the prompt short, since actions are meant to be quick.",[14,1669,1670,1673,1674,1677],{},[207,1671,1672],{},"Can I trigger an action automatically when a feature is added?","\nNo — actions are user-invoked. React to ",[230,1675,1676],{},"featureAdded"," on the layer from a plugin or form code instead.",[14,1679,1680,1683],{},[207,1681,1682],{},"How is an action different from a plugin?","\nAn action lives with one layer and needs nothing installed; a plugin lives with the user's QGIS and can serve every layer. Graduate an action to a plugin once several layers or projects need it.",[14,1685,1686,1689],{},[207,1687,1688],{},"Is it safe to open projects containing actions from others?","\nPython actions only run when a user triggers them, but they can do anything once triggered. Review actions in untrusted projects before running them.",[196,1691,1693],{"id":1692},"related","Related",[201,1695,1696,1701,1707,1713,1719],{},[204,1697,1698,1700],{},[26,1699,29],{"href":28}," — the guide this recipe belongs to",[204,1702,1703],{},[26,1704,1706],{"href":1705},"\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fadd-python-logic-to-attribute-form-pyqgis\u002F","Add Python Logic to an Attribute Form in PyQGIS",[204,1708,1709],{},[26,1710,1712],{"href":1711},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fadd-layer-context-menu-action-pyqgis\u002F","Add a Layer Context Menu Action in PyQGIS",[204,1714,1715],{},[26,1716,1718],{"href":1717},"\u002Fqgis-plugin-development\u002Fcustom-map-tools-and-canvas-interaction\u002Fidentify-feature-under-cursor-pyqgis\u002F","Identify the Feature Under the Cursor in PyQGIS",[204,1720,1721],{},[26,1722,1724],{"href":1723},"\u002Fpyqgis-fundamentals-environment-setup\u002Fworking-with-qgis-expressions\u002Fselect-features-by-expression-pyqgis\u002F","Select Features by Expression in PyQGIS",[1726,1727,1728],"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}",{"title":244,"searchDepth":267,"depth":267,"links":1730},[1731,1732,1733,1734,1735,1736,1737,1738,1739,1740,1741],{"id":198,"depth":267,"text":199},{"id":224,"depth":267,"text":225},{"id":436,"depth":267,"text":437},{"id":738,"depth":267,"text":739},{"id":1122,"depth":267,"text":1123},{"id":1336,"depth":267,"text":1337},{"id":1567,"depth":267,"text":1568},{"id":1596,"depth":267,"text":1597},{"id":1649,"depth":267,"text":1650},{"id":1656,"depth":267,"text":1657},{"id":1692,"depth":267,"text":1693},"Add feature actions to a layer from Python with QgsAction — open a URL built from attributes, run Python against the clicked feature, choose scopes for the canvas, attribute table and forms, run actions from scripts, and keep them safe to share.","md",{"slug":1745,"type":1746,"breadcrumb":1747,"datePublished":1748,"dateModified":1748},"create-layer-actions-pyqgis","article","Create Layer Actions","2026-09-17","\u002Fqgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fcreate-layer-actions-pyqgis",{"title":5,"description":1742},"qgis-plugin-development\u002Fattribute-forms-and-layer-actions\u002Fcreate-layer-actions-pyqgis\u002Findex","APRiCdQDJ1oIDXizkD8rLmpA8hvvLsyA5XfilmvKGkM",1789632907433]