Query Result Property
What It Is
A Query result property (pQueryResult, in the Special property family) turns a saved query into a live field on a record. Instead of storing a value a user types, it stores a reference to a Query and, at display time, runs that query in the context of the record it lives on. The output can be a count, a list of matched record IDs, a list of titles, or a list of clickable links — and the mask decides which.
Use it when a record should show something about other records that relate to it: "how many orders does this customer have", "list the open tasks for this project", "sum of invoice lines on this order".
Why It Matters
Without this property you would model the same information as a calculation, an automation that writes a counter, or a custom canvas. Those all duplicate logic and can drift out of date. A Query result property has three advantages:
- It is always current, because it runs the query when the value is read.
- It reuses an existing query, so the selection logic lives in one place.
- It can be counted, listed, or linked from the same definition just by changing the render mode in the
mask.
It is the lightweight way to surface related data on a record without building a view or a canvas.
Mental Model
Keep three things separate:
- The bound query — which records are matched. This is the property's value: a reference to a Query.
- The context — the query runs scoped to the record the property is on. Inside the query's filters,
{$this}refers to that owning record, so a filter like{$this.id}matches "rows that point back to me". This is what makes "count my related records" work. - The render mode — how the matched records are turned into the displayed value. This is controlled entirely by the
mask.
The first two are query design (see Queries and Query Parameters). The third — the mask — is what the rest of this page is about.
Binding The Query
The property's stored value is the query to run. You have two ways to set it:
- Fixed for every record — set
default_valueto the target query. Every record runs the same query. This is the common case. - Chosen per record — leave the value editable so users pick which query a given record should run. The field renders as a select of available queries.
If no value and no default_value resolve to a query, the property renders nothing.
The Mask: Render Modes And Formatting
For most property types mask is just a display-formatting expression. For a Query result property the mask does double duty: its first token selects a render mode, and anything after it is an optional formatting expression. This is the part that is easy to get wrong, so the rules below are exact.
Mask Grammar
[render:MODE] [| formatting-expression]
render:MODEis an optional keyword that must be at the very start of the mask.- A pipe
|separates the keyword from an optional formatting expression. - A mask with no
render:keyword (including an empty mask) is treated as count mode, and the whole mask is used as the formatting expression.
The Four Render Modes
| Keyword | What it produces (no formatting expression) | Iterates |
|---|---|---|
render:count (default) | A single number: the count of matched records, or the query's calculation result (see below). | once |
render:raw | The id of each matched record. | per record |
render:plain | The title of each matched record, as plain text. | per record |
render:full | The title of each matched record, as a clickable link to that record. | per record |
If you omit the keyword entirely, you get render:count.
Rule 1 — The keyword must come first
The render mode is recognized only at the start of the mask string. render:full works; count of {$name}: render:full does not — the keyword in the middle is ignored and the whole string is treated as a count-mode formatting expression. Only the four keywords above are recognized.
Rule 2 — self is the value being formatted
Inside the formatting expression, self is bound to the value this property is currently producing:
- In count mode,
selfis the number (count or calculation result). - In raw / plain / full mode,
selfis the matched record, and the expression runs once per record.
Everything else in the expression resolves against the owning record (the record the property lives on), because that record is in scope. So use self to reach the matched record or count, and {$...} / {{...}} to reach the owning record's own properties.
Rule 3 — A formatting expression replaces the mode's default output
This is the subtle one. The built-in output of each mode (the number, the ID, the title, the link) is used only when you do not supply a formatting expression. The moment you add | expression, your expression's result becomes the output and the built-in rendering is discarded.
A practical consequence: once you add an expression, render:raw, render:plain, and render:full behave identically — they all just run your expression once per matched record with self set to that record. Pick whichever reads clearly; the difference between them only matters when the mask is just the keyword with no expression. If you want a link, build it in the expression yourself (the matched record is available as self).
Rule 4 — self is tag-stripped
The value bound to self has its HTML tags stripped and escaped before the expression runs. You cannot smuggle markup in through self. If you need HTML output (a badge, a link, an icon), build it in the expression body, not via self.
Rule 5 — Multiple records are joined
In raw / plain / full mode the per-record results are concatenated using the property's multiple_separator. If that is empty, the tenant's default item separator is used. Count mode produces a single value and is not joined.
Rule 6 — Count mode respects the query's calculation
If the bound query defines a calculation (for example a sum or a distinct count), render:count returns that calculation's result, not a plain row count. If the query has no calculation, you get the number of matched records. This lets the same property show "number of invoice lines" or "total amount" purely by how the query is built.
Examples
All examples assume the bound query selects records related to the owning record (for example, orders whose customer is {$this.id}).
Plain count — leave the mask empty, or:
render:count
Count with a label — count mode, formatting expression applied to the number:
{= self } open item(s)
(No keyword needed — bare expressions are count mode. self is the number.)
Count, but show the query's total instead — give the bound query a sum calculation and keep:
render:count
List titles — comma-separated record titles (set multiple_separator to , ):
render:plain
List as links — clickable links to each matched record:
render:full
Custom per-record output — list a different property of each matched record:
render:plain|{$self.invoice_number}
Here self is the matched record, so {$self.invoice_number} reads that record's field. The mode keyword could be raw, plain, or full interchangeably because the expression supplies the output.
Performance Note
Count mode over the common "count my related records" pattern is optimized: when a list page shows many records each carrying the same count-style Query result property, the platform computes those counts in one grouped query for the whole page rather than running the full query per row. The list-friendly path covers render:count (and the empty/default mask). The raw, plain, and full modes, and count masks with formatting expressions, evaluate per record. Prefer render:count for columns shown across large lists, and reserve the per-record modes for detail views or smaller result sets.
Exports (for example XLSX) always render the matched records as plain text titles, regardless of the configured mode.
Best Practices
- Build the relationship logic in the query, not the mask. The mask should only decide presentation.
- Use
render:countfor list columns and dashboards; userender:fullon detail views where users will want to navigate to the related records. - Keep formatting expressions short. If a mask expression grows complex, reconsider whether a dedicated view or canvas is the better surface.
- Remember that a formatting expression overrides the mode's default rendering — do not expect
render:full|...to still produce a link unless your expression builds one. - Set
multiple_separatordeliberately for the per-record modes so the joined output reads well on the target surface.