Keys are the bridge between your data and your templates. They enable you to access and display dynamic content, from simple values to complex nested structures. This chapter covers everything you need to know about working with Liquid keys effectively.

What are Liquid keys?

Keys are the most fundamental element of Liquid templates, serving as the connection points between your template and the data that populates it. When you wrap a key in double curly braces {{ }}, Liquid replaces it with the actual value during rendering. Think of keys as variables that point to data passed into your template.

Keys can reference different data types: strings, numbers, booleans, objects, and arrays. In the context of notifications, keys typically represent user attributes, event data, or system values that need to be displayed.

There are three types of keys:

  • Simple keys.
  • Nested keys.
  • Arrays.

Understanding how to work with each type unlocks the full power of Liquid templating.

Simple (or top-level) keys

Simple keys are the most basic way to access data in Liquid. When you write {{ name }} in your template, Liquid looks for a value called "name" in your data and outputs it into the template. These are called "top-level" keys because they sit at the main level of your data, not nested inside other structures.

Data
{
  "name": "Sarah",
  "order_number": "12345",
  "status": "shipped",
  "total": 89.99
}
Template
Hello {{ name }}!
Order #{{ order_number }}
Status: {{ status }}
Total: ${{ total }}
Output
Hello Sarah!
Order #12345
Status: shipped
Total: $89.99

When a key doesn't exist or has a null value, Liquid outputs nothing—no error, just empty space. This forgiving behavior prevents templates from breaking when optional data is missing.

Data
{
  "first_name": "Alex"
}
Template
Welcome {{ first_name }} {{ last_name }}!
Output
Welcome Alex !

Nested keys with dot notation

Real-world data rarely stays flat. Liquid uses dot notation to traverse nested objects, similar to JavaScript property access. In the example below, information like name and address are stored on the customer object, and the address itself is another object with its own properties.

To access these nested keys, you can use a dot, or period character ( . ), to tell Liquid that you want to access the values stored on one of those nested properties.

Data
{
  "customer": {
    "name": "Jamie Chen",
    "address": {
      "street": "123 Main St",
      "city": "Seattle",
      "state": "WA",
      "zip": "98101"
    }
  }
}
Template
Ship to:
{{ customer.name }}
{{ customer.address.street }}
{{ customer.address.city }}, {{ customer.address.state }} {{ customer.address.zip }}
Output
Ship to:
Jamie Chen
123 Main St
Seattle, WA 98101

Dot notation chains can go as deep as your data structure requires, where each “dot” accesses a deeper level of the structure. If any part of the chain doesn’t exist, Liquid gracefully handles these missing intermediate objects and won’t throw a runtime error. Instead, it silently returns nil, which renders as an empty string.

<!-- Returns empty string if user or preferences don't exist -->
{{ user.preferences.notifications.email }}

Working with arrays and lists

Arrays are lists of items, like a shopping list or a roster of names. In Liquid, you can't just output the entire array at once. Instead, you work with individual items. You can select a specific item by its position (also known as an index), check how many items are in the list using size, or use shortcuts like first and last to get the items at the beginning and end of the list.

Accessing array elements by index

To access a specific item in an array, you use square brackets [] with a number inside. Arrays start counting from zero (not one), so the first item is [0], the second is [1], and so on. You can also count backwards from the end using negative numbers—[-1] gives you the last item, [-2] gives you the second-to-last, and so forth.

Data
{
  "items": [
    {
      "name": "Laptop",
      "price": 999
    },
    {
      "name": "Mouse",
      "price": 29
    },
    {
      "name": "Keyboard",
      "price": 79
    }
  ]
}
Template
First item: {{ items[0].name }}
Second item: {{ items[1].name }}
Last item: {{ items[-1].name }}
Output
First item: Laptop
Second item: Mouse
Last item: Keyboard

You can also find out how many items are in an array using the size property:

Data
{
  "cart": {
    "items": [
      {
        "name": "Laptop",
        "price": 999
      },
      {
        "name": "Mouse",
        "price": 29
      }
    ]
  }
}
Template
You have {{ cart.items.size }} items in your cart
Output
You have 2 items in your cart

