• October 01, 2024

HubL Variables and Filters: Types, Mutation, and Guards

HubL Variables and Filters: Types, Mutation, and Guards
10:00

HubL exposes a wide range of data, but that data does not arrive in a single, consistent shape. Values may be strings, numbers, booleans, sequences, or dictionaries depending on their source, and a variable’s type can change as filters are applied. Variables may also exist without containing usable data, or disappear entirely depending on context and configuration.

With that in mind, we’ll break down the core variable types you’ll encounter, explain how filters can mutate both values and types, and introduce a small set of filters that help prevent missing or empty data from causing unexpected output.

HubL Variable Types

Strings

A string is a data type made up of letters, numbers, and symbols typically surrounded in quotes in code and rendered as plain text. The large majority of HubSpot's predefined variables, including dictionary values, are returned as a string.

Some variables in HubSpot's reference sheets are documented as enumerations, such as the email variables primary_font and secondary_font. Being labeled as enumeration means they come from a fixed set of predefined options. However, in HubL, the values of these variables resolve as strings and should be treated as such.
    {#- A single word variable expression that 
	outputs the local time zone as a string -#}
{{ local_time_zone }}

{#- A dictionary attribute variable expression that
	outputs the current page's base URL -#}
{{ content.absolute_url }}

{#- Setting your own string variable 
	using quotes to surround the value.
	The contents of the quotes is what is output on render. -#}
{% set string_var = "This is a string!" %}
{{ string_var }}

  
Output

America/Chicago

https://alyssawilie.com/blog/hubl-variables-and-filters

This is a string!

Booleans

A boolean variable returns as one of two values, true or false. Predefined variables of this type are normally used for checking statuses, determining content types, or if an element should be visible. 

    {# a boolean determining if the current page 
	has been archived (a status) #}
{{ content.archived }}

{# a boolean determining if the current page
	is the "all posts" page of a blog (a type) #}
{{ simple_list_page }}

{# a boolean determining if comments should show
	on a blog post (visibility) #}
{{ group.allow_comments }}

  

Numbers

Number type variables are able to be used in HubL mathematical equations. In some cases though you may have a number being returned as a string, such as values from multi-select fields, but filters like |int and |float can be used to convert them to the appropriate type for calculations.

Integer & Long

Whole numbers (positive, zero, negative) in HubL are represented using two closely related types: integer and long. Functionally, both behave the same way and can be used interchangeably in calculations, comparisons, loops, and filters.

In practice, HubL defaults to using long for most whole-number values, including custom variables, numerical identifiers, and mathematical results. The integer type appears primarily on certain predefined variables or when a value is changed to the integer type using the |int filter.

Internally, HubSpot defaults to long to avoid the size limitations of integer types, ensuring numeric values can grow safely without requiring special handling.

Because of this, long should be considered the standard whole-number type in HubL, with Integer acting as an explicitly defined variant rather than a distinct behavioral category.

Float

A float is a number with a decimal. While there are not any float type variables listed in HubSpot's references, it is commonly used for outputting currencies and fractional number data.

Datetime

Datetime is often a major source of confusion due to being able to receive two different formats of output:

An ISO-derived formatted datetime (YYYY-MM-DD HH:MM:SS) is usually based off the timezones set in your portal, with some exceptions, and the time only returns up to the second, in 24-hour military time.

Meanwhile, a variable returning a 13-digit unix timestamp uses UTC for its timezone, returns up to the millisecond in time, and technically is output as a long type value.

Although HubL outputs datetime variables as YYYY-MM-DD HH:MM:SS without a visible timezone offset, the value retains its timezone context internally. Time-based filters and functions operate on the underlying datetime object, not the formatted string, and normalize all values to UTC before evaluation, making both formats usable together.

Where things can become messy is variables that return a date, but as a string (such as content.publish_date_localized), not a datetime or a long unix timestamp. To use it in calculations one would need to convert it using |strtotime , properly formatted with the timezone supplied. Generally it is advised to only use these string dates for direct display, rather than any conversions or calculations.

HubL does not have an out-of-the-box way to render Datetimes based on a visitor's timezone or locale. This could be achieved by storing a contact's timezone/locale in a contact property but this requires 1. the visitor being a contact and 2. using the contact dict which prevents caching. Generally, JavaScript would be preferred for outputting localized Datetimes.
    	
{# an ISO formatted datetime object of the 
	current date and time, timezone based on
	portal settings. #}
{{ local_dt }}

{# a Unix timestamp of when the current page 
	was last updated, in UTC timezone #}
{{ content.updated }}

{# an ISO formatted datetime object of the
	current page's publish date, in UTC timezone #}
{{ content.publish_date }}

  
Output
2026-02-23 08:46:13
1771857000851
2026-02-23 14:30:00

Dictionaries

A dictionary, or dict, is a data-structure made up of keys and values contained within a single set of curly brackets. Certain keys within a dict can be accessed by appending a key name to the dictionary (e.g. contact) through dot notation (e.g. contact.name). Dictionary values can be of any data type including strings, booleans, numbers, dictionaries, and sequences.

Sequences

Sequences are an iterable list of items, contained within a single set of square brackets, and much like dictionaries can contain values of any data type. While some sequences may appear as an array (a static collection of one type of data), HubL dictates and acts on all sequences as a list (a dynamic collection of multiple types of data). You will see sequences commonly used when pulling data from multi-select fields, HubDB tables, and CRM Objects. If you can loop through it, it's a sequence.

Mutating HubL Variables with Filters

When outputting data with HubL, filters can be used to alter how values are rendered, everything from changing text case to stripping content or performing simple calculations. While many filters are used purely for display, others mutate the underlying value itself.

This distinction matters. Once a value has been mutated, it may no longer behave the way it originally did, which can quietly break conditionals, loops, or calculations further down the template.

Type Conversion Filters

Type conversion filters (int, float, string, bool) convert values into a new type and should be used early when preparing data for logic or calculation. Because the intent of these filters is explicit, their impact on downstream logic is predictable, making them safer to use than filters that mutate values implicitly. That said, changing a variable's type can still break logic that relies on its original form, so these conversions should be applied as close to the source as possible.

    {% set number_string = "12345" %}
{{ type(number_string) }} {# will return as a string #}
{% set number = number_string|int %}
{{ type(number) }} {# will return as an integer #}

  
Output
str
int

Inspecting Variable Types During Development

HubL provides the type() function to inspect how a value is being interpreted at render time. This is primarily a debugging tool and is most useful when a variable behaves differently than expected, particularly after filters have been applied. Keep in mind, type() reports the runtime type of a value as HubL sees it, not necessarily how it is documented in HubSpot's reference material.

Display-Oriented (Formatting) Filters

Display-oriented filters are typically used to control how a value is rendered rather than how it behaves in logic. While many operate on specific variable types, others normalize compatible values into a formatted string regardless of their original representation (e.g. format_datetime, format_number, wordwrap). This flexibility can make them convenient, but it also introduces uncertainty around both the required input type and the type ultimately returned.

Because these filters often rely on implicit assumptions, misuse does not always result in obvious errors. Some formatting filters perform little or no input validation and will fail silently when applied to incompatible or incomplete values. For example, filters such as cut and escape_html attempt to remove or modify portions of a value, but if the expected content does not exist, the filter simply returns the original value without warning. For this reason, display-oriented filters should be verified against the contexts in which the data is actually rendered.

    {# original set number returns as a float #}
{% set original_number = 5235.99 %}
{{ type(original_number) }}
{# the value changes to a string after formatting with a filter #}
{% set formatted_number = original_number|format_number('fr') %}
{{ type(formatted_number) }}

{# |cut will remove the specified text from a string,
	if that text does not exist in the string it will
	return the original string unaltered without error #}
{{ "Hello People"|cut('People') }}
{{ "Hello World"|cut('People') }}

  
Output
float
str

Hello
Hello World

Structural Transformation Filters

Structural transformation filters alter the shape of a value, which can fundamentally change how it behaves in loops, conditionals, and other logic. These filters extract, combine, or reorganize data from sequences and dictionaries (for example, first, join, unique), or derive structural information about a value (such as length).

Because these filters change how data can be iterated, accessed, or evaluated, they are typically applied to control iteration patterns or to drive logic based on the structure of the data rather than its display. Once a value has been structurally transformed, it should be treated as a different kind of input for any downstream logic.

    {# our initial variable renders as a list #}
{% set list = [1,2,3,4] %} 
{{ list }} : {{ type(list) }}

{# using |join our list will change to a String,
	this means it is no longer iterable
	unless re-split into a List #}
{% set list_string = list|join('|') %}
{{ list_string }} : {{ type(list_string) }}

{# using |first the first item of the list is extracted,
	the final output being a Long type value #}
{% set list_first = list|first %}
{{ list_first }} : {{ type(list_first) }}

  
Output
[1, 2, 3, 4] : list
1|2|3|4 : str
1 : long

Arithmetic Mutation Filters 

Arithmetic mutation filters perform calculations that produce a new value derived from one or more inputs. While these filters can be used either directly in output or earlier to drive logic, the result should be treated as a new value rather than a transformed version of the original data.

    {# returns a calculated Long number of days between 
	two Datetime objects #}
{{ content.created|between_times(local_dt, 'days') }}

{# returns the calculated sum of numbers in a List, 
	as a Long number #}
{{ [25, 3, 16, 37]|sum }}

{# returns the calculated result of one 
	number multiplied by the other #}
{{ 15|multiply(209) }}

  
Output
21
81
3135

Why Filter Order Matters

When applying multiple filters to a value, it’s important to remember that filters are evaluated left to right, with each filter operating on the output of the previous one. Because both the type and structure of a value can change at each step, filter order directly affects how the final value behaves.

As a general rule, filters that transform or calculate values should be applied before display-oriented filters. Display-oriented filters are best reserved for the final rendering step, while transformation and arithmetic filters should be ordered based on the intermediate types and structures required to reach the intended result.

    {% set items = ["apple", "banana", "pear"] %}

{# list is joined into a string and then returns 
	the character length of that string #}
{{ items|join(", ")|length }}

{# only the length of the list is returned since after 
	|length is applied there is no longer a list for |join
	to act on #}
{{ items|length|join(",") }}

  
Output
19
3

Defensive Value Handling in HubL

HubL frequently operates on data that may be undefined or empty, depending on context and configuration. Before values are looped over, filtered further, or rendered, it’s often necessary to establish that they exist and contain usable data.

Setting Defaults

The |default filter is one of the most effective tools for preventing broken logic in HubL. It allows you to supply a fallback value that will be used when a variable is undefined or, optionally, when it evaluates as falsy.

This is especially useful when working with sequences that may not be defined in all contexts. By applying |default([]), you ensure that an undefined value is replaced with an empty sequence, allowing loops and sequence-based filters to run safely.

Important: Some CRM object properties do not resolve to an empty or undefined value when data is unavailable. Instead, they may return a literal placeholder string such as CONTACT.FIRSTNAME. Because this value is a defined, non-empty string, filters like |default will not apply.

In these cases, additional checks are required to detect placeholder values before rendering or comparison.

Checking for Usability

The |length filter is commonly used to determine whether a value contains usable data. While empty sequences and strings evaluate as falsy in conditionals, |length converts the question of existence into an explicit numeric check.

This is particularly useful when working with values that may change shape or type through filtering. By evaluating a value's length, you avoid relying on truthiness rules and instead base logic on a numeric result.