OxifyOxify

Notification emails

Show a bundle as one grouped row with its SKU and total in order confirmation, shipping confirmation, and invoice emails.

By default, an order confirmation email lists every component of a box as its own row with its own price. This block regroups them into one row: the bundle's name and SKU, the components indented underneath, and one combined price.

Where to edit: Shopify admin → Settings → Notifications → pick a notification → Edit code.

The same block works in Order confirmation, Shipping confirmation, Order invoice, and any other notification that loops over line_items. Replace the loop that prints the items — {% for line in line_items %}, or {% for line in subtotal_line_items %} (the same list without tip lines) — with everything below. If yours uses subtotal_line_items, use that name wherever this snippet says line_items.

The row markup for the non-bundle items below is a bare-bones stand-in. Paste your template's own row markup in its place.

Back it up first

Copy the original template into a text file before editing. Shopify's Revert to default button exists, but it throws away all your other customizations too.

The snippet

{%- comment -%} Collect the distinct bundle ids on this order {%- endcomment -%}
{%- assign bundle_ids = "" -%}
{%- for line in line_items -%}
  {%- if line.properties._bundle_id != blank -%}
    {%- assign bundle_ids = bundle_ids | append: line.properties._bundle_id | append: "~" -%}
  {%- endif -%}
{%- endfor -%}
{%- assign bundle_ids = bundle_ids | split: "~" | uniq -%}
 
{%- comment -%} 1. Non-bundle items render exactly as before {%- endcomment -%}
{%- for line in line_items -%}
  {%- if line.properties._bundle_id == blank -%}
    <tr>
      <td><img src="{{ line.image | image_url: width: 200 }}" width="100" alt=""></td>
      <td>
        {{ line.title }}<br>
        <span>x {{ line.quantity }}</span>
      </td>
      <td>{{ line.final_line_price | money }}</td>
    </tr>
  {%- endif -%}
{%- endfor -%}
 
{%- comment -%} 2. One grouped block per bundle {%- endcomment -%}
{%- for bid in bundle_ids -%}
  {%- assign bundle_name  = "" -%}
  {%- assign bundle_sku   = "" -%}
  {%- assign bundle_image = "" -%}
  {%- assign bundle_total = 0 -%}
  {%- assign bundle_was   = 0 -%}
 
  {%- for line in line_items -%}
    {%- if line.properties._bundle_id == bid -%}
      {%- if bundle_sku == "" -%}
        {%- assign bundle_sku = line.properties["Bundle SKU"] -%}
      {%- endif -%}
      {%- if bundle_name == "" -%}
        {%- if line.properties._oxify_builder != blank -%}
          {%- assign name_parts = line.properties._oxify_builder | split: '"n":"' -%}
          {%- if name_parts.size > 1 -%}
            {%- assign bundle_name = name_parts[1] | split: '"' | first -%}
          {%- endif -%}
        {%- endif -%}
        {%- if bundle_name == "" -%}
          {%- assign bundle_name = line.properties._bundle_name -%}
        {%- endif -%}
        {%- if line.properties._bundle_image_url != blank -%}
          {%- assign bundle_image = line.properties._bundle_image_url -%}
        {%- else -%}
          {%- assign bundle_image = line.image | image_url: width: 200 -%}
        {%- endif -%}
      {%- endif -%}
      {%- assign bundle_total = bundle_total | plus: line.final_line_price -%}
      {%- assign bundle_was   = bundle_was   | plus: line.original_line_price -%}
    {%- endif -%}
  {%- endfor -%}
 
  <tr>
    <td><img src="{{ bundle_image }}" width="100" alt=""></td>
    <td>
      <strong>{{ bundle_name | default: "Bundle" }}</strong>
      {%- if bundle_sku != blank -%}
        <div style="color:#6b7177; font-size:12px;">SKU {{ bundle_sku }}</div>
      {%- endif -%}
      <div style="margin-top:6px; padding-left:12px; border-left:2px solid #e3e3e3;">
        {%- for line in line_items -%}
          {%- if line.properties._bundle_id == bid -%}
            <div style="color:#6b7177; font-size:13px;">
              {{ line.title }} &times; {{ line.quantity }}
            </div>
          {%- endif -%}
        {%- endfor -%}
      </div>
    </td>
    <td>
      {%- if bundle_was > bundle_total -%}
        <s>{{ bundle_was | money }}</s><br>
      {%- endif -%}
      {{ bundle_total | money }}
    </td>
  </tr>
{%- endfor -%}

How it works

  • _bundle_id is the grouping key. Two boxes from the same builder in one order have different ids, so they render as two separate groups. That's intentional.
  • Totals are summed from the components, not read from the bundle parent — the parent doesn't exist on the order. final_line_price already includes every discount that applied (bundle pricing and any of your own automatic discounts), so the total in the email always matches what was charged. In merged mode the Cart Transform applies the bundle price to the merged line; in individual mode it's an automatic discount named after the bundle.
  • The strikethrough price only renders when the components were actually discounted, so a no-discount bundle shows a single clean price.
  • The SKU comes from the visible Bundle SKU property and is printed once in the header rather than on every child row.
  • The name comes from _oxify_builder (individual mode) or _bundle_name (older merged lines). A merged box ordered today carries neither, so its header reads Bundle — see Getting the bundle's name to name it yourself, or lead with the SKU.

Gotchas

  • Don't sort children by _slot_index with Liquid's sort — it's a string, so slot 10 would sort before slot 2. line_items already comes back in cart order.
  • Per-item discount labels (your own "BUY 4 → 20% OFF" and similar) are dropped by this layout by design, since the group shows one combined price. To keep them, iterate line.discount_allocations inside the child loop.
  • Test with a real order. Shopify's notification preview uses fake line items with no properties, so nothing groups in the preview. Place a test order, then use Orders → the order → Resend email.

Next

Order Printer — invoices, pick lists, and return forms.

On this page