> ## Documentation Index
> Fetch the complete documentation index at: https://www.courier.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

> ## Agent Instructions
> Install the Courier skill before writing code: `npx skills add trycourier/courier-skills`. It carries the verified SDK shapes and the rules you cannot get wrong.
> Authenticate every request with `Authorization: Bearer <API_KEY>`. A workspace has several environments and each has its own keys, which are plain `pk_` strings with no environment prefix. Start with Test.
> Send with `client.send.message`, the default import of the v7 Node SDK. Reference a template by its `nt_` id or its alias.
> A send accepts a bare Elemental element list, but storing content on a template requires the top-level elements wrapped in a channel element.
> Templates and journeys can be built in the Courier app or created through the API. Either way they live in the workspace and are referenced by ID when you send.
> The hosted MCP server is https://mcp.courier.com. For a briefing on what Courier is and when to use it, read https://www.courier.com/llms.txt.
> Prefer the Guides tab for how-do-I questions and the Docs tab for how-does-it-behave questions. The API reference lives under /api-reference.

# Elemental control flow

> Use if, loop, ref, and channels on any element to show, repeat, reuse, or target content.

export const Doc = ({href, children, name, bare}) => {
  const label = children || name || href;
  if (bare) {
    return <a href={href}>{label}</a>;
  }
  return <a className="cx-endpoint" data-kind="doc" href={href}>
      <span className="cx-endpoint-label">{label}</span>
      <span className="cx-endpoint-method">DOC</span>
    </a>;
};

Elemental has four control flow properties:

