
# Variable scope in Velocity

> Understand how variable scope works in Velocity, including the difference between global and loop-scoped variables, common pitfalls with #set and null values, macro scope leaking, and when to use #define vs #set.

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

## Global vs loop-scoped variables

Velocity exposes two kinds of variables depending on where they come from:

- **Global variables** like `$currentPage`, `$_`, and `$_DateTool` are set once when the Format begins executing and remain available everywhere in the template. `$currentPage` always refers to the page being rendered.
- **Loop-scoped variables** like `$item` in `#foreach($item in ...)` change on each iteration and only carry meaning inside the loop body.

Understanding which variable points to which asset is the single most important scoping concept in Cascade CMS Velocity development.

> **Tip**
>
> `$currentPage` refers to the page the Velocity Format is rendered on. Inside a `#foreach` loop you’re iterating over a list of items (pages, groups, assets), so the iteration variable changes on each pass, but `$currentPage` is a global variable and never changes.

## Common mistake — $currentPage inside loops

The most frequent scoping bug is referencing `$currentPage` inside a loop when you actually mean the iteration variable. Consider a Format that queries for pages and lists them:

```velocity
## BUG: $currentPage always refers to the rendering page, not the query result
#set ($results = $_query.byContentType("page").execute())
#foreach ($child in $results)
  <h2>$currentPage.metadata.title</h2>
  <p>$currentPage.metadata.description</p>
#end
```

Every iteration prints the **rendering** page’s title and description because `$currentPage` never changes. The loop variable `$child` is the one that advances through the results on each pass.

The fix is straightforward—reference `$child` instead:

```velocity
## CORRECT: use the loop variable for each result's data
#set ($results = $_query.byContentType("page").execute())
#foreach ($child in $results)
  <h2>$child.metadata.title</h2>
  <p>$child.metadata.description</p>
#end
```

> **Warning**
>
> This bug produces output that *looks* correct at first glance because the rendering page’s title renders without errors. The problem only becomes visible when you have multiple results and notice every row shows the same data.

## #set does not unset

In Velocity, `#set($var = $something)` does **not** clear `$var` when `$something` resolves to null. The variable retains whatever value it held before the assignment. This is a common source of stale data in loops.

```velocity
## BUG: $subtitle leaks between iterations
#set ($items = $currentPage.getStructuredDataNodes("row"))
#foreach ($item in $items)
  #set ($subtitle = $item.getChild("subtitle").value)
  ## If this child has no "subtitle" field, $subtitle
  ## still holds the PREVIOUS child's value
  <p>$subtitle</p>
#end
```

When a row lacks a “subtitle” field, the `#set` receives null, but `$subtitle` keeps the last non-null value. The output leaks one row’s subtitle into the next row.

The fix is to reset the variable to an empty string before each `#set` call:

```velocity
## CORRECT: reset before each #set to avoid stale data
#set ($items = $currentPage.getStructuredDataNodes("row"))
#foreach ($item in $items)
  #set ($subtitle = "")
  #set ($subtitle = $item.getChild("subtitle").value)
  #if ($subtitle != "")
    <p>$subtitle</p>
  #end
#end
```

> **Tip**
>
> The reset-before-set pattern (`#set($var = "")` followed by `#set($var = ...)`) is the standard Velocity idiom for handling potentially null values inside loops. Always reset to an empty string or a known default before the real assignment.

## Macro 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 overwrite variables in the outer context.

```velocity
## BUG: the macro overwrites the outer $title
#macro (renderCard $page)
  #set ($title = $page.metadata.title)
  #set ($link = $page.link)
  <div class="card">
    <a href="$link">$title</a>
  </div>
#end

#set ($title = "My Page Title")
<h1>$title</h1>          ## outputs "My Page Title"
#renderCard($someOtherPage)
<h1>$title</h1>          ## outputs the other page's title!
```

After `#renderCard` executes, `$title` in the outer context has been silently replaced with `$someOtherPage`’s title.

The fix is to use a naming prefix convention for variables inside macros so they don’t collide with the caller’s variables:

```velocity
## CORRECT: prefix macro variables to avoid collisions
#macro (renderCard $page)
  #set ($card_title = $page.metadata.title)
  #set ($card_link = $page.link)
  <div class="card">
    <a href="$card_link">$card_title</a>
  </div>
#end

#set ($title = "My Page Title")
<h1>$title</h1>          ## outputs "My Page Title"
#renderCard($someOtherPage)
<h1>$title</h1>          ## still "My Page Title"
```

> **Important**
>
> Velocity macros do not have their own scope. Every `#set` inside a macro writes directly to the same namespace as the calling Format. Use a naming prefix (e.g. `$card_`, `$nav_`) for variables inside macros to prevent accidental overwrites.

## #define vs #set

`#set` evaluates the right-hand side immediately and stores the result. `#define` captures a block of Velocity code that is evaluated later, each time the variable is referenced. This distinction matters when the surrounding context changes between definition and use.

```velocity
## #set evaluates immediately
#set ($count = 0)
#set ($message = "Count is $count")
#set ($count = 5)
$message  ## outputs: Count is 0
```

`#set` captured the value of `$count` at the moment of assignment. Changing `$count` afterward has no effect on `$message`.

```velocity
## #define evaluates lazily
#set ($count = 0)
#define ($message)Count is $count#end
#set ($count = 5)
$message  ## outputs: Count is 5
```

`#define` deferred evaluation until `$message` was referenced. At that point, `$count` was already 5.

This makes `#define` useful for creating reusable template blocks. Here is a simple example with an array:

```velocity
## Define a list item template
#define ($listItem)
  <li>$fruit</li>
#end

#set ($fruits = ["Apple", "Banana", "Cherry"])

<ul>
#foreach ($fruit in $fruits)
  $listItem  ## re-evaluates each time, picking up the current $fruit
#end
</ul>

## Output:
## <ul>
##   <li>Apple</li>
##   <li>Banana</li>
##   <li>Cherry</li>
## </ul>
```

The same pattern works well with Cascade query results:

```velocity
## Define a card template once, reuse it in the loop
#define ($card)
  <div class="card">
    <h3>$item.metadata.title</h3>
    <p>$item.metadata.description</p>
  </div>
#end

#set ($results = $_query.byContentType("page").execute())
#foreach ($item in $results)
  $card  ## evaluates fresh each iteration with the current $item
#end
```

In both cases the block is defined once but picks up the current loop variable on each iteration because `#define` defers evaluation until the variable is referenced. Use `#set` when you want a fixed snapshot of a value. Use `#define` when you want a reusable template block that should pick up the latest context.

> **Tip**
>
> A good mental model: `#set` is like taking a photo (captured once), while `#define` is like a live camera feed (always shows the current state when viewed).
