Query Tool directives
Overview
Query directives are an alternative to the .execute() method on the Query Tool, introduced in Cascade CMS 8.26. They process results one asset at a time instead of loading everything into a list, so they can handle up to 100,000 results against a limit of 2,000 on .execute().
All three directives follow the same pattern: pass in a query object and an asset variable, write your logic in the body, and close with #end. To check which directives your environment has available, output $enabledCustomDirectives.
Directive arguments
All three directives take the same first two arguments. Only the body differs.
| Argument | Type | Description |
|---|---|---|
| query | SearchQuery required |
A query object. |
| asset | Object required |
Populated automatically. Points to a new asset on each iteration, and the previous asset is cleared to save memory. |
| body | Logic required |
Your logic. What it needs to return depends on the directive, covered in each section below. |
When to use directives
| Scenario | Use |
|---|---|
| Results under 2,000 with no custom filtering or sorting | .execute() |
| Need more than 2,000 results | #queryexecute |
| Need to filter on values not available via built-in query methods | #queryfilter |
| Need to sort by structured data, dynamic metadata, or computed values | #querysortvalue |
| Under 2,000 results but need custom filtering or sorting | #queryfilter / #querysortvalue + .execute() |
| Large dataset with custom filter + sort + output | All three combined |
Built-in query methods are faster than directives. Reach for a directive only when no built-in method covers what you need: .bySiteName() outperforms #queryfilter with equivalent site-name logic, and .sortBy("name") outperforms #querysortvalue with $asset.name.
#queryexecute
Use #queryexecute in place of the .execute() + #foreach pattern. It iterates over results directly, one asset at a time, and supports maxResults() up to 100,000.
The body runs once per asset, and whatever it outputs becomes that asset's output.
Basic usage
Build a query, then pass it to #queryexecute instead of calling .execute().
## Build the query (do NOT call .execute())
#set ($query = $_.query().byContentType("Article").maxResults(10).sortBy("startDate").sortDirection("desc"))
## Iterate with the directive
#queryexecute($query, $page)
<h3>$page.metadata.title</h3>
<p>$page.metadata.summary</p>
#end
Important
Unlike #foreach, this directive clears the previous asset from memory on each iteration. This means $foreach.count, $foreach.hasNext, $foreach.index, and other loop variables are not available. If you need a counter or first-iteration check, manage it yourself with a #set variable.
Generate a sitemap
Sitemaps often produce very large result sets, which is exactly what directives handle.
#set ($query = $_.query().byContentType("Default-Page").indexableOnly(true).preloadDynamicMetadata())
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
#queryexecute($query, $page)
<url>
<loc>https://yoursite.com${page.path}.html</loc>
</url>
#end
</urlset>
Preload data for large result sets
When working with structured data across many results, preload it on the query to avoid per-asset database round trips.
#set ($query = $_.query().byContentType("Event").preloadStructuredData().maxResults(5000).sortBy("startDate").sortDirection("asc"))
#queryexecute($query, $event)
#set ($start = $event.getStructuredDataNode("startDateTime").textValue)
#set ($title = $event.metadata.title)
<div class="event-card">
<h3>$title</h3>
<span>$start</span>
</div>
#end
Query across sites
Query across all sites for assets using a shared Content Type.
#set ($query = $_.query().byContentType("site://Global/Shared Page").searchAcrossAllSites().preloadDynamicMetadata().maxResults(10000))
#queryexecute($query, $page)
<li>
<a href="${page.link}">${page.metadata.title}</a>
<small>(${page.siteName})</small>
</li>
#end
Output a JSON feed
Since loop variables aren't available, use a flag variable like $isFirstIteration for comma separation in JSON output.
#set($query = $_.query().byContentType("Event").preloadStructuredData().sortBy("startDate"))
#set($isFirstIteration = true)
[
#queryexecute($query, $event)
#if(!$isFirstIteration),#end
#set($title = $_EscapeTool.java($event.metadata.title.trim()))
#set($date = $event.getStructuredDataNode("event-date").textValue)
{
"title": "${title}",
"date": "${date}",
"url": ""
}
#set($isFirstIteration = false)
#end
]
#if(!$isFirstIteration),#end outputs a comma before every entry except the first, replacing what you'd normally do with $foreach.hasNext.
#queryfilter
Use #queryfilter to test each asset matching a query before maxResults() is applied. Return the exact string true to include an asset in the results.
Filter by path pattern
Only include index pages from the query results.
#set ($query = $_.query().byContentType("Article"))
#queryfilter($query, $asset)
$asset.path.contains("index")
#end
#queryexecute($query, $page)
<li>${page.metadata.title}</li>
#end
Return a value from the filter body
Capture the result with #set, then output it. This handles negation and multiple conditions without wrapping each one in #if / true / #end, and the examples below all use it.
#queryfilter($query, $asset)
#set($result = $asset.path.contains("index"))
$result
#end
To check what your body logic actually returns, move it into #queryexecute temporarily and output the result.
Filter by structured data value
Include only assets where a structured data checkbox has a specific value. This is useful for multi-value fields where built-in methods can't filter directly.
#set ($query = $_.query().byContentType("News").preloadStructuredData())
#queryfilter($query, $asset)
$asset.getStructuredDataNode("details/is-featured").textValue.contains("Yes")
#end
#queryexecute($query, $page)
<div class="featured-news">
<h3>${page.metadata.title}</h3>
<p>${page.metadata.summary}</p>
</div>
#end
Filter by dynamic metadata
Include only assets where a dynamic metadata field matches a condition.
#set ($query = $_.query().byContentType("Page").preloadDynamicMetadata())
#queryfilter($query, $asset)
$asset.metadata.getDynamicField("display-in-nav").value.contains("Yes")
#end
#queryexecute($query, $page)
<li><a href="${page.link}">${page.metadata.displayName}</a></li>
#end
Filter by empty or populated field
Use $_PropertyTool.isNotEmpty() to include only assets where a field has a value. Negate it to find assets where a field is empty.
## Only pages where the author field is populated
#set ($query = $_.query().byContentType("Article"))
#queryfilter($query, $asset)
$_PropertyTool.isNotEmpty($asset.metadata.author)
#end
#queryexecute($query, $page)
<li>${page.metadata.title} — ${page.metadata.author}</li>
#end
To find assets where a field is empty, negate the check with #set:
#queryfilter($query, $asset)
#set($result = !$_PropertyTool.isNotEmpty($asset.metadata.author))
$result
#end
Combine multiple conditions
Use #set to combine conditions with && or ||. The result is captured as a boolean and output directly.
#set ($query = $_.query().byContentType("Article").preloadStructuredData())
#queryfilter($query, $asset)
#set($result = $asset.path.contains("index") && $asset.getStructuredDataNode("details/is-featured").textValue.contains("Yes"))
$result
#end
#queryexecute($query, $page)
<li>${page.metadata.title}</li>
#end
OR logic works the same way:
#queryfilter($query, $asset)
#set($result = $asset.metadata.title.contains("News") || $asset.metadata.title.contains("Events"))
$result
#end
Exclude assets matching a condition
To exclude assets matching a condition, negate the check.
#set ($query = $_.query().byContentType("Article"))
#queryfilter($query, $asset)
#set($result = !$asset.path.contains("/archive/"))
$result
#end
#queryexecute($query, $page)
<li>${page.metadata.title}</li>
#end
#querysortvalue
Use #querysortvalue to set each asset's sort value before maxResults() is applied. It is an alternative to .sortBy() and works with both #queryexecute and .execute().
Sorting is string-based. Pad numbers with $_NumberTool.withPadding so they sort correctly, and set .sortDirection() on the query to control direction.
Sort by metadata title
#set ($query = $_.query().byContentType("Article"))
#querysortvalue($query, $asset)
$asset.metadata.title
#end
#queryexecute($query, $page)
<li>${page.metadata.title}</li>
#end
Sort by structured data text field
Sort by a text value stored in structured data rather than a metadata field.
#set ($query = $_.query().byContentType("Article").preloadStructuredData().sortDirection("asc"))
#querysortvalue($query, $asset)
$asset.getStructuredDataNode("title").textValue
#end
#queryexecute($query, $page)
<li>${page.getStructuredDataNode("title").textValue}</li>
#end
Sort numbers with $_NumberTool.withPadding
Since #querysortvalue sorts by string, numeric values need padding to sort correctly (e.g., "5" would sort after "10" without padding).
#set ($query = $_.query().byContentType("Course").preloadStructuredData().sortDirection("asc"))
#querysortvalue($query, $asset)
$_NumberTool.withPadding($asset.getStructuredDataNode("course-number").textValue)
#end
#queryexecute($query, $course)
<li>${course.getStructuredDataNode("course-number").textValue} - ${course.metadata.title}</li>
#end
Sort by more than one field
Since sorting is string-based, you can concatenate multiple values to sort by more than one field. The first value acts as the primary sort, the second as the tiebreaker, and so on.
Alphabetical by last name, then first name
#set ($query = $_.query().byContentType("Staff").preloadStructuredData().sortDirection("asc"))
#querysortvalue($query, $asset)
$asset.getStructuredDataNode("last-name").textValue $asset.getStructuredDataNode("first-name").textValue
#end
#queryexecute($query, $person)
<li>${person.getStructuredDataNode("last-name").textValue}, ${person.getStructuredDataNode("first-name").textValue}</li>
#end
By department number, then course name
#set ($query = $_.query().byContentType("Course").preloadStructuredData().sortDirection("asc"))
#querysortvalue($query, $asset)
$_NumberTool.withPadding($asset.getStructuredDataNode("department-number").textValue) $asset.metadata.title
#end
#queryexecute($query, $course)
<li>${course.getStructuredDataNode("department-number").textValue} - ${course.metadata.title}</li>
#end
Note the $_NumberTool.withPadding on the department number. Without it, department "5" would sort after "10" since it's comparing strings.
By author, then title
#set ($query = $_.query().byContentType("Article").sortDirection("asc").maxResults(50))
#querysortvalue($query, $asset)
$asset.metadata.author $asset.metadata.title
#end
#queryexecute($query, $page)
<li>${page.metadata.author} — ${page.metadata.title}</li>
#end
Sort by date stored in structured data
Date strings like 01-15-2024 02:30:00 PM won't sort chronologically as plain strings because the month comes first. Convert the value to epoch milliseconds with $_DateTool so the sort is truly chronological.
#set ($query = $_.query().byContentType("Event").preloadStructuredData().sortDirection("desc").maxResults(20))
#querysortvalue($query, $asset)
#set ($dt = $asset.getStructuredDataNode("event-date").textValue)
#if($_PropertyTool.isNotEmpty($dt))
#set ($date = $_DateTool.toDate("MM-dd-yyyy hh:mm:ss a", $dt))
${date.getTime()}
#else
0
#end
#end
#queryexecute($query, $event)
<li>
${event.metadata.title}
<span>${event.getStructuredDataNode("event-date").textValue}</span>
</li>
#end
The format string passed to $_DateTool.toDate() must match the format stored in your data definition. Adjust it to match your field's format.
Sort descending
The directive respects .sortDirection(). Set it on the query object before using #querysortvalue.
#set ($query = $_.query().byContentType("News").sortDirection("desc"))
#querysortvalue($query, $asset)
$asset.metadata.title
#end
#queryexecute($query, $page)
<li>${page.metadata.title}</li>
#end
Combining directives
All three directives can be used together on the same query. The order of operations is: #queryfilter runs first to narrow the results, then #querysortvalue determines the sort order, and finally #queryexecute outputs the results up to the maxResults() limit.
Full example: filtered, sorted event listing
Query all events, filter to only featured ones, sort by a structured data date field, and output the first 50.
## Build the query
#set ($query = $_.query().byContentType("Event").preloadStructuredData().sortDirection("asc").maxResults(50))
## Filter: only featured events
#queryfilter($query, $asset)
$asset.getStructuredDataNode("is-featured").textValue.contains("Yes")
#end
## Sort: by event date stored in structured data
#querysortvalue($query, $asset)
$asset.getStructuredDataNode("event-date").textValue
#end
## Output
<ul>
#queryexecute($query, $event)
<li>
<strong>${event.metadata.title}</strong>
<span>${event.getStructuredDataNode("event-date").textValue}</span>
</li>
#end
</ul>
Date range filter with chronological sorting
Filter to events within a specific date range and sort them chronologically. The filter parses each date into epoch milliseconds for comparison, and the sort does the same for ordering.
#set ($query = $_.query().byContentType("Event").preloadStructuredData().sortDirection("asc").maxResults(100))
#set ($rangeStart = $_DateTool.toDate("yyyy-MM-dd", "2024-01-01").getTime())
#set ($rangeEnd = $_DateTool.toDate("yyyy-MM-dd", "2024-12-31").getTime())
## Filter: only events in 2024
#queryfilter($query, $asset)
#set ($dt = $asset.getStructuredDataNode("event-date").textValue)
#if($_PropertyTool.isNotEmpty($dt))
#set ($ts = $_DateTool.toDate("MM-dd-yyyy hh:mm:ss a", $dt).getTime())
#if($ts >= $rangeStart && $ts <= $rangeEnd)
true
#end
#end
#end
## Sort: chronological by event date
#querysortvalue($query, $asset)
#set ($dt = $asset.getStructuredDataNode("event-date").textValue)
#if($_PropertyTool.isNotEmpty($dt))
#set ($date = $_DateTool.toDate("MM-dd-yyyy hh:mm:ss a", $dt))
${date.getTime()}
#else
0
#end
#end
## Output with formatted date
#queryexecute($query, $event)
#set ($dt = $event.getStructuredDataNode("event-date").textValue)
#set ($date = $_DateTool.toDate("MM-dd-yyyy hh:mm:ss a", $dt))
#set ($formatted = $_DateTool.format("MMMM d, yyyy", $date))
<li>
<strong>${event.metadata.title}</strong>
<span>$formatted</span>
</li>
#end
In-depth examples
These examples demonstrate advanced patterns that build on the basics above. They reference a Content Type with the following fields. Adapt the content type path and field names to match your own setup.
- Structured data:
title(text),event-date(datetime inMM-dd-yyyy hh:mm:ss aformat),asset-chooser(page chooser) - Dynamic metadata:
multiselect(multi-select),checkbox(checkbox),department(dropdown),radio(radio),noindex(radio) - Standard metadata: title, author, displayName, summary, description, teaser, keywords, startDate, endDate
Filtering
Check that a multi-value field contains every expected value using multiple .contains() calls joined with &&.
#set ($query = $_.query().byContentType("Article").preloadDynamicMetadata().maxResults(-1))
#queryfilter($query, $asset)
#set ($cb = $asset.metadata.getDynamicField("checkbox").value)
#set($result = $cb.contains("Easy") && $cb.contains("Fast") && $cb.contains("Cheap"))
$result
#end
#queryexecute($query, $page)
<li>${page.metadata.title}</li>
#end
Filter using fields from three different sources in one condition. Preload both structured data and dynamic metadata on the query.
#set ($query = $_.query().byContentType("Article").preloadStructuredData().preloadDynamicMetadata().maxResults(-1))
#queryfilter($query, $asset)
#set($result = $_PropertyTool.isNotEmpty($asset.getStructuredDataNode("event-date").textValue) && $asset.metadata.getDynamicField("checkbox").value.contains("Easy") && $asset.getStructuredDataNode("title").textValue.contains("Guide"))
$result
#end
#queryexecute($query, $page)
<li>${page.getStructuredDataNode("title").textValue}</li>
#end
Derive a year from the page's folder structure and compare it against a structured data date field to show only events from that year. This pattern is useful when content is organized into year-based folders (e.g. /events/2025/_data) and each folder's index should list only the events for that year.
#if($currentPage.name == "_data")
#set($currentYear = $_MathTool.toInteger($currentPage.parentFolder.name))
#set($currentYearStr = "${currentYear}")
#set($query = $_.query().byContentType("site://_redesign/Events/Event Details").preloadDynamicMetadata().preloadStructuredData().searchAcrossAllSites().maxResults(-1))
#queryfilter($query, $asset)
#set($dt = $asset.getStructuredDataNode("start").value)
#set($dt = $_MathTool.toNumber($dt))
#if($_PropertyTool.isNotEmpty($dt))
#set($year = $_DateTool.format("yyyy", $dt))
#set($result = $year == $currentYearStr)
$result
#end
#end
#queryexecute($query, $page)
#set($dt = $page.getStructuredDataNode("start").value)
$page.link $_DateTool.format("yyyy", $_MathTool.toNumber($dt))
#end
#end
Sorting
Concatenate a text field with epoch milliseconds for a two-level sort. The text field acts as the primary sort, and the date as the tiebreaker.
#set ($query = $_.query().byContentType("Event").preloadStructuredData().preloadDynamicMetadata().sortDirection("asc").maxResults(30))
#querysortvalue($query, $asset)
#set ($group = $asset.metadata.getDynamicField("department").value)
#set ($dt = $asset.getStructuredDataNode("event-date").textValue)
#if($_PropertyTool.isNotEmpty($dt))
#set ($date = $_DateTool.toDate("MM-dd-yyyy hh:mm:ss a", $dt))
${group} ${date.getTime()}
#else
${group} 0
#end
#end
#queryexecute($query, $page)
<li>${page.metadata.getDynamicField("department").value} — ${page.metadata.title}</li>
#end
Combined
Combine #queryfilter to narrow by category, #querysortvalue to sort by date descending, and maxResults(10) to take only the top 10.
#set ($query = $_.query().byContentType("Article").preloadStructuredData().sortDirection("desc").maxResults(10))
#queryfilter($query, $asset)
$asset.metadata.title.contains("Science")
#end
#querysortvalue($query, $asset)
#set ($dt = $asset.getStructuredDataNode("event-date").textValue)
#if($_PropertyTool.isNotEmpty($dt))
#set ($date = $_DateTool.toDate("MM-dd-yyyy hh:mm:ss a", $dt))
${date.getTime()}
#else
0
#end
#end
#queryexecute($query, $page)
<li>${page.metadata.title}</li>
#end
Filter on three conditions (dynamic metadata + standard metadata), then sort by two concatenated fields.
#set ($query = $_.query().byContentType("Article").preloadStructuredData().preloadDynamicMetadata().sortDirection("asc").maxResults(25))
#queryfilter($query, $asset)
#set($result = $asset.metadata.getDynamicField("checkbox").value.contains("Fast") && $asset.metadata.getDynamicField("noindex").value.contains("No") && $_PropertyTool.isNotEmpty($asset.metadata.summary))
$result
#end
#querysortvalue($query, $asset)
$asset.metadata.getDynamicField("department").value $asset.metadata.author
#end
#queryexecute($query, $page)
<li>
${page.metadata.getDynamicField("department").value} —
${page.metadata.author} —
${page.metadata.title}
</li>
#end
Sort by the grouping field so identical values are adjacent, then detect when the value changes to insert a header.
#set ($query = $_.query().byContentType("Article").preloadDynamicMetadata().sortDirection("asc").maxResults(50))
#querysortvalue($query, $asset)
$asset.metadata.getDynamicField("department").value $asset.metadata.title
#end
#set ($currentGroup = "")
#queryexecute($query, $page)
#set ($group = $page.metadata.getDynamicField("department").value)
#if($group != $currentGroup)
<h3>$group</h3>
#set ($currentGroup = $group)
#end
<li>${page.metadata.title}</li>
#end
Advanced patterns
Use a map to count how many pages have each value in a field. Arithmetic (+) only works inside #set directives, not inside method arguments like .put().
#set ($query = $_.query().byContentType("Article").preloadDynamicMetadata().maxResults(-1))
#set ($counts = {"Yes": 0, "No": 0, "Maybe": 0})
#queryexecute($query, $page)
#set ($val = $page.metadata.getDynamicField("radio").value)
#if($val.contains("Yes"))
#set ($n = $counts.get("Yes") + 1)
#set ($dummy = $counts.put("Yes", $n))
#elseif($val.contains("No"))
#set ($n = $counts.get("No") + 1)
#set ($dummy = $counts.put("No", $n))
#elseif($val.contains("Maybe"))
#set ($n = $counts.get("Maybe") + 1)
#set ($dummy = $counts.put("Maybe", $n))
#end
#end
Yes: $counts.get("Yes"), No: $counts.get("No"), Maybe: $counts.get("Maybe")
Filter to pages with a date, sort chronologically using epoch milliseconds, then format the date for readable output with $_DateTool.format().
#set ($query = $_.query().byContentType("Event").preloadStructuredData().sortDirection("desc").maxResults(15))
#queryfilter($query, $asset)
$_PropertyTool.isNotEmpty($asset.getStructuredDataNode("event-date").textValue)
#end
#querysortvalue($query, $asset)
#set ($dt = $asset.getStructuredDataNode("event-date").textValue)
#set ($date = $_DateTool.toDate("MM-dd-yyyy hh:mm:ss a", $dt))
${date.getTime()}
#end
#queryexecute($query, $event)
#set ($dt = $event.getStructuredDataNode("event-date").textValue)
#set ($date = $_DateTool.toDate("MM-dd-yyyy hh:mm:ss a", $dt))
#set ($formatted = $_DateTool.format("MMMM d, yyyy", $date))
<li>
<strong>${event.metadata.title}</strong>
<span>$formatted</span>
</li>
#end
Define a map of categories with initial counts of zero. Iterate all pages, check each category, and increment the count. Output the breakdown with #foreach on the map entries.
#set ($query = $_.query().byContentType("Article").maxResults(-1))
#set ($cats = {"Technology": 0, "Science": 0, "Education": 0, "Healthcare": 0, "Business": 0})
#queryexecute($query, $page)
#set ($t = $page.metadata.title)
#foreach($cat in $cats.keySet())
#if($t.contains($cat))
#set ($n = $cats.get($cat) + 1)
#set ($dummy = $cats.put($cat, $n))
#end
#end
#end
<ul>
#foreach($entry in $cats.entrySet())
<li>$entry.key: $entry.value</li>
#end
</ul>
Migration guide
Convert an existing .execute() pattern by removing the call and moving the loop body into a directive.
Before: .execute() + #foreach
#set ($results = $_.query().byContentType("Article").maxResults(10).sortBy("startDate").sortDirection("desc").execute())
#foreach ($page in $results)
<h3>$page.metadata.title</h3>
<p>$page.metadata.summary</p>
#end
After: #queryexecute
#set ($query = $_.query().byContentType("Article").maxResults(10).sortBy("startDate").sortDirection("desc"))
#queryexecute($query, $page)
<h3>$page.metadata.title</h3>
<p>$page.metadata.summary</p>
#end
Key changes:
- Remove
.execute()from the query chain. - Replace
#foreach ($page in $results)with#queryexecute($query, $page). - The body stays the same.
Before: .execute() with manual filtering
#set ($list = $_.query().byContentType("News").preloadStructuredData().execute())
#foreach ($page in $list)
#if ($page.getStructuredDataNode("details/is-featured").textValue.contains("Yes"))
<div>$page.metadata.title</div>
#end
#end
After: #queryfilter + #queryexecute
#set ($query = $_.query().byContentType("News").preloadStructuredData())
#queryfilter($query, $asset)
$asset.getStructuredDataNode("details/is-featured").textValue.contains("Yes")
#end
#queryexecute($query, $page)
<div>$page.metadata.title</div>
#end
Key changes:
- Remove
.execute()and the#foreach/#ifblock. - Move the condition into
#queryfilter. The body must return the exact stringtrue. - Use
#queryexecutefor the output, now with only matching results.
Because #queryfilter runs before maxResults(), you get the full count of matching results rather than filtering down from a truncated list.