
# Query Tool directives

> Introduced in Cascade CMS 8.26, Query Directives are an alternative to the .execute() method on the Query API. They process results one asset at a time instead of loading

URL: https://www.hannonhill.com/cascadecms/latest/articles/developing-in-cascade/script-formats/query-tool-directives.html
category: Developing in Cascade
article-type: reference
content-kind: article
release-availability: all
tags: velocity, query-tool
updated: 2026-10-06

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

### Filter by all values in a multi-select field

Check that a multi-value field contains every expected value using multiple `.contains()` calls joined with `&&`.

```velocity
#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
```

### Combine structured data, dynamic metadata, and standard metadata

Filter using fields from three different sources in one condition. Preload both structured data and dynamic metadata on the query.

```velocity
#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
```

### Filter by date range using folder context

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.

```velocity
#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

### Sort by field value, then by date chronologically

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.

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

### Combined

### Most recent pages in a filtered category

Combine `#queryfilter` to narrow by category, `#querysortvalue` to sort by date descending, and `maxResults(10)` to take only the top 10.

```velocity
#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
```

### Three-field filter with two-field sort

Filter on three conditions (dynamic metadata + standard metadata), then sort by two concatenated fields.

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

### Group results under dynamic headers

Sort by the grouping field so identical values are adjacent, then detect when the value changes to insert a header.

```velocity
#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

### Aggregate counts by field value

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()`.

```velocity
#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")
```

### Parse, sort, and format dates for display

Filter to pages with a date, sort chronologically using epoch milliseconds, then format the date for readable output with `$_DateTool.format()`.

```velocity
#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
```

### Count pages per category using a map

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.

```velocity
#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

```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.
1. Replace `#foreach ($page in $results)` with `#queryexecute($query, $page)`.
1. 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.
1. Move the condition into `#queryfilter`. The body must return the exact string `true`.
1. 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.