For accessing the first or last element of an array, you can use either index notation or the convenience properties first and last:

Data
{
  "products": [
    {
      "name": "Widget A",
      "stock": 50
    },
    {
      "name": "Widget B",
      "stock": 30
    },
    {
      "name": "Widget C",
      "stock": 20
    }
  ]
}
Template
First product: {{ products.first.name }}
Last product: {{ products.last.name }}
Output
First product: Widget A
Last product: Widget C

Complex data structures

Real applications often combine nested objects with arrays, creating complex hierarchies. Liquid handles these naturally by combining dot notation with array indexing.

Data
{
  "company": {
    "name": "Acme Corp",
    "departments": [
      {
        "name": "Engineering",
        "manager": {
          "name": "Sarah Kim",
          "email": "[email protected]"
        },
        "members": [
          {
            "name": "Alex Rivera",
            "role": "Senior Developer",
            "projects": [
              {
                "name": "API Redesign",
                "status": "In Progress"
              },
              {
                "name": "Performance Optimization",
                "status": "Completed"
              }
            ]
          },
          {
            "name": "Jordan Lee",
            "role": "Frontend Developer",
            "projects": [
              {
                "name": "UI Refresh",
                "status": "Planning"
              }
            ]
          }
        ]
      }
    ]
  }
}
Template
Company: {{ company.name }}
Department: {{ company.departments[0].name }}
Manager: {{ company.departments[0].manager.name }}

First team member: {{ company.departments[0].members[0].name }}
Their role: {{ company.departments[0].members[0].role }}
Their latest project: {{ company.departments[0].members[0].projects.first.name }}

Second team member: {{ company.departments[0].members[1].name }}
Their current project: {{ company.departments[0].members[1].projects[0].name }}
Output
Company: Acme Corp
Department: Engineering
Manager: Sarah Kim

First team member: Alex Rivera
Their role: Senior Developer
Their latest project: API Redesign

Second team member: Jordan Lee
Their current project: UI Refresh

Key naming conventions

While Liquid is flexible with key names, following conventions improves template readability:

<!-- Good: Clear, descriptive names -->
{{ user.email_address }}
{{ order.total_amount }}
{{ product.is_available }}
 
<!-- Avoid: Ambiguous or overly abbreviated -->
{{ u.e }}
{{ ord.amt }}
{{ p.avail }}

For multi-word keys, use snake_case (words separated by underscores and written in all lowercase letters), as it's the most common convention in Liquid templates. This consistency helps when working with data from various sources.

Defensive templating with keys

When working with keys in notification templates, defensive programming is crucial. Users might have incomplete profiles, optional fields might be empty, or data structures might vary. Liquid provides several ways to handle these scenarios:

<!-- Check if key exists before using -->
{% if user.address %}
  Shipping to: {{ user.address.city }}
{% else %}
  No shipping address on file
{% endif %}
 
<!-- Provide fallback defaults for missing values -->
{{ product.name | default: "Unknown Product" }}
 
<!-- Use if/then conditions to provide fallback values for empty arrays -->
{% if order.items.size > 0 %}
  {% for item in order.items %}
    {{ item.name }}
  {% endfor %}
{% else %}
  No items in order
{% endif %}

Performance considerations

While Liquid handles missing keys gracefully, deeply nested structures can impact template rendering performance. Consider these practices:

  1. Flatten deeply nested data in your application layer when possible.
  2. Use variables to store frequently accessed nested values.
  3. Limit array iterations for large datasets.
<!-- Instead of repeating deep access... -->
{% for item in order.items %}
  {{ user.preferences.display.currency }} {{ item.price }}
{% endfor %}
 
<!-- ...store it in a variable -->
{% assign currency = user.preferences.display.currency %}
{% for item in order.items %}
  {{ currency }} {{ item.price }}
{% endfor %}

Keys are fundamental to Liquid's power and flexibility. Master these patterns, and you'll write templates that are both powerful and maintainable, gracefully handling whatever data structures your application requires.