Skip to main content

Overview

Courier Elemental supports four control flow properties that enable dynamic, data-driven notification templates:
  • if - Conditionally render elements based on data or conditions
  • loop - Repeat elements for each item in an array
  • ref - Reference elements to check their visibility or properties
  • channels - Show elements only on specific channels
All control flow properties are optional and can be combined on the same element. These properties are available on all Elemental elements.
Control flow properties are evaluated at render time using Handlebars expressions. You can access data from message.data, message.to.data, and other message context.

If

The if property conditionally renders an element based on a Handlebars expression. If the expression evaluates to a truthy value, the element is rendered; otherwise, it’s skipped. When to use:
  • Show different content based on user type, subscription status, or feature flags
  • Display elements only when certain data exists
  • Create personalized experiences based on user context
  • Hide elements that aren’t relevant to the current recipient
Applies to: All Elemental elements

Basic Example

{
  "type": "text",
  "content": "Welcome, premium member!",
  "if": "data.user_tier === 'premium'"
}

Realistic Examples

Conditional welcome message based on user type:
{
  "version": "2022-01-01",
  "elements": [
    {
      "type": "text",
      "content": "Welcome back, {{first_name}}!",
      "if": "data.is_returning_user"
    },
    {
      "type": "text",
      "content": "Welcome to {{company_name}}, {{first_name}}!",
      "if": "!data.is_returning_user"
    },
    {
      "type": "action",
      "content": "Upgrade to Premium",
      "href": "https://example.com/upgrade",
      "if": "data.user_tier !== 'premium'"
    }
  ]
}
Show elements only when data exists:
{
  "type": "group",
  "if": "data.items && data.items.length > 0",
  "elements": [
    {
      "type": "text",
      "content": "Your order contains {{data.items.length}} item(s)"
    }
  ]
}

Ref

The ref property names an element so it can be referenced by other elements. Referenced elements provide access to their properties and a visible property that indicates whether the element was rendered. When to use:
  • Check if another element was rendered before showing related content
  • Create dependencies between elements
  • Build complex conditional logic based on element visibility
  • Access element properties from other elements
Applies to: All Elemental elements Note: An element cannot reference another element that is defined after it in the elements array. References must point to elements that appear earlier in the template.

Basic Example

{
  "type": "text",
  "content": "Hello, {{first_name}}",
  "ref": "greeting"
},
{
  "type": "text",
  "content": "This shows if greeting is visible",
  "if": "refs.greeting.visible"
}

Realistic Example

Show follow-up content only if initial element is visible:
{
  "version": "2022-01-01",
  "elements": [
    {
      "type": "text",
      "content": "Special offer for premium members!",
      "ref": "premium_offer",
      "if": "data.user_tier === 'premium'"
    },
    {
      "type": "action",
      "content": "Claim Offer",
      "href": "https://example.com/claim",
      "if": "refs.premium_offer.visible"
    }
  ]
}

Loop

The loop property renders an element multiple times, once for each item in an iterable data source (typically an array). When to use:
  • Display lists of products, orders, notifications, or other array data
  • Create dynamic content that adapts to variable-length data
  • Build repeating patterns like product cards or notification items
  • Iterate over nested data structures
Applies to: All Elemental elements Loop variables:
  • $.item - The current item in the iteration
  • $.index - The zero-based index of the current iteration

Basic Example

{
  "type": "text",
  "content": "* {{$.item.name}} - {{$.item.price}}",
  "loop": "data.products"
}

Realistic Examples

Product list with nested data:
{
  "version": "2022-01-01",
  "elements": [
    {
      "type": "group",
      "loop": "data.products",
      "elements": [
        {
          "type": "text",
          "content": "{{$.item.name}}",
          "text_style": "h2"
        },
        {
          "type": "text",
          "content": "Price: {{$.item.price}}"
        },
        {
          "type": "image",
          "src": "{{$.item.image_url}}",
          "alt_text": "{{$.item.name}}"
        },
        {
          "type": "action",
          "content": "View Details",
          "href": "https://example.com/products/{{$.item.id}}"
        },
        {
          "type": "divider"
        }
      ]
    }
  ]
}
Using $.index for item numbering:
When using $.index in loops, you can display a 1-based item number by using the add Handlebars helper:
Item {{add $.index 1}}: {{$.item.name}}
This will output “Item 1”, “Item 2”, etc., instead of starting from zero.
{
  "type": "text",
  "content": "Item {{add $.index 1}}: {{$.item.name}} - {{$.item.price}}",
  "loop": "data.products"
}

Channels

The channels property selectively renders an element based on the current notification channel. This allows you to show different content for email, SMS, push, and other channels. When to use:
  • Show detailed content in email, concise content in SMS
  • Display channel-specific formatting or elements
  • Customize content per channel while maintaining a single template
  • Hide elements that don’t work well on certain channels
Applies to: All Elemental elements Valid channels: email, push, direct_message, sms, or provider-specific channels like slack, discord, etc.
For more advanced channel customization, you can also use Channel elements to define completely different content structures per channel.

Basic Example

{
  "type": "text",
  "content": "This only appears in email and push",
  "channels": ["email", "push"]
}

Realistic Examples

Channel-specific content:
{
  "version": "2022-01-01",
  "elements": [
    {
      "type": "text",
      "content": "Your order #{{order_number}} has shipped! Track it here: {{tracking_url}}",
      "channels": ["email"]
    },
    {
      "type": "text",
      "content": "Order {{order_number}} shipped. Track: {{tracking_url}}",
      "channels": ["sms"]
    },
    {
      "type": "text",
      "content": "Your order has shipped!",
      "channels": ["push"]
    }
  ]
}
Hide complex elements on SMS:
{
  "type": "columns",
  "channels": ["email"],
  "elements": [
    {
      "type": "column",
      "width": "50%",
      "elements": [
        {
          "type": "image",
          "src": "{{product_image}}"
        }
      ]
    },
    {
      "type": "column",
      "width": "50%",
      "elements": [
        {
          "type": "text",
          "content": "{{product_name}}"
        }
      ]
    }
  ]
}

Combining Control Flow Properties

You can combine multiple control flow properties on the same element:
{
  "type": "text",
  "content": "Premium feature available!",
  "if": "data.user_tier === 'premium'",
  "channels": ["email", "push"],
  "ref": "premium_notice"
}
Evaluation order:
  1. channels - Element must match current channel
  2. if - Condition must evaluate to truthy
  3. loop - Element is repeated for each item (if present)
  4. ref - Element is registered for reference (if present)

Best Practices

  • Use if for conditional content: Show/hide elements based on data or user context
  • Use loop with group: Wrap looped elements in a group for better organization
  • Reference order matters: Elements must be defined before they’re referenced
  • Test with real data: Control flow expressions are evaluated at render time, so test with realistic data structures
  • Combine with locales: Use control flow with localization for fully dynamic, multi-language notifications
  • Channel considerations: Remember that some elements (like columns) may not render well on all channels