COS Documentation

Search HubSpot Designers Site

HubL Syntax Reference


The underlying syntax used in COS templates is referred to as HubSpot Markup Language or HubL. HubL syntax is heavily inspired by Django, Jinja, and Python. However it should be noted that not all jinja operations or functionality are fully supported.

Templates contain tokens and tags. Tokens are indicated by a curl brace syntax: {{ }} and are replaced with values when the template is processed. Tags control the logic of the template and provide support for various components.

Here is a simple template for a blog post page:

<!DOCTYPE html>
<html lang="en">
<title>{{ page_meta.html_title }}</title>
<h1>{{ }}</a></h1>
<div class="byline">posted by {{ content.blog_post_author.display_name }} at {{ content.publish_date_localized }}</div>
{{ content.post_body }}
<div class="topics">
{% for topic in content.topic_list %}
<a class="topic-link" href="{{ group.absolute_url }}/topic/{{ topic.slug }}">{{ }}</a>{% if not loop.last %},{% endif %} 
{% endfor %}

Notice the two types of delimeters. {% ... %} and {{ ... }}. The first one is used to execute statements such as for-loops or assigns values, the latter prints the result of the expression to the template.


The HubSpot COS populates the template context with variables that can be displayed via the curly brace syntax. In the above code sample, {{ content.post_body }}, {{standard_header_includes}} etc. are all examples of variables. The available variables varies based on what type of content you are editing. For a full reference of variables allowed in templates, see our reference document.

Note: Using tokens in unsupported content types may have unexpected results.

For example using email specific style tokens in CSS stylesheets applied to Site Pages or Landing Pages will not result in settings adjustments in the HubSpot content settings properly updating the live pages until the stylesheets are republished manually.

Common Tokens for Email

CAN-SPAM (Required for Email Templates)

All email templates must be validated as complying with CAN SPAM rules, otherwise you will see an error when you try to save your template as a Basic Email template. In order for your email to be validated, you must include the following tokens:

  • {{unsubscribe_link}}
  • {{unsubscribe_link_all}}
  • {{unsubscribe_anchor}}
  • {{unsubscribe_section}}
  • {{site_settings.company_street_address_1}}
  • {{site_settings.company_city }}
  • {{site_settings.company_state}}
  • {{site_settings.company_zip}}

Company Information from Settings

  • {{site_settings.company_name}} - Add company name from settings
  • {{site_settings.office_location_name}}- Add company office location name from settings
  • {{site_settings.company_street_address_1}} - Add company address line 1 from settings
  • {{site_settings.company_street_address_2}} - Add company address line 2 from settings
  • {{site_settings.company_city}} - Add company city from settings
  • {{ site_settings.company_state}} - Add company state from settings
  • {{site_settings.company_zip}} - Add company zip from settings

CAN-SPAM and company information requirements can exist either in the base template or in an inherited extension template.

