OxifyOxify

The properties

The line item properties Bundle Builder writes on every component line — what each one holds, when it's there, and which ones your templates should read.

Grouping in Liquid works because every product added from a builder carries identifying properties. They're written when the shopper adds the box to the cart, and every value is plain text. Bundle SKU is the only one a shopper can see; every other name starts with an underscore. This page is the reference; the template pages use them.

The ones you should read

PropertyExampleWhat it's for
Bundle SKUBOX-COFFEE-6Visible, human-facing. Your parent product's SKU. Only present when the setting is on and the parent product has a SKU.
_bundle_idbundle-65f1c2a4b3d8e9a1f2c3d4e5-1730000000001The grouping key. The word bundle, the builder's ID, then the moment the box was added to the cart (a millisecond timestamp). Unique per box the shopper built — two boxes from the same builder in one order get two different ids, so they render as two groups. Present on every line, in both cart line modes.
_bundle_builder_id65f1c2a4b3d8e9a1f2c3d4e5Which builder the box came from. It's the long ID at the end of the builder's address in the app (…/app/bundle-builder-set/<id>), not the Unique ID shown on the Settings tab. Use it if you want different markup per builder.
_slot_index0, 1, 2 …Position in the box. It's also what keeps two slots that hold the same product as two separate lines.

The ones that may or may not be there

These exist for the app's own pricing and merging logic, and which ones a line carries depends on the builder's Show products as individual lines in the cart setting. Read them only as fallbacks, and always guard with != blank.

PropertyWhen it's present
_oxify_builderIndividual cart-line mode only. A compact JSON string with the box's id, the discount type and value, and the bundle name (the Bundle Name from General Settings) — for example {"id":"bundle-65f1c2a4b3d8e9a1f2c3d4e5-1730000000001","dt":"percentage","dv":"10","n":"Coffee Box"}. A q key (the item count the discount was earned at) is added when Allow customers to add more items than the selected quantity is on.
_bundle_cart_modeIndividual mode only — the value is individual.
_bundle_discountMerged mode, current format: the discount type and value joined by a colon, e.g. percentage:50, fixed-price:49.99 or no-discount:0.
_bundle_discount_type, _bundle_discount_value, _bundle_name, _bundle_image_url, _bundle_parent_variantMerged mode, legacy format only — older orders, and builders whose settings hadn't yet synced to the Cart Transform. Don't rely on _bundle_name or _bundle_image_url being there; fall back to the first component's image.

Underscored properties are hidden from customers, not from you

Shopify's checkout doesn't show _-prefixed properties, and themes such as Dawn skip them when they list properties, but they are always visible on the admin order page. That's a Shopify behaviour with no way to turn it off — it's why the app writes as few of them as it can. Any loop you write yourself over line_item.properties has to skip them too (see below).

Getting the bundle's name

No single property always holds the name. A merged box ordered with the current format doesn't carry it on the line at all — the name lives in the app's Cart Transform configuration instead — so templates resolve it in this order:

Native group title. On packing slips, a merged bundle exposes line_item.groups; the non-deliverable group's title is the bundle's name.

_oxify_builder. In individual mode, split the JSON string on "n":" and take what's before the next quote — Liquid can't parse JSON, so string-splitting is the only option. (A name that contains a double quote or a backslash comes out cut short or doubled.)

_bundle_name. Present on legacy merged-mode lines only.

A literal fallback, like Bundle.

Each template page implements this chain for you — you don't have to write it yourself.

In the email, Order Printer and theme snippets, a merged box ordered today ends at the fallback. To show its real name in an email, on an invoice or in your theme, map _bundle_builder_id to a name yourself. The builder's ID is the last part of its address in the app. Paste this right after the snippet's _bundle_name lookup, inside the same if block, and use your snippet's own variable names (line and bundle_name in emails, line_item and b_name in Order Printer):

{% case line.properties._bundle_builder_id %}
  {% when "65f1c2a4b3d8e9a1f2c3d4e5" %}{% assign bundle_name = "Coffee Box" %}
  {% when "65f1c2a4b3d8e9a1f2c3d4e6" %}{% assign bundle_name = "Tea Box" %}
{% endcase %}

Reading a property in Liquid

Two syntaxes, both valid. Use the bracket form when the name has a space, like Bundle SKU:

{{ line_item.properties["Bundle SKU"] }}
{{ line_item.properties._bundle_id }}

To print a row's properties yourself, skip the ones you've already displayed in the header, and every property whose name starts with an underscore. A loop over line_item.properties includes those — Dawn filters them with this same first-character check:

{% for property in line_item.properties %}
  {% assign first_char = property.first | slice: 0 %}
  {% unless property.last == blank or first_char == '_' or property.first == 'Bundle SKU' %}
    {{ property.first }}: {{ property.last }}
  {% endunless %}
{% endfor %}

Next

Packing slips — the full worked example.

On this page