---
title: "Variables and Liquid in Workflow Transactional Email"
description: "Pass Shopify Flow data into email subjects, bodies and HTTP requests with Liquid. Full reference for variables, secrets, shop values, loops and conditionals."
canonical: "https://crm-tool.k8s-gcp-dev.eu.codecreationlabs.cloud/h/flow-transactional-email/variables-and-liquid"
---

# Variables and Liquid

Everything dynamic in this app is Liquid - the same templating language Shopify themes use. You write a placeholder, and the app fills it in at send time.

## The four namespaces

| Namespace | Where the value comes from | Available in |
| --- | --- | --- |
| `{{ variables.key }}` | The **Variables (JSON)** field on the Shopify Flow action | Email subject and body, HTTP URL, headers and body |
| `{{ secrets.key }}` | The **Secrets** page | Same, plus SMTP username and password |
| `{{ shop.name }}`, `{{ shop.domain }}`, `{{ shop.email }}` | Your store | Email and HTTP actions |
| `{{ flow.key }}` | The raw Shopify Flow payload | **HTTP requests only** |

The **System Variables** card in the editor lists the shop values with their current contents, and clicking one copies it.

## Passing variables from Shopify Flow

In the Flow action, put a JSON object in the **Variables (JSON)** field:

```
{
  "firstName": "{{ customer.firstName }}",
  "orderNumber": "{{ order.name }}",
  "total": "{{ order.totalPriceSet.shopMoney.amount }}"
}
```

Then reference them anywhere in the layout:

```
Hi {{ variables.firstName }}, your order {{ variables.orderNumber }} is on its way.
```

The keys are yours to choose. Whatever you name on the left is what you use after `variables.`.

## Filters

Standard Liquid filters work. The most useful one is `default`, which protects you from an empty value:

```
Hi {{ variables.firstName | default: "there" }}
```

Others behave as you would expect:

```
{{ variables.productTitle | upcase }}
{{ variables.total | round: 2 }}
{{ variables.note | truncate: 100 }}
```

> [!NOTE]
> Templates are rendered leniently on purpose. An unknown variable renders as an empty string and an unknown filter is ignored, rather than throwing an error and failing the send. That keeps a typo from blocking a customer's email - but it also means a typo shows up as a blank, not an error message. When something renders empty, check the spelling first.

## Conditionals and loops

Use the **HTML / Code** block for conditionals:

```
{% if variables.isVip %}
  <p>As a VIP you get free shipping on this order.</p>
{% else %}
  <p>Spend a little more to unlock free shipping.</p>
{% endif %}
```

Use the **Loop** block to repeat over an array, for example order line items:

```
{% for item in variables.items %}
  <tr>
    <td>{{ item.title }}</td>
    <td>{{ item.quantity }}</td>
    <td>{{ item.price }}</td>
  </tr>
{% endfor %}
```

Pass the array from Flow as JSON:

```
{
  "items": [
    { "title": "Classic White T-Shirt", "quantity": 2, "price": "29.00" },
    { "title": "Eco Water Bottle", "quantity": 1, "price": "19.00" }
  ]
}
```

To preview a loop in the builder, enter the same JSON array as the value in the **Test Variables** tab.

## Objects behave differently in email and HTTP

This is the one sharp edge worth knowing.

- In an **HTTP request**, interpolating an object or array serialises it to JSON automatically. `{{ variables.records }}` becomes a proper JSON array in the request body. A `json` filter is also available if you want to be explicit.
- In an **email**, there is no such conversion. Interpolating a whole object renders the literal text `[object Object]`.

So in emails, always reach into the object rather than printing it:

```
Wrong:  {{ variables.customer }}
Right:  {{ variables.customer.firstName }} {{ variables.customer.lastName }}
```

Or loop over an array instead of printing it whole.

## Secrets in templates

Reference a stored secret the same way:

```
Authorization: Bearer {{ secrets.airtableAccessToken }}
```

Use the exact syntax `{{ secrets.keyName }}`. These do **not** work:

- `{{ secret.keyName }}` - singular
- `{{ secrets['keyName'] }}` - bracket syntax

Key names are case-sensitive. See [Secrets](https://crm-tool.k8s-gcp-dev.eu.codecreationlabs.cloud/h/flow-transactional-email/secrets.md).

## Body override for HTTP requests

The HTTP action's variables field has a second mode. Pass a `body` key and it replaces the saved request body entirely:

```
{
  "body": { "anything": "you like", "shaped": ["however"] }
}
```

This is useful when the payload shape is decided by an earlier Flow step. The one exception: if your saved body itself references `{{ variables.body }}`, it is treated as an ordinary variable instead.

## Testing what you wrote

The on-page preview does **not** evaluate Liquid - it shows the tags as written. To see resolved values:

- For emails, use **Send test email** from the layout editor.
- For HTTP requests, use the **Test** button, which shows the **Resolved URL** and **Resolved Body** alongside the response.

## Related

- [Build an email layout](https://crm-tool.k8s-gcp-dev.eu.codecreationlabs.cloud/h/flow-transactional-email/build-an-email-layout.md) - where these variables go.
- [Secrets](https://crm-tool.k8s-gcp-dev.eu.codecreationlabs.cloud/h/flow-transactional-email/secrets.md) - storing credentials safely.
- [Make an HTTP request from Shopify Flow](https://crm-tool.k8s-gcp-dev.eu.codecreationlabs.cloud/h/flow-transactional-email/make-an-http-request.md) - variables in a real request.