Unsubscribe information {{unsubscribe_link}} - Add a simple link to the email preference management screen that can be used with copy of your choosing. {{unsubscribe_link_all}} - Add a simple unsubscribe link to all email types that can be used with copy of your choosing. This is a one click unsubscribe link. {{unsubscribe_anchor}} - Add a simple link to the email preference management screen with default anchor text set to "Unsubscribe". * ```{{unsubscribe_section}} - Add a standard unsubscribe section with default copy and anchor text. This will link to the email preferences screen.

Main editable content region (Required for HubSpot Email Templates)

All email templates must include an editable main body content module.

 {% content_attribute "email_body" %}
{% end_content_attribute %}

<!-- or -->

{{ content.email_body }}

You will also need to wrap your main body content with a properly formatted <div> so the preview in the email editor will function properly.

For instance:

<div data-hubspot-form-id="emailbody" class="hubspot-editable">
{% content_attribute "email_body" %}
{% end_content_attribute %} 

Note: You can replace{{default_email_body}} with any HTML copy you want to use as starter copy in emails made from your template.

CSS for Email Templates

For better compatibility with most email clients, templates should not be wider than 600 pixels and should avoid using these CSS rules: Floats Positioning * Background images

For more information on CSS compatibility with email clients, check out Email On Acid's great user forums

Basic Tokens for Landing Page Templates

Add Forms To Your Landing Page Templates

Include the following code to add a HubSpot form module to your template

{% form "form module name" label="Sample form" %}

Note: There is no need to wrap a form module in <form> tags. The form module will generate all of the necessary HTML and JavaScript it needs to function properly when a form is selected in the content editor. Before a form is selected, form modules display empty space in the template. If you would like to include a dummy form in your landing page template preview to display before a form is selected, you can use the sample code found here

You can add non-HubSpot forms to your template with HTML and JavaScript, but it will not be configured to submit to HubSpot Contacts unless you use the Contacts API. Contacts API documentation is found here

Required Tokens for Landing Page Templates

(Pages may not render appropriately and will not be tracked properly for analytics without these tokens)

  • {{standard_header_includes}} - Add standard header content such as JQuery and any additional header content from the Content Editor.
  • {{standard_footer_includes}} - Add standard footer content such as HubSpot tracking code and any additional footer content from the Content Editor

Recommended Tokens for Landing Page Templates

  • {% style %} - CSS attribute value is set in the Style Manager UI.
  • {{ content.html_title }} - Set to the page title from the Content Editor.
  • {{ content.meta_description }} - Set to the meta description from the Content Editor.

Favicon (Recommended for Landing Page Templates)

Use this code to properly set a favicon if one is set in site settings or use a default favicon

{% if site_settings.favicon_src %} <link rel="shortcut icon" href="{{ site_settings.favicon_src }}">{% endif %}

CSS for Landing Page Templates

  • You can create CSS documents in the template editor that can be included in your template with an HTML <style> tag or this token {{include_css('URL')}}

Use the {% style %} token to allow CSS attribute values to be set in the Style Manager UI. {% style %} will follow the hierarchy it is placed in just like normal CSS rules. If you want your styles to take precedence over Style Manager CSS rules,then set your CSS rules below {% style %}

Javascript for Landing Page Templates

Simple Template Include JS in the template using HTML <script> tags

Base Template with Extensions Include JS in the base template using HTML <script> tags if you want to be able to update it across all extensions easily. Alternatively, include your JS in an extension if it is specific to that template variation.

Include from file -{{include_javascript('URL')}}

Unique to Landing Page Variation - Include the {{standard_header_includes}}and {{standard_footer_includes}} tokens in your template and any JS typed into the content editor will be included on the unique page.

Using Modules

Modules are dynamic components that you can drop into a template. The end-user will then get a chance to configure the component for each page that they user creates. Some of our modules include: "text", "image", "rich_text", "form", and "menu"

A module is added to a page like so:

{% module_type "a_unique_name" label="Label that shows up in the editor", attr="val" %}

module_type is one of the module types listed in the full refrence of all modules.

Module name is the unique name of the module. If you change this module name in a template, all pages previously created will have their configuration for the module hidden and will show the default content. Make sure the name of your module is different from the supported modules' names.

The value of Label is replaced with the name of the module in the content editor UI. It can be the same as the Module Name.

Attr is replaced with an attribute name for that module type. Ex. An image module would have a src attribute.

Val is replaced with the value of the attribute that precedes it. Ex: An image URL.

Here are two quick examples:

{% rich_text "a_unique_name" html="This will be <b>default html</b>" %} {% image "an_image_widget_name" src="/default-image.png", width="100", height="200" %}

Module Names

Each element needs to be defined with a name. For example: {% text "Headline" %} or {% image "Logo" %}. Using the same name in multiple templates will allow the user to change templates and retain the associated content if that particular content should be swappable. However each module within a template must have a unique name.

{% module_type "Module Name" label="Label That Appears On Edit Page" %}

Labels: Labels are used to identify the editable content in the user interface of the content editor. These labels do not need to match the Module Names. They should be easy to understand for your Marketing users. Additionally the same label can be used across multiple modules regardless of the Module Names. If you are a developer creating templates for marketing end users, you will want to put a description of how you intend the field to be used.

{% module_type "module_name" label="<strong>Label That Appears On Edit Page</strong>" %}

Block style syntax: sometimes you need a little more room to put in the default content. So you can use the widget block syntax:

{% widget_block rich_text "some_unique_name" %}
{% widget_attribute "html" %}
Now I have lots of room to do a &lt;b>multi-line&lt;/b> value for the attribute.
I do not have to cram the default content into a string, and I can use "quotes" freely.
{% end_widget_attribute %}
{% end_widget_block %}

Global Modules

Global modules are modules that are linked across templates. When a user edits the content of a global module it is updated in all pages and emails using that module. This allows for simple reuse and edit from a single interface for commonly reused modules. Once a global module is created it can be used in coded template files just like any other module.

Note: Global modules must be created in the HubSpot Global Module manager before they can be made available in templates.

{% global_widget "Module_Name" label="Module Label" %}

Special module attributes

Module Wrappers

In order for some of our content editing user interface to work properly we wrap all widgets in a <div>. This can be disabled for your custom templates if required for a particular execution using no_wrapper=True as a parameter on the module.

Example: {% text "default value" label="Your Label", no_wrapper=True %}

Using Module Values with Logic

Sometimes instead of directly printing the value of a module to a page, you want to use it in template logic. Use the attribute export_to_template_context=True. The value of the widget will then be availble in the context variable {{ widget_data.module_name.attr_name }}


{% color "primary_color" export_to_template_context=True, color="#F7EEEE" %}
body { 
background-color: {{ widget_data.primary_color.color }}; 
h1 { 
color: {{ color_variant( widget_data.primary_color.color, -140) }}; 

Note: Modules and widgets are synonyms. We call them "Modules" in the UI and "widgets" in the template code. We apologize for the confusion, if we could wave a magic wand we would make all the names one thing.

Module Containers

Flexible columns are vertical columns in a template that allow the end user to insert a variety of modules of their choosing into the template while editing in the content editor.


Flexible Column with two rich text modules by default

{% widget_container "Flexible_column_name" has_rows=False, overrideable=False, label='Flexible Column', params_str="overrideable=True, label='Flexible Column' ", is_row=False %}
{% widget_block rich_text "module_name_1" overrideable=True, label='Rich text one' %}
{% widget_attribute "html" %}<h2>New Module</h2>
<p>Add content here.</p>
{% end_widget_attribute %}
{% end_widget_block %}
{% widget_block rich_text "module_name_2" overrideable=True, label='Rich text two' %}
{% widget_attribute "html" %}<h2>New Module</h2>
<p>Add content here.</p>
{% end_widget_attribute %}
{% end_widget_block %}
{% end_widget_container %} 

Including Common Headers or Footers

You can use includes to reference another template within a template. This can be used to duplicate content across templates that you wish to keep linked. This can be used in a similar fashion as global modules and global groups.

Use an include in the target template like this:

{% include "custom/category/folder/your_filename.html" %} 

You can use base and extension templates to create a set of templates that are all variations of one or more master templates. A section of a template can be extended to include multiple template variations of the base template. For example to add a two column and one column version of the same overall template your templates would include code like this.

For loops

Loop over each item in a sequence. For example, to display a list of items:

{% set fruits = ['apple', 'orange', 'banana'] %}
{% for fruit in fruits %}
<li>{{ fruit }}</li>
{% endfor %}

Inside of a for-loop block you can access some special variables:

loop.index The current iteration of the loop. (1 indexed)
loop.index0 The current iteration of the loop. (0 indexed)
loop.revindex The number of iterations from the end of the loop (1 indexed)
loop.revindex0 The number of iterations from the end of the loop (0 indexed)
loop.first True if first iteration.
loop.last True if last iteration.
loop.length The number of items in the sequence.
loop.cycle A helper function to cycle between a list of sequences. See the explanation below.
loop.depth Indicates how deep in deep in a recursive loop the rendering currently is. Starts at level 1
loop.depth0 Indicates how deep in deep in a recursive loop the rendering currently is. Starts at level 0

Within a for-loop, it’s possible to cycle among a list of strings/variables each time through the loop by using the special loop.cycle helper:

{% for row in rows %}
<li class="{{ loop.cycle('odd', 'even') }}">{{contact.row}}</li>
{% endfor %}

If statement

In the simplest form you can use it to test if a variable is defined, not empty or not false:

{% if users %}
{% for user in users %}
<li>{{ user.username|e }}</li>
{% endfor %}
{% endif %}

For multiple branches elif and else can be used like in Python. You can use more complex expressions there too:

{% if kenny.sick %}
Kenny is sick.
{% elif kenny.dead %}
You killed Kenny! You bastard!!!
{% else %} 
Kenny looks okay --- so far 
{% endif %}