---
title: "Passing Shopify Flow data into an email - the Variables (JSON) field"
description: "How to fill the Variables (JSON) field in the Send Transactional Email action, why a variable arrives empty, and how to fix line items that need a loop."
canonical: "https://crm-tool.k8s-gcp-dev.eu.codecreationlabs.cloud/h/flow-transactional-email/passing-data-from-shopify-flow"
---

# Passing data from Shopify Flow

Your email layout is a shape with holes in it. **Variables (JSON)** is how Shopify Flow fills those holes.

This is the single most common thing to get wrong, so it is worth five minutes.

## The two halves

| Where | What you write | Example |
| --- | --- | --- |
| In your **email layout** | The placeholder | `Hi {{ variables.firstName }}` |
| In the **Flow action** | The JSON that supplies it | `{"firstName": "{{ order.customer.firstName }}"}` |

The key on the left of the JSON must match the name after `variables.` in your layout, **exactly** - same spelling, same capitalisation. `firstName` and `firstname` are two different variables.

## Let the app write the JSON for you

You do not have to write this by hand, and you probably should not.

Open your layout. In the sidebar (or the **Test Variables** tab in the visual builder) there is a **Use in Shopify Flow** card containing the exact JSON for that layout, with every key already correct. Press **Copy JSON**, then paste it into the **Variables (JSON)** field of your Send Transactional Email action.

The app fills in the Flow property for fields it recognises - customer name, order name, order total, status page URL and similar. Anything it cannot infer appears as `<pick a Flow variable>` for you to replace.

> [!TIP]
> When replacing a placeholder, use Shopify Flow's own variable picker next to the field rather than typing the path from memory. A mistyped path is not an error in Flow - it simply produces nothing, which is why so many of these problems end up invisible.

## What the JSON must look like

```
{
  "firstName": "{{ order.customer.firstName }}",
  "orderName": "{{ order.name }}",
  "statusPageUrl": "{{ order.statusPageUrl }}"
}
```

Rules worth knowing:

- **Wrap every Flow value in double quotes.** `"orderName": {{ order.name }}` is invalid JSON; `"orderName": "{{ order.name }}"` is correct.
- **Commas between entries, none after the last one.**
- **Key names are yours to choose.** They only need to match your layout. If your layout says `{{ variables.kunde }}`, the key is `kunde`.
- Values arrive as text. Numbers and dates come through as whatever Flow rendered them to.

## The most common mistake: line items

An order has *many* line items, so there is no single `variant.title` to read. This returns nothing:

```
{{ order.lineItems.variant.title }}
```

You have two good options.

**Option 1 - loop inside the layout.** Send the list, then iterate. In the Flow action:

```
{
  "items": "{{ order.lineItems | json }}"
}
```

In your layout, use a **Loop** block:

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

**Option 2 - flatten it in Flow.** If you only need one line of text, build it in Flow with a `for` loop and send the finished string:

```
{
  "itemList": "{% for li in order.lineItems %}{{ li.title }} x{{ li.quantity }}{% unless forloop.last %}, {% endunless %}{% endfor %}"
}
```

If the app spots a variable that looks like line-item data - anything named `variant`, `product`, `item`, `sku` or `quantity` - it warns you about this on the **Use in Shopify Flow** card.

## Test values are not live values

This trips up nearly everyone.

- **Test Variables** in the layout editor drive the **preview** and **Send test email** only.
- A **live Flow run** ignores them completely and uses whatever the Flow action sends.

So a test email that looks perfect proves your *design* works. It proves nothing about your Flow configuration. Always confirm with a real workflow run.

## When a variable comes out empty

Since 1.10.0 the app tells you instead of leaving you guessing.

If a live send renders a variable to nothing, the entry in **History** carries an **Empty variables** badge, and opening it shows exactly which ones, split into two cases:

| Case | Meaning | Fix |
| --- | --- | --- |
| **Sent by Shopify Flow but empty** | The key was in your JSON, but Flow produced no value | The Flow property path is wrong, or genuinely empty for this order (a guest checkout has no `customer.firstName`) |
| **Not sent by Shopify Flow at all** | The key is missing from your JSON entirely | Add it - the Copy JSON button gives you the full set |

> [!NOTE]
> The email is still delivered when this happens. Templates render leniently on purpose, so a typo can never block a customer's email - the trade-off is that a mistake shows up as a blank rather than an error. That is exactly what the History warning is there to catch.

## Guarding against empty values

For anything optional, give it a fallback in the layout so a blank never reaches a customer:

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

With a `default:` filter in place, the app treats an empty value as intentional and will not warn about it.

## Checklist

1. Copy the JSON from the **Use in Shopify Flow** card on your layout.
2. Paste it into the Flow action's **Variables (JSON)** field.
3. Replace any `<pick a Flow variable>` using Flow's variable picker.
4. Loop over line items rather than reading them directly.
5. Add `| default:` to anything optional.
6. Run the workflow for real, then check **History** for an **Empty variables** badge.

## Related

- [Variables and Liquid](https://crm-tool.k8s-gcp-dev.eu.codecreationlabs.cloud/h/flow-transactional-email/variables-and-liquid.md) - the full templating reference.
- [Build an email layout](https://crm-tool.k8s-gcp-dev.eu.codecreationlabs.cloud/h/flow-transactional-email/build-an-email-layout.md) - designing the layout itself.
- [History and troubleshooting](https://crm-tool.k8s-gcp-dev.eu.codecreationlabs.cloud/h/flow-transactional-email/history-and-troubleshooting.md) - reading a failed or blank send.
