Skip to main content

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.

Arguments shared by all three query directives.
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

Quick decision guide: .execute() vs 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().

Velocity
## 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.

Velocity
#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.

Velocity
#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.

Velocity
#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.

Velocity
#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.

Velocity
#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.

Velocity
#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.

Velocity
#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.

Velocity
#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.

Velocity
## 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} &mdash; ${page.metadata.author}</li>
#end

To find assets where a field is empty, negate the check with #set:

Velocity
#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.

Velocity
#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:

Velocity
#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.

Velocity
#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

Velocity
#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.

Velocity
#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).

Velocity
#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

Velocity
#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

Velocity
#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

Velocity
#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} &mdash; ${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.

Velocity
#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.

Velocity
#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.

Velocity
## 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.

Velocity
#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 in MM-dd-yyyy hh:mm:ss a format), 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

Sorting

Combined

Advanced patterns

Migration guide

Convert an existing .execute() pattern by removing the call and moving the loop body into a directive.

Before: .execute() + #foreach

Velocity
#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

Velocity
#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:

  1. Remove .execute() from the query chain.
  2. Replace #foreach ($page in $results) with #queryexecute($query, $page).
  3. The body stays the same.

Before: .execute() with manual filtering

Velocity
#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

Velocity
#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:

  1. Remove .execute() and the #foreach/#if block.
  2. Move the condition into #queryfilter. The body must return the exact string true.
  3. Use #queryexecute for 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.