Velocity Directives
Velocity directives are special instructions that control how your template is processed. They always begin with a # character. This article covers the core Velocity directives you are most likely to use when building Cascade CMS Formats, with practical examples and common pitfalls. Cascade-specific query directives are covered separately in the Query Tool Directives article.
| Directive | What it does |
|---|---|
#set |
Assign a value to a variable. |
#if / #elseif / #else |
Render content conditionally. |
#foreach |
Iterate over a collection. |
#break |
Exit the current #foreach loop early. |
#stop |
Halt all template processing immediately. |
#macro |
Define a reusable, parameterized block of code. |
#import |
Pull macros and shared code from another Format. |
#evaluate |
Execute a Velocity string built at runtime. |
#define |
Capture a block of template content for later reuse. |
#parse |
Include and evaluate another Format inline. |
#set
The #set directive assigns a value to a variable. Once set, the variable is available for the remainder of the template (or until overwritten).
Basic syntax
#set ($variable = "value")
Supported value types
## Strings
#set ($name = "Engineering")
## Numbers
#set ($count = 42)
## Booleans
#set ($isActive = true)
## Lists (ArrayLists)
#set ($colors = ["red", "green", "blue"])
## Ranges
#set ($range = [1..5]) ## creates [1, 2, 3, 4, 5]
## Maps (HashMaps)
#set ($person = {"name": "Jane", "role": "Developer"})
## Expressions
#set ($total = $price * $quantity)
## String concatenation
#set ($fullName = "${firstName} ${lastName}")
## Method calls
#set ($title = $currentPage.metadata.title)
The null-doesn’t-unset pitfall
When the right-hand side of a #set resolves to null, the variable retains its previous value. This is one of the most common sources of bugs in Velocity.
## BUG: $subtitle leaks between iterations
#foreach ($item in $items)
#set ($subtitle = $item.getChild("subtitle").value)
<p>$subtitle</p> ## shows PREVIOUS item's subtitle if current item has none
#end
The fix is to reset the variable before each assignment:
## CORRECT: reset before each #set to avoid stale data
#foreach ($item in $items)
#set ($subtitle = "")
#set ($subtitle = $item.getChild("subtitle").value)
#if ($_PropertyTool.isNotEmpty($subtitle))
<p>$subtitle</p>
#end
#end
Tip — for a deeper discussion of variable scoping, the reset-before-set idiom, and how #set behaves inside macros, see the Variable Scope article.
#if / #elseif / #else
Conditional directives control which portions of the template are rendered based on boolean expressions.
#if ($condition)
## rendered when $condition is true
#elseif ($otherCondition)
## rendered when $otherCondition is true
#else
## rendered when none of the above are true
#end
Cascade CMS provides the $_PropertyTool which offers reliable methods for checking whether a value is null, empty, or blank. This is the recommended approach over manual null/empty checks.
## $_PropertyTool.isNotEmpty() — true when value is not null AND not empty
#set ($title = $currentPage.metadata.title)
#if ($_PropertyTool.isNotEmpty($title))
<h1>$title</h1>
#end
## $_PropertyTool.isEmpty() — true when value is null or empty
#if ($_PropertyTool.isEmpty($currentPage.metadata.description))
<meta name="description" content="Default description" />
#else
<meta name="description" content="$currentPage.metadata.description" />
#end
## Negate with ! for "has a value" checks
#if (!$_PropertyTool.isEmpty($currentPage.metadata.title))
<title>$currentPage.metadata.title</title>
#else
<title>Untitled</title>
#end
Tip — prefer $_PropertyTool.isNotEmpty() and $_PropertyTool.isEmpty() over manual checks like $var != "". The PropertyTool handles null, empty strings, and edge cases consistently.
## Check if a structured data field exists and has content
#set ($heroImage = "")
#set ($heroImage = $currentPage.getStructuredDataNode("hero-image"))
#if ($_PropertyTool.isNotEmpty($heroImage) && $heroImage.asset)
<img src="$heroImage.asset.link" alt="$currentPage.metadata.title" />
#end
## Conditionally render based on metadata
#set ($pageType = "")
#set ($pageType = $currentPage.metadata.getDynamicField("page-type").value)
#if ($pageType == "landing")
## render landing page layout
#elseif ($pageType == "detail")
## render detail page layout
#else
## render default layout
#end
## Check if a collection has results
#set ($results = $_query.byContentType("news").execute())
#if ($_PropertyTool.isNotEmpty($results))
## render results
#else
<p>No news articles found.</p>
#end
Note — in Velocity, #if ($var) evaluates to false when $var is null or boolean false, but its behavior with empty strings can be inconsistent. Using $_PropertyTool.isEmpty() and $_PropertyTool.isNotEmpty() removes that ambiguity.
Comparison operators
| Operator | Meaning | Example |
|---|---|---|
== |
Equal to | #if ($status == "published") |
!= |
Not equal to | #if ($type != "hidden") |
> |
Greater than | #if ($count > 0) |
< |
Less than | #if ($count < 10) |
>= |
Greater than or equal | #if ($count >= 1) |
<= |
Less than or equal | #if ($count <= 100) |
&& |
Logical AND | #if ($a && $b) |
|| |
Logical OR | #if ($a || $b) |
! |
Logical NOT | #if (!$isHidden) |
#foreach
The #foreach directive iterates over a collection (list, array, map, or query result set) and repeats its body for each element.
#foreach ($item in $collection)
## loop body — $item changes on each iteration
#end
Velocity provides a built-in $foreach object with useful properties inside any #foreach loop:
| Property | Description | Example value (3 items) |
|---|---|---|
$foreach.index |
Zero-based index of the current iteration | 0, 1, 2 |
$foreach.count |
One-based count of the current iteration | 1, 2, 3 |
$foreach.hasNext |
True if there are more items after this one | true, true, false |
$foreach.first |
True on the first iteration only | true, false, false |
$foreach.last |
True on the last iteration only | false, false, true |
#set ($news = $_query.byContentType("news-article").sortBy("startDate").sortDirection("desc").maxResults(10).execute())
#foreach ($article in $news)
<article>
<h2><a href="$article.link">$article.metadata.title</a></h2>
<p>$article.metadata.description</p>
</article>
#end
#set ($slides = $currentPage.getStructuredDataNodes("slide"))
#foreach ($slide in $slides)
#set ($heading = "")
#set ($heading = $slide.getChild("heading").value)
#set ($caption = "")
#set ($caption = $slide.getChild("caption").value)
<div class="slide #if($foreach.first)slide--active#end">
#if ($_PropertyTool.isNotEmpty($heading))
<h2>$heading</h2>
#end
#if ($_PropertyTool.isNotEmpty($caption))
<p>$caption</p>
#end
</div>
#end
## Build a comma-separated tag list
#set ($tags = $currentPage.metadata.getDynamicField("tags").values)
#foreach ($tag in $tags)${tag}#if($foreach.hasNext), #end#end
## Output: News, Featured, Campus Life
## Render a table from structured data with rows and columns
#set ($rows = $currentPage.getStructuredDataNodes("row"))
<table>
#foreach ($row in $rows)
<tr>
#set ($cells = $row.getChildren("cell"))
#foreach ($cell in $cells)
<td>$cell.value</td>
#end
</tr>
#end
</table>
Warning — always use the reset-before-set pattern — #set ($var = "") then #set ($var = ...) — for any variable assigned inside a #foreach loop. Without this, null values from one iteration will leak the previous iteration’s data.
#break
#break exits the current #foreach loop immediately. Execution continues with whatever comes after the #end of the loop. This is useful when you need to find the first match in a collection or limit output based on a condition.
#foreach ($item in $collection)
#if ($someCondition)
#break
#end
#end
## Find the first featured news article
#set ($featured = false)
#set ($results = $_query.byContentType("news").execute())
#foreach ($article in $results)
#set ($isFeatured = "")
#set ($isFeatured = $article.metadata.getDynamicField("featured").value)
#if ($isFeatured == "Yes")
#set ($featured = $article)
#break
#end
#end
#if ($featured)
<div class="featured-article">
<h2>$featured.metadata.title</h2>
<p>$featured.metadata.description</p>
</div>
#end
## Show only the first 3 visible items from a structured data group
#set ($items = $currentPage.getStructuredDataNodes("item"))
#set ($shown = 0)
#foreach ($item in $items)
#set ($isVisible = "")
#set ($isVisible = $item.getChild("visible").value)
#if ($isVisible == "Yes")
<div class="item">$item.getChild("title").value</div>
#set ($shown = $shown + 1)
#if ($shown >= 3)
#break
#end
#end
#end
#break vs #stop — #break exits only the current #foreach loop and continues processing the rest of the template. #stop halts all template processing entirely. In nested loops, #break only exits the innermost loop.
#stop
#stop immediately halts all template processing. No further output is generated after this directive executes. This is useful for early exits when required data is missing or for debugging.
#stop
## Guard clause: stop if the page has no content type assigned
#set ($contentType = "")
#set ($contentType = $currentPage.metadata.getDynamicField("content-type").value)
#if ($_PropertyTool.isEmpty($contentType))
<!-- Error: no content type configured for this page -->
#stop
#end
## Rest of the template only runs if $contentType is set
<div class="$contentType">
## ...
</div>
Render a friendly empty state and stop, so the rest of the template (which assumes items exist) doesn’t try to iterate or render headings.
#set ($items = $currentPage.getStructuredDataNodes("item"))
#if ($items.isEmpty())
<p class="empty-state">No items configured yet.</p>
#stop
#end
## The rest of the template only runs when $items has content
<ul>
#foreach ($item in $items)
<li>$item.getChild("title").value</li>
#end
</ul>
## Temporarily stop execution to inspect variables at this point
<pre>
title: $currentPage.metadata.title
results count: $results.size()
</pre>
#stop
## Everything below is skipped during debugging
Tip — place #stop after a <pre> block that dumps variable values to create a quick-and-dirty debugging checkpoint. Remember to remove it before publishing.
#macro
The #macro directive defines a reusable block of Velocity code that can accept parameters. Macros help you avoid duplicating template logic across your Formats.
Defining and calling macros
## Define a macro
#macro (renderCard $page $extraClass)
#set ($rc_title = "")
#set ($rc_title = $page.metadata.title)
#set ($rc_desc = "")
#set ($rc_desc = $page.metadata.description)
<div class="card $extraClass">
<h3><a href="$page.link">$rc_title</a></h3>
#if ($_PropertyTool.isNotEmpty($rc_desc))
<p>$rc_desc</p>
#end
</div>
#end
## Call the macro
#set ($results = $_query.byContentType("page").maxResults(6).execute())
#foreach ($item in $results)
#renderCard($item "card--featured")
#end
Scope leaking
Variables set with #set inside a macro are not scoped to that macro. They write directly to the same namespace as the calling Format, which means they can silently overwrite variables in the outer context.
## BUG: macro overwrites the outer $title
#macro (renderHeading $page)
#set ($title = $page.metadata.title)
<h2>$title</h2>
#end
#set ($title = "My Page Title")
<h1>$title</h1> ## "My Page Title"
#renderHeading($someOtherPage)
<h1>$title</h1> ## now shows the OTHER page's title!
The fix is to prefix all variables inside macros with a unique identifier to avoid collisions:
## CORRECT: prefix macro variables to avoid collisions
#macro (renderHeading $page)
#set ($rh_title = $page.metadata.title)
<h2>$rh_title</h2>
#end
#set ($title = "My Page Title")
<h1>$title</h1> ## "My Page Title"
#renderHeading($someOtherPage)
<h1>$title</h1> ## still "My Page Title"
Important — Velocity macros do not have their own scope. Every #set inside a macro writes to the calling template’s namespace. Use a naming prefix (e.g. $rc_, $nav_) for macro-internal variables. See the Variable Scope article for more details.
#import
The #import directive includes the contents of another Velocity Format from the CMS into the current template. This is how you share macros, utility functions, and common template fragments across multiple Formats.
Syntax
## Import a shared macro library
#import ("_cms/formats/shared/macros/utility")
The path is relative to the root of the site in Cascade CMS. After importing, all macros and variables defined in the imported Format are available in the current template.
Common pattern: shared macro library
## In _cms/formats/shared/macros/utility (the imported Format):
#macro (truncate $text $maxLength)
#if ($text.length() > $maxLength)
${text.substring(0, $maxLength)}...
#else
$text
#end
#end
#macro (formatDate $dateString $pattern)
#set ($fd_formatted = $_DateTool.format($pattern, $_DateTool.toDate("yyyy-MM-dd'T'HH:mm:ss", $dateString)))
$fd_formatted
#end
## In the main Format:
#import ("_cms/formats/shared/macros/utility")
## Now you can use the macros
<p>#truncate($currentPage.metadata.description, 150)</p>
<time>#formatDate($currentPage.metadata.startDate, "MMMM d, yyyy")</time>
Performance — each #import directive requires a round trip to the database to fetch the Format’s contents. Consolidate related macros into fewer Formats to minimize the number of #import calls. See the Best Practices for Performance article for detailed guidance.
#evaluate
#evaluate takes a string and processes it as Velocity code at runtime. The most common use in Cascade is calling a macro whose name is stored in a variable: you build the directive call as a string, then evaluate it. This lets a single dispatcher macro route to any named macro without a chain of #if/#elseif branches.
Syntax
#evaluate($stringContainingVelocityCode)
Examples
A dispatcher macro that calls any named macro by building the call string at runtime. The $"+"data" split prevents Velocity from trying to resolve $data inside the string literal before #evaluate runs.
## Dispatcher: call any macro by name
#macro(runMacro $name $data $origin)
#set ($macroBuild = "#" + $name + "($" + "data $" + "origin)")
#set ($macroRun = "#evaluate($macroBuild)")
$macroRun
#end
## Usage — $name is resolved at call time
#runMacro("heroBlock" $pageData "default")
To locate and import a Format before calling its macro, use $_.locateFormat() and #import first, then dispatch with #runMacro.
## Locate and import the Format that defines the macro
#set ($formatPath = "/_components/hero")
#set ($formatSitePath = $_.locateFormat($formatPath, $globalSite))
#import($formatSitePath)
## Now dispatch to the macro by variable name
#set ($macroName = "heroBlock")
#runMacro($macroName $pageData "default")
Evaluate a Velocity snippet stored in a structured data field. Use this only when you control what goes into that field — never evaluate untrusted user input.
#set ($snippet = "")
#set ($snippet = $currentPage.getStructuredDataNode("velocity-snippet").value)
#if ($_PropertyTool.isNotEmpty($snippet))
#evaluate($snippet)
#end
Use sparingly — #evaluate executes code that isn’t visible in the Format, which makes templates harder to read and debug. Only evaluate strings you control. When the set of possible values is small and known, #if/#elseif branching or #macro parameters are usually clearer.
#define
#define captures a block of Velocity code that is not evaluated immediately. Instead, the block is evaluated each time the variable is referenced. This makes it behave like a reusable template fragment that always reflects the current state of your variables.
Syntax
#define ($blockName)
## template content here — evaluated later, not now
#end
How it differs from #set
#set evaluates the right-hand side immediately and stores the result as a fixed value. #define stores the block as unevaluated code and re-evaluates it every time the variable is referenced. This distinction matters whenever the variables inside the block change between the point of definition and the point of use.
## #set evaluates immediately (snapshot)
#set ($count = 0)
#set ($message = "Count is $count")
#set ($count = 5)
$message ## outputs: Count is 0
## #define evaluates lazily (live feed)
#set ($count = 0)
#define ($message)Count is $count#end
#set ($count = 5)
$message ## outputs: Count is 5
Examples
The most common use for #define is a template fragment re-evaluated inside a loop. Because the block evaluates fresh each iteration, it picks up the current loop variable.
## Define a card template once, reuse it in the loop
#define ($card)
<div class="card">
<h3><a href="$item.link">$item.metadata.title</a></h3>
<p>$item.metadata.description</p>
</div>
#end
#set ($results = $_query.byContentType("page").execute())
#foreach ($item in $results)
$card ## re-evaluates each iteration with the current $item
#end
Use #define to prepare layout sections that adapt to context. The same block can appear in different places, picking up whatever data the surrounding code has set.
## Define a sidebar block that always reflects the latest $sidebarItems
#define ($sidebar)
<aside class="sidebar">
#foreach ($link in $sidebarItems)
<a href="$link.link">$link.metadata.title</a>
#end
</aside>
#end
## Use it with different data sets on the same page
#set ($sidebarItems = $_query.byContentType("quick-link").maxResults(5).execute())
$sidebar ## renders quick links
#set ($sidebarItems = $_query.byContentType("related-resource").maxResults(5).execute())
$sidebar ## renders related resources with the same markup
Define a small badge once, reuse it inside a loop. Each reference re-evaluates $foreach.count and $items.size() against the current loop state.
#define ($badge)
<span class="badge">Item $foreach.count of $items.size()</span>
#end
#set ($items = $_query.byContentType("news").execute())
#foreach ($item in $items)
$badge <a href="$item.link">$item.metadata.title</a>
#end
Define the link markup once, reuse it after each card. The $item reference resolves lazily at render time, so each call picks up the current loop item.
#define ($readMore)
<a href="$item.link" class="read-more">Read more →</a>
#end
#foreach ($item in $featured)
<article>
<h3>$item.metadata.title</h3>
<p>$item.metadata.description</p>
$readMore
</article>
#end
#define vs #macro
Both #define and #macro let you reuse template blocks, but they work differently:
| Feature | #define | #macro |
|---|---|---|
| Parameters | None — relies on variables already in scope | Accepts explicit parameters |
| Evaluation | Lazy — re-evaluated each time the variable is referenced | Evaluated when called |
| Scope | Reads from the current scope at evaluation time | Shares the caller’s scope (no isolation) |
| Reuse across Formats | No — local to the current template | Yes — can be imported via #import |
| Best for | Template fragments reused within one Format | Utility functions shared across Formats |
Tip — think of #set as taking a photo (captured once) and #define as a live camera feed (always shows the current state when viewed). Use #define when you want a reusable template block that adapts to changing variables. Use #set when you want a fixed snapshot. See the Variable Scope article for more examples.
#parse
The #parse directive includes and evaluates another Velocity template inline. The parsed template shares the same variable context as the calling template—variables set before #parse are available inside the parsed Format, and variables set inside the parsed Format are visible in the caller after the call.
Syntax
#parse ("_cms/formats/shared/header-fragment")
The path is relative to the root of the site in Cascade CMS, just like #import.
Differences from #import
| Feature | #import | #parse |
|---|---|---|
| Primary use | Loading macro definitions | Including template fragments that produce output |
| Variable sharing | Macros become available; variable side effects may occur | Full variable context is shared both ways |
| Output | Typically produces no direct output (macro definitions only) | Output is rendered inline at the point of the #parse call |
| Database cost | One round trip per call | One round trip per call |
Examples
Since #parse shares the caller’s variable context, set variables before the call to control how the parsed template behaves. This is how you pass “parameters” to a parsed Format.
## Main Format: set variables, then parse the fragment
#set ($navStyle = "horizontal")
#set ($showSearch = true)
#parse ("_cms/formats/shared/navigation")
## Inside _cms/formats/shared/navigation:
## $navStyle and $showSearch are available here from the caller
<nav class="nav nav--$navStyle">
<ul>
#set ($navItems = $_query.byContentType("nav-item").sortBy("sortOrder").sortDirection("asc").execute())
#foreach ($navItem in $navItems)
<li><a href="$navItem.link">$navItem.metadata.title</a></li>
#end
</ul>
#if ($showSearch)
<form class="nav-search" action="/search">
<input type="search" name="q" placeholder="Search..." />
</form>
#end
</nav>
A common Cascade CMS pattern is to use #parse to assemble a page layout from multiple shared Formats, each handling a different section of the page.
## Main page Format
#import ("_cms/formats/shared/macros/utility")
<!DOCTYPE html>
<html lang="en">
<head>
<title>$currentPage.metadata.title</title>
#parse ("_cms/formats/shared/head-assets")
</head>
<body>
#parse ("_cms/formats/shared/site-header")
<main>
## Page-specific content here
$currentPage.getStructuredDataNode("body").value
</main>
#parse ("_cms/formats/shared/site-footer")
</body>
</html>
Variables set inside a #parsed Format remain in scope after the call returns. This can be useful (the parsed template can compute values for the caller), but it can also cause unexpected collisions.
## Inside _cms/formats/shared/head-assets:
#set ($siteName = "My University")
#set ($cssVersion = "3.2.1")
<link rel="stylesheet" href="/css/main.css?v=$cssVersion" />
## Back in the main Format, after the #parse call:
#parse ("_cms/formats/shared/head-assets")
## $siteName and $cssVersion are now available here
<title>$currentPage.metadata.title | $siteName</title>
Performance — like #import, each #parse call requires a database round trip. Use naming prefixes in parsed Formats to avoid variable collisions with the caller, just as you would with macros.