* **`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 four are optional, available on every element, and can be combined on the same element.

<Info>
  Control flow properties are evaluated at render time using Handlebars expressions. You can read `message.data`, `message.to.data`, and other message context.
</Info>

## If

The `if` property renders an element only when a Handlebars expression is truthy. Otherwise the element is 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**

<CodeGroup>
  ```json Elemental highlight={4} theme={null}
  {
    "type": "text",
    "content": "Welcome, premium member!",
    "if": "data.user_tier === 'premium'"
  }
  ```

  ```javascript Node.js highlight={12} theme={null}
  const { requestId } = await client.send.message({
    message: {
      to: {
        email: "sarah@acme-corp.com",
      },
      content: {
        version: "2022-01-01",
        elements: [
          {
            type: "text",
            content: "Welcome, premium member!",
            if: "data.user_tier === 'premium'",
          },
        ],
      },
    },
  });
  ```

  ```python Python highlight={12} theme={null}
  response = client.send.message(
      message={
        "to": {
          "email": "sarah@acme-corp.com",
        },
        "content": {
          "version": "2022-01-01",
          "elements": [
            {
              "type": "text",
              "content": "Welcome, premium member!",
              "if": "data.user_tier === 'premium'",
            },
          ],
        },
      },
  )
  ```

  ```bash cURL highlight={15} theme={null}
  curl -X POST https://api.courier.com/send \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
        "message": {
          "to": {
            "email": "sarah@acme-corp.com"
          },
          "content": {
            "version": "2022-01-01",
            "elements": [
              {
                "type": "text",
                "content": "Welcome, premium member!",
                "if": "data.user_tier === '\''premium'\''"
              }
            ]
          }
        }
      }'
  ```

  ```ruby Ruby highlight={12} theme={null}
  response = courier.send_.message(
    message: {
      to: {
        email: "sarah@acme-corp.com"
      },
      content: {
        version: "2022-01-01",
        elements: [
          {
            type: "text",
            content: "Welcome, premium member!",
            if: "data.user_tier === 'premium'"
          }
        ]
      }
    }
  )
  ```

  ```go Go highlight={8} theme={null}
  // Elemental nodes carry no typed content fields, so pass the document as raw JSON.
  content := param.Override[shared.ElementalContentParam](json.RawMessage(`{
    "version": "2022-01-01",
    "elements": [
      {
        "type": "text",
        "content": "Welcome, premium member!",
        "if": "data.user_tier === 'premium'"
      }
    ]
  }`))

  response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
  	Message: courier.SendMessageParamsMessage{
  		To: courier.SendMessageParamsMessageToUnion{
  			OfUserRecipient: &shared.UserRecipientParam{
  				Email: courier.String("sarah@acme-corp.com"),
  			},
  		},
  		Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content},
  	},
  })
  ```

  ```java Java highlight={10} theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com")))
          .content(JsonValue.from(java.util.Map.of(
                  "version", "2022-01-01",
                  "elements", java.util.List.of(
                    java.util.Map.of(
                      "type", "text",
                      "content", "Welcome, premium member!",
                      "if", "data.user_tier === 'premium'")))))
          .build())
      .build();
  SendMessageResponse response = client.send().message(params);
  ```

  ```php PHP highlight={12} theme={null}
  $response = $client->send->message(
    message: [
      'to' => [
        'email' => 'sarah@acme-corp.com',
      ],
      'content' => [
        'version' => '2022-01-01',
        'elements' => [
          [
            'type' => 'text',
            'content' => 'Welcome, premium member!',
            'if' => 'data.user_tier === \'premium\'',
          ],
        ],
      ],
    ],
  );
  ```

  ```csharp C# highlight={15} theme={null}
  // Elemental nodes expose no typed content fields, so build the document from raw JSON.
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          To = new UserRecipient { Email = "sarah@acme-corp.com" },
          Content = ElementalContent.FromRawUnchecked(
              JsonSerializer.Deserialize<Dictionary<string, JsonElement>>("""
              {
                "version": "2022-01-01",
                "elements": [
                  {
                    "type": "text",
                    "content": "Welcome, premium member!",
                    "if": "data.user_tier === 'premium'"
                  }
                ]
              }
              """)),
      },
  };

  var response = await client.Send.Message(parameters);
  ```

  ```bash CLI highlight={4} theme={null}
  courier send message \
    --api-key "$COURIER_API_KEY" \
    --message.to '{"email": "sarah@acme-corp.com"}' \
    --message.content '{"version": "2022-01-01", "elements": [{"type": "text", "content": "Welcome, premium member!", "if": "data.user_tier === '\''premium'\''"}]}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send an email to sarah@acme-corp.com and show the discount line only for premium users.
  ```
</CodeGroup>

**Realistic Examples**

**Conditional welcome message based on user type:**

```json theme={null}
{
  "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:**

```json theme={null}
{
  "type": "group",
  "if": "data.items && data.items.length > 0",
  "elements": [
    {
      "type": "text",
      "content": "Your order contains {{data.items.length}} item(s)"
    }
  ]
}
```

**Structured conditions**

The `if` field also accepts a **structured condition array**: one or more condition groups evaluated at render time. The element renders if **any** group matches, so groups are OR'd together. Within a group, conditions combine using the group's `logical_operator`.

**Supported property namespaces** (dot-paths only):

| Namespace   | Description                                | Example                     |
| ----------- | ------------------------------------------ | --------------------------- |
| `data.*`    | Values from the send `data` payload        | `data.order_total`          |
| `profile.*` | Recipient profile fields                   | `profile.plan`              |
| `refs.*`    | Visibility of another element by its `ref` | `refs.promo_banner.visible` |

**Supported operators:**

| Operator                                                                     | Type   | `value` required |
| ---------------------------------------------------------------------------- | ------ | ---------------- |
| `equals`, `not_equals`                                                       | binary | yes              |
| `greater_than`, `less_than`, `greater_than_or_equals`, `less_than_or_equals` | binary | yes              |
| `contains`, `not_contains`                                                   | binary | yes              |
| `is_empty`, `is_not_empty`                                                   | unary  | no               |

**Design Studio labels these operators in prose, and three do not match their JSON key.** The
editor builds the same object either way. This is the mapping:

| Editor reads   | JSON key                 |
| -------------- | ------------------------ |
| does not equal | `not_equals`             |
| is at least    | `greater_than_or_equals` |
| is at most     | `less_than_or_equals`    |

The other seven read as their key: equals, is greater than, is less than, contains, does not
contain, is empty, is not empty.

**There is no empty structured condition.** Delete a group's last condition and the group goes
with it. Delete the last group and the `if` field is removed rather than left as `[]`, so an
empty array is not the way to express "no conditions" and an element carrying one is malformed.

**Single group (all conditions AND'd):**

```json theme={null}
{
  "type": "text",
  "content": "You qualify for the enterprise plan.",
  "if": [
    {
      "logical_operator": "and",
      "conditions": [
        { "property": "data.account_type", "operator": "equals", "value": "enterprise" },
        { "property": "profile.email_verified", "operator": "equals", "value": "true" }
      ]
    }
  ]
}
```

**Multiple groups (OR between groups):**

```json theme={null}
{
  "type": "text",
  "content": "Upgrade available!",
  "if": [
    {
      "logical_operator": "and",
      "conditions": [{ "property": "data.plan", "operator": "equals", "value": "free" }]
    },
    {
      "logical_operator": "and",
      "conditions": [{ "property": "data.plan", "operator": "equals", "value": "starter" }]
    }
  ]
}
```

**Unary operator (no `value` needed):**

```json theme={null}
{
  "type": "text",
  "content": "No promo code applied.",
  "if": [
    {
      "logical_operator": "and",
      "conditions": [{ "property": "data.promo_code", "operator": "is_empty" }]
    }
  ]
}
```

**Using `refs` to check element visibility:**

```json theme={null}
{
  "type": "text",
  "content": "Here is what you unlocked:",
  "if": [
    {
      "logical_operator": "and",
      "conditions": [{ "property": "refs.promo_block.visible", "operator": "equals", "value": "true" }]
    }
  ]
}
```

<Info>
  String `if` and structured `if` are mutually exclusive on the same element. Use one or the other. String expressions remain fully supported.
</Info>

## Ref

The `ref` property names an element so other elements can reference it. A referenced element exposes its properties plus `visible`, which says whether it 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 can only reference elements defined earlier in the `elements` array.

**Basic Example**

<CodeGroup>
  ```json Elemental highlight={4,9} theme={null}
  {
    "type": "text",
    "content": "Hello, {{first_name}}",
    "ref": "greeting"
  },
  {
    "type": "text",
    "content": "This shows if greeting is visible",
    "if": "refs.greeting.visible"
  }
  ```

  ```javascript Node.js highlight={12,17} theme={null}
  const { requestId } = await client.send.message({
    message: {
      to: {
        email: "sarah@acme-corp.com",
      },
      content: {
        version: "2022-01-01",
        elements: [
          {
            type: "text",
            content: "Hello, {{first_name}}",
            ref: "greeting",
          },
          {
            type: "text",
            content: "This shows if greeting is visible",
            if: "refs.greeting.visible",
          },
        ],
      },
    },
  });
  ```

  ```python Python highlight={12,17} theme={null}
  response = client.send.message(
      message={
        "to": {
          "email": "sarah@acme-corp.com",
        },
        "content": {
          "version": "2022-01-01",
          "elements": [
            {
              "type": "text",
              "content": "Hello, {{first_name}}",
              "ref": "greeting",
            },
            {
              "type": "text",
              "content": "This shows if greeting is visible",
              "if": "refs.greeting.visible",
            },
          ],
        },
      },
  )
  ```

  ```bash cURL highlight={15,20} theme={null}
  curl -X POST https://api.courier.com/send \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
        "message": {
          "to": {
            "email": "sarah@acme-corp.com"
          },
          "content": {
            "version": "2022-01-01",
            "elements": [
              {
                "type": "text",
                "content": "Hello, {{first_name}}",
                "ref": "greeting"
              },
              {
                "type": "text",
                "content": "This shows if greeting is visible",
                "if": "refs.greeting.visible"
              }
            ]
          }
        }
      }'
  ```

  ```ruby Ruby highlight={12,17} theme={null}
  response = courier.send_.message(
    message: {
      to: {
        email: "sarah@acme-corp.com"
      },
      content: {
        version: "2022-01-01",
        elements: [
          {
            type: "text",
            content: "Hello, {{first_name}}",
            ref: "greeting"
          },
          {
            type: "text",
            content: "This shows if greeting is visible",
            if: "refs.greeting.visible"
          }
        ]
      }
    }
  )
  ```

  ```go Go highlight={8,13} theme={null}
  // Elemental nodes carry no typed content fields, so pass the document as raw JSON.
  content := param.Override[shared.ElementalContentParam](json.RawMessage(`{
    "version": "2022-01-01",
    "elements": [
      {
        "type": "text",
        "content": "Hello, {{first_name}}",
        "ref": "greeting"
      },
      {
        "type": "text",
        "content": "This shows if greeting is visible",
        "if": "refs.greeting.visible"
      }
    ]
  }`))

  response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
  	Message: courier.SendMessageParamsMessage{
  		To: courier.SendMessageParamsMessageToUnion{
  			OfUserRecipient: &shared.UserRecipientParam{
  				Email: courier.String("sarah@acme-corp.com"),
  			},
  		},
  		Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content},
  	},
  })
  ```

  ```java Java highlight={10,14} theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com")))
          .content(JsonValue.from(java.util.Map.of(
                  "version", "2022-01-01",
                  "elements", java.util.List.of(
                    java.util.Map.of(
                      "type", "text",
                      "content", "Hello, {{first_name}}",
                      "ref", "greeting"),
                    java.util.Map.of(
                      "type", "text",
                      "content", "This shows if greeting is visible",
                      "if", "refs.greeting.visible")))))
          .build())
      .build();
  SendMessageResponse response = client.send().message(params);
  ```

  ```php PHP highlight={12,17} theme={null}
  $response = $client->send->message(
    message: [
      'to' => [
        'email' => 'sarah@acme-corp.com',
      ],
      'content' => [
        'version' => '2022-01-01',
        'elements' => [
          [
            'type' => 'text',
            'content' => 'Hello, {{first_name}}',
            'ref' => 'greeting',
          ],
          [
            'type' => 'text',
            'content' => 'This shows if greeting is visible',
            'if' => 'refs.greeting.visible',
          ],
        ],
      ],
    ],
  );
  ```

  ```csharp C# highlight={15,20} theme={null}
  // Elemental nodes expose no typed content fields, so build the document from raw JSON.
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          To = new UserRecipient { Email = "sarah@acme-corp.com" },
          Content = ElementalContent.FromRawUnchecked(
              JsonSerializer.Deserialize<Dictionary<string, JsonElement>>("""
              {
                "version": "2022-01-01",
                "elements": [
                  {
                    "type": "text",
                    "content": "Hello, {{first_name}}",
                    "ref": "greeting"
                  },
                  {
                    "type": "text",
                    "content": "This shows if greeting is visible",
                    "if": "refs.greeting.visible"
                  }
                ]
              }
              """)),
      },
  };

  var response = await client.Send.Message(parameters);
  ```

  ```bash CLI highlight={4} theme={null}
  courier send message \
    --api-key "$COURIER_API_KEY" \
    --message.to '{"email": "sarah@acme-corp.com"}' \
    --message.content '{"version": "2022-01-01", "elements": [{"type": "text", "content": "Hello, {{first_name}}", "ref": "greeting"}, {"type": "text", "content": "This shows if greeting is visible", "if": "refs.greeting.visible"}]}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send an email to sarah@acme-corp.com that reuses an earlier element by reference.
  ```
</CodeGroup>

**Realistic Example**

**Show follow-up content only if initial element is visible:**

```json theme={null}
{
  "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**

<CodeGroup>
  ```json Elemental highlight={4} theme={null}
  {
    "type": "text",
    "content": "* {{$.item.name}} - {{$.item.price}}",
    "loop": "data.products"
  }
  ```

  ```javascript Node.js highlight={12} theme={null}
  const { requestId } = await client.send.message({
    message: {
      to: {
        email: "sarah@acme-corp.com",
      },
      content: {
        version: "2022-01-01",
        elements: [
          {
            type: "text",
            content: "* {{$.item.name}} - {{$.item.price}}",
            loop: "data.products",
          },
        ],
      },
      data: {
        products: [
          { name: "Notebook", price: 12.5 },
          { name: "Pen", price: 3 },
        ],
      },
    },
  });
  ```

  ```python Python highlight={12} theme={null}
  response = client.send.message(
      message={
        "to": {
          "email": "sarah@acme-corp.com",
        },
        "content": {
          "version": "2022-01-01",
          "elements": [
            {
              "type": "text",
              "content": "* {{$.item.name}} - {{$.item.price}}",
              "loop": "data.products",
            },
          ],
        },
        "data": {
          "products": [
            {"name": "Notebook", "price": 12.5},
            {"name": "Pen", "price": 3},
          ],
        },
      },
  )
  ```

  ```bash cURL highlight={15} theme={null}
  curl -X POST https://api.courier.com/send \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
        "message": {
          "to": {
            "email": "sarah@acme-corp.com"
          },
          "content": {
            "version": "2022-01-01",
            "elements": [
              {
                "type": "text",
                "content": "* {{$.item.name}} - {{$.item.price}}",
                "loop": "data.products"
              }
            ]
          },
          "data": {
            "products": [
              { "name": "Notebook", "price": 12.5 },
              { "name": "Pen", "price": 3 }
            ]
          }
        }
      }'
  ```

  ```ruby Ruby highlight={12} theme={null}
  response = courier.send_.message(
    message: {
      to: {
        email: "sarah@acme-corp.com"
      },
      content: {
        version: "2022-01-01",
        elements: [
          {
            type: "text",
            content: "* {{$.item.name}} - {{$.item.price}}",
            loop: "data.products"
          }
        ]
      },
      data: {
        products: [
          { name: "Notebook", price: 12.5 },
          { name: "Pen", price: 3 }
        ]
      }
    }
  )
  ```

  ```go Go highlight={8} theme={null}
  // Elemental nodes carry no typed content fields, so pass the document as raw JSON.
  content := param.Override[shared.ElementalContentParam](json.RawMessage(`{
    "version": "2022-01-01",
    "elements": [
      {
        "type": "text",
        "content": "* {{$.item.name}} - {{$.item.price}}",
        "loop": "data.products"
      }
    ]
  }`))

  response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
  	Message: courier.SendMessageParamsMessage{
  		To: courier.SendMessageParamsMessageToUnion{
  			OfUserRecipient: &shared.UserRecipientParam{
  				Email: courier.String("sarah@acme-corp.com"),
  			},
  		},
  		Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content},
  		Data: map[string]any{
  			"products": []any{
  				map[string]any{"name": "Notebook", "price": 12.5},
  				map[string]any{"name": "Pen", "price": 3},
  			},
  		},
  	},
  })
  ```

  ```java Java highlight={10} theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com")))
          .content(JsonValue.from(java.util.Map.of(
                  "version", "2022-01-01",
                  "elements", java.util.List.of(
                    java.util.Map.of(
                      "type", "text",
                      "content", "* {{$.item.name}} - {{$.item.price}}",
                      "loop", "data.products")))))
          .data(JsonValue.from(java.util.Map.of(
                  "products", java.util.List.of(
                    java.util.Map.of("name", "Notebook", "price", 12.5),
                    java.util.Map.of("name", "Pen", "price", 3)))))
          .build())
      .build();
  SendMessageResponse response = client.send().message(params);
  ```

  ```php PHP highlight={12} theme={null}
  $response = $client->send->message(
    message: [
      'to' => [
        'email' => 'sarah@acme-corp.com',
      ],
      'content' => [
        'version' => '2022-01-01',
        'elements' => [
          [
            'type' => 'text',
            'content' => '* {{$.item.name}} - {{$.item.price}}',
            'loop' => 'data.products',
          ],
        ],
      ],
      'data' => [
        'products' => [
          ['name' => 'Notebook', 'price' => 12.5],
          ['name' => 'Pen', 'price' => 3],
        ],
      ],
    ],
  );
  ```

  ```csharp C# highlight={15} theme={null}
  // Elemental nodes expose no typed content fields, so build the document from raw JSON.
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          To = new UserRecipient { Email = "sarah@acme-corp.com" },
          Content = ElementalContent.FromRawUnchecked(
              JsonSerializer.Deserialize<Dictionary<string, JsonElement>>("""
              {
                "version": "2022-01-01",
                "elements": [
                  {
                    "type": "text",
                    "content": "* {{$.item.name}} - {{$.item.price}}",
                    "loop": "data.products"
                  }
                ]
              }
              """)),
          Data = new Dictionary<string, JsonElement>()
          {
              { "products", JsonSerializer.SerializeToElement(new[]
                  {
                      new { name = "Notebook", price = 12.5 },
                      new { name = "Pen", price = 3 },
                  })
              },
          },
      },
  };

  var response = await client.Send.Message(parameters);
  ```

  ```bash CLI highlight={4} theme={null}
  courier send message \
    --api-key "$COURIER_API_KEY" \
    --message.to '{"email": "sarah@acme-corp.com"}' \
    --message.content '{"version": "2022-01-01", "elements": [{"type": "text", "content": "* {{$.item.name}} - {{$.item.price}}", "loop": "data.products"}]}' \
    --message.data '{"products": [{"name": "Notebook", "price": 12.5}, {"name": "Pen", "price": 3}]}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send an email to sarah@acme-corp.com listing the two products in data.products, one line per item.
  ```
</CodeGroup>

**Realistic Examples**

**Product list with nested data:**

```json theme={null}
{
  "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:**

<Tip>
  To show a 1-based item number, pass `$.index` to the `add` <Doc href="/docs/design/templates/variables">Handlebars helper</Doc>:

  ```handlebars theme={null}
  Item {{add $.index 1}}: {{$.item.name}}
  ```

  This outputs "Item 1", "Item 2", and so on, instead of starting at zero.
</Tip>

```json theme={null}
{
  "type": "text",
  "content": "Item {{add $.index 1}}: {{$.item.name}} - {{$.item.price}}",
  "loop": "data.products"
}
```

## Channels

The `channels` property renders an element only on the channels you list. Use it to show different content on 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.

<Info>
  For a fully different content structure per channel, use <Doc href="/docs/design/elemental/elements/channel">Channel elements</Doc>.
</Info>

**Basic Example**

<CodeGroup>
  ```json Elemental highlight={4} theme={null}
  {
    "type": "text",
    "content": "This only appears in email and push",
    "channels": ["email", "push"]
  }
  ```

  ```javascript Node.js highlight={12} theme={null}
  const { requestId } = await client.send.message({
    message: {
      to: {
        email: "sarah@acme-corp.com",
      },
      content: {
        version: "2022-01-01",
        elements: [
          {
            type: "text",
            content: "This only appears in email and push",
            channels: [
              "email",
              "push",
            ],
          },
        ],
      },
    },
  });
  ```

  ```python Python highlight={12} theme={null}
  response = client.send.message(
      message={
        "to": {
          "email": "sarah@acme-corp.com",
        },
        "content": {
          "version": "2022-01-01",
          "elements": [
            {
              "type": "text",
              "content": "This only appears in email and push",
              "channels": [
                "email",
                "push",
              ],
            },
          ],
        },
      },
  )
  ```

  ```bash cURL highlight={15} theme={null}
  curl -X POST https://api.courier.com/send \
    -H "Authorization: Bearer $COURIER_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
        "message": {
          "to": {
            "email": "sarah@acme-corp.com"
          },
          "content": {
            "version": "2022-01-01",
            "elements": [
              {
                "type": "text",
                "content": "This only appears in email and push",
                "channels": [
                  "email",
                  "push"
                ]
              }
            ]
          }
        }
      }'
  ```

  ```ruby Ruby highlight={12} theme={null}
  response = courier.send_.message(
    message: {
      to: {
        email: "sarah@acme-corp.com"
      },
      content: {
        version: "2022-01-01",
        elements: [
          {
            type: "text",
            content: "This only appears in email and push",
            channels: [
              "email",
              "push"
            ]
          }
        ]
      }
    }
  )
  ```

  ```go Go highlight={8} theme={null}
  // Elemental nodes carry no typed content fields, so pass the document as raw JSON.
  content := param.Override[shared.ElementalContentParam](json.RawMessage(`{
    "version": "2022-01-01",
    "elements": [
      {
        "type": "text",
        "content": "This only appears in email and push",
        "channels": [
          "email",
          "push"
        ]
      }
    ]
  }`))

  response, err := client.Send.Message(context.TODO(), courier.SendMessageParams{
  	Message: courier.SendMessageParamsMessage{
  		To: courier.SendMessageParamsMessageToUnion{
  			OfUserRecipient: &shared.UserRecipientParam{
  				Email: courier.String("sarah@acme-corp.com"),
  			},
  		},
  		Content: courier.SendMessageParamsMessageContentUnion{OfElementalContent: &content},
  	},
  })
  ```

  ```java Java highlight={10} theme={null}
  SendMessageParams params = SendMessageParams.builder()
      .message(SendMessageParams.Message.builder()
          .to(JsonValue.from(java.util.Map.of("email", "sarah@acme-corp.com")))
          .content(JsonValue.from(java.util.Map.of(
                  "version", "2022-01-01",
                  "elements", java.util.List.of(
                    java.util.Map.of(
                      "type", "text",
                      "content", "This only appears in email and push",
                      "channels", java.util.List.of(
                        "email",
                        "push"))))))
          .build())
      .build();
  SendMessageResponse response = client.send().message(params);
  ```

  ```php PHP highlight={12} theme={null}
  $response = $client->send->message(
    message: [
      'to' => [
        'email' => 'sarah@acme-corp.com',
      ],
      'content' => [
        'version' => '2022-01-01',
        'elements' => [
          [
            'type' => 'text',
            'content' => 'This only appears in email and push',
            'channels' => [
              'email',
              'push',
            ],
          ],
        ],
      ],
    ],
  );
  ```

  ```csharp C# highlight={15} theme={null}
  // Elemental nodes expose no typed content fields, so build the document from raw JSON.
  SendMessageParams parameters = new()
  {
      Message = new()
      {
          To = new UserRecipient { Email = "sarah@acme-corp.com" },
          Content = ElementalContent.FromRawUnchecked(
              JsonSerializer.Deserialize<Dictionary<string, JsonElement>>("""
              {
                "version": "2022-01-01",
                "elements": [
                  {
                    "type": "text",
                    "content": "This only appears in email and push",
                    "channels": [
                      "email",
                      "push"
                    ]
                  }
                ]
              }
              """)),
      },
  };

  var response = await client.Send.Message(parameters);
  ```

  ```bash CLI highlight={4} theme={null}
  courier send message \
    --api-key "$COURIER_API_KEY" \
    --message.to '{"email": "sarah@acme-corp.com"}' \
    --message.content '{"version": "2022-01-01", "elements": [{"type": "text", "content": "This only appears in email and push", "channels": ["email", "push"]}]}'
  ```

  ```text MCP theme={null}
  With Courier MCP, send an email and a push to sarah@acme-corp.com, showing the footer on email only.
  ```
</CodeGroup>

**Realistic Examples**

**Channel-specific content:**

```json theme={null}
{
  "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:**

```json theme={null}
{
  "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:

```json theme={null}
{
  "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 <Doc href="/docs/design/elemental/locales">localization</Doc> for fully dynamic, multi-language notifications
* **Channel considerations**: some elements (like columns) may not render well on all channels
