I´m creating a Qfield project in QGIS and I´m creating a Point-shapefile to work with in offline mode in the field.

In my project there are different plots where the field work will be developed. On each plot, trees will be sampled and registered as points (offline shapefile). When a point is created within one of these plots, the attribute value of the name of this plot will be transfered to a column of this point. Hence, it will be known in which plot the point was created. The name of the Plot is, for example, "P1".

When a point (tree) is created within a plot, the ID of this point is constructed as: Plot_ID + Tree_ID. Hence, it will be known where the tree was created and the ID of this tree within this plot. For example, the first tree recordered in the Plot 1, will be written as "P1B1". Then, the second will be written as "P1B2", and so on.


So far I´ve managed to set up the column of Tree_ID so that its ID number increases. This is, when several trees are created in the same plot, the values are created as expected: "P1B1"; "P1B2"; "P1B3". However, when I try to create another tree in a different plot, the plot is not differentiated, and the ID number continues increasing, rather than starting from 1 again. For example, if I create another tree in P2, the name will be wrongly created as "P2B4". This tree should be created as "P2B1", since this would be the first tree created within this plot.



After trying different approaches, the approach that gave me the result I was looking for is the following, where Point_trees is the points-shp that are being created, and plots_layer is the polygon-shp containing the plots where the field work will be made:

count('Point_trees', group_by:="Plot_ID", filter:= "Plot_ID"= aggregate(layer:='plots_layer', aggregate:='max', expression:="Name_of_plot", filter:=intersects($geometry, geometry(@parent))))+1

This code counts the number of features which Plot_ID is equal to the actual Plot_ID of the point which is being created (through the intersection part after filter:=)

2 Answers 2


Does each tree feature include separate attributes for plotid and treeid. If so to return next number for plot

Coalesce(Maximimum(treeid, filter:=plotid=plotid)+1,0)+1

If treeid contains plotid, then you need to use string functions to extract the number part and perform maximum on this.

I suggest it makes sense to have plotid as an attribute of trees, it can then serve as a foreign key for a relation. I'd also use a separate attributes for the number part.


So I've created a small test project with layers 'tree' and 'plot'.

'plot' has attribute 'plotid' 'tree' has attributes 'plotid', 'treeid'

I tried the following code and it worked in preview but failed when using the form in QGIS. I then changed the data type of 'treeid' from integer' to 'text' and it works. I have not tested in QField.

In 'tree'.'plotid' I have set default value to


In 'tree'.'treeid' I have set default value to

maximum("treeid", "PlotID")+1

In the above expression "PlotID" is the Group By argument of maximum(). If the expression for 'tree'.'plotid' fails, try replacing the layer name with the full unique reference generated in QGIS expressions dialog.


  • Yes, I have sepparate attributes for plotid and treeid. More specifically, I have one for the plotid, a second one which creates only the number of the tree on each plot, and a third one which concatenates the plotid, a letter 'B', and the number of the tree. This could be seen as follows: [Plotid] = P1; [ID] = 1; [Treeid] = P1B1.
    – Astro
    Dec 15, 2022 at 12:53
  • Then does my expression work? Which would now be Coalesce(Maximimum(id, filter:=plotid=plotid)+1,0)+1 Coalesce() is used to cater for the case where there is no existing tree.
    – Oisin
    Dec 15, 2022 at 13:45
  • Unfortunately not, it gives "1" as value everytime. So 1 for the first tree, 1 for the second tree, and so on. I've tried also with this expression: aggregate(layer:='points', aggregate:='count', expression:="Plot_ID", filter:= Plot_ID=Plot_ID)+1. In this case it counts as normal (1,2,3,4...) but there is no distinction between different plots.
    – Astro
    Dec 15, 2022 at 14:26
  • See amended answer...
    – Oisin
    Dec 15, 2022 at 16:54
  • This also works for solving the problem. See also the EDITED part of my post for the approach I used for solving it.
    – Astro
    Dec 16, 2022 at 18:19

Assuming you have the plot geometry as a polygon layer, you can set an expression for the default value of the treeid field. The treeid is generated by getting the plot_id of the plot that the tree point falls within, and combining it with how many tree points already exist inside that plot +1.

    overlay_within('plots', "plot_id")[0],      -- get plot id
        num_geometries(                         -- count how many points exist in the current plot
                geometry(get_feature('plots', 'plot_id', @plot))
        -- construct the tree id by concatenating the elements
        -- set counter to 1 if no points are already in the plot
        @plot || 'B' || case when @num_trees_in_plot is null then 1 else @num_trees_in_plot + 1 end

enter image description here

The default value is set via the layer properties > Attributes Form:

enter image description here

  • This is an expression, no Python involved.
    – Matt
    Dec 15, 2022 at 17:36
  • '@Matt. Apologies for my error.
    – Oisin
    Dec 15, 2022 at 17:39
  • No worries :) the indentation makes it look Pythony. I like to format it like that to keep an overview of what's going on
    – Matt
    Dec 15, 2022 at 17:45
  • Also see my answer, which only goes so far as generating the next sequence number within a plot, but is much less verbose. I hadn't realised this was how the Group By clause worked, I had been trying filter expressions to no avail. Also, intersting it works for a text field but not an integer - the result was an integer, not a string.
    – Oisin
    Dec 15, 2022 at 17:48
  • In my case I wouldn't need to plot it, but I think this approach would also solve the problem.
    – Astro
    Dec 15, 2022 at 18:54

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge that you have read and understand our privacy policy and code of conduct.

Not the answer you're looking for? Browse other questions tagged or ask your own question.