Switchboard DocsPolaris-native Shopify Functions

Liquid Cookbook

Drop-in theme.liquid snippets that drive Switchboard from Shopify's customer Liquid object. Every recipe assumes the Switchboard SDK app embed is enabled (see /docs/sdk for how).

These snippets are starter material. Adapt to your theme, your section structure, and your customer-segmentation strategy. They're written to be readable, not minified.


Where to put these

Add to theme.liquid (the main layout), inside a section that loads on every page, or in a snippet that you {% render %} from theme.liquid. The key requirement is that the snippet runs on every page the customer visits, so the cart attributes are set well before they reach checkout.

If a snippet only runs on a specific template (e.g. product.liquid), the attributes may not be set when the customer visits the cart from a different page. Avoid that.


What customer exposes

When the customer is logged in, the customer Liquid object is available everywhere. Useful fields:

{{ customer.id }}
{{ customer.email }}
{{ customer.first_name }}{{ customer.last_name }}
{{ customer.orders_count }}
{{ customer.total_spent }}              {# in cents #}
{{ customer.tags }}                     {# comma-separated list #}

{{ customer.default_address.first_name }}
{{ customer.default_address.last_name }}
{{ customer.default_address.address1 }}
{{ customer.default_address.city }}
{{ customer.default_address.province_code }}    {# "ON", "QC", etc. #}
{{ customer.default_address.country_code }}     {# "CA", "US", etc. #}
{{ customer.default_address.zip }}

{{ customer.addresses | size }}              {# all saved addresses #}
{{ customer.last_order.created_at }}
{{ customer.last_order.shipping_address.province_code }}

For pickup-history routing, you can iterate customer.orders and read order.line_items[].fulfillment.location.name. Be aware Shopify limits customer.orders to a recent slice — typically the last 50.


Pickup Reorder recipes

Pin based on the customer's saved default address

Pairs with Pickup Reorder → Auto-match mode if your location titles match province codes, or By cart attribute (Substring) mode if your titles only contain the province as a substring.

{%- if customer and customer.default_address.province_code -%}
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      if (window.Switchboard) {
        Switchboard.setPickup({{ customer.default_address.province_code | json }});
      }
    });
  </script>
{%- endif -%}

Last-order pickup

If you offered the customer pickup last time and they used it, surface that same location first next time.

Pairs with Pickup Reorder → By cart attribute (Exact name) mode.

{%- if customer.last_order -%}
  {%- for line in customer.last_order.line_items -%}
    {%- if line.fulfillment.location.name -%}
      <script>
        document.addEventListener('DOMContentLoaded', function () {
          if (window.Switchboard) {
            Switchboard.setPickup({{ line.fulfillment.location.name | json }});
          }
        });
      </script>
      {%- break -%}
    {%- endif -%}
  {%- endfor -%}
{%- endif -%}

Tier routing (B2B vs retail)

Different customer tags → different pickup orderings.

Pairs with Pickup Reorder → By cart attribute (Exact name) mode.

{%- if customer.tags contains 'wholesale' -%}
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      if (window.Switchboard) {
        Switchboard.setPickup(['Wholesale Hub', 'Main Store']);
      }
    });
  </script>
{%- elsif customer.tags contains 'vip' -%}
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      if (window.Switchboard) {
        Switchboard.setPickup(['VIP Lounge', 'Main Store']);
      }
    });
  </script>
{%- endif -%}

Cart Validation recipes

Tag the cart with the customer's country

Used by validation rules that gate by country, region, etc.

{%- if customer and customer.default_address.country_code -%}
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      if (window.Switchboard) {
        Switchboard.setCartAttribute('country', {{ customer.default_address.country_code | json }});
      }
    });
  </script>
{%- endif -%}

Tag with customer tier (loyalty / B2B segment)

{%- if customer -%}
  {%- assign tier = 'standard' -%}
  {%- if customer.tags contains 'wholesale' -%}{%- assign tier = 'wholesale' -%}{%- endif -%}
  {%- if customer.tags contains 'vip' -%}{%- assign tier = 'vip' -%}{%- endif -%}
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      if (window.Switchboard) {
        Switchboard.setCartAttribute('loyalty_tier', {{ tier | json }});
      }
    });
  </script>
{%- endif -%}

Push several attributes at once (more efficient)

One cart write instead of three.

{%- if customer -%}
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      if (window.Switchboard) {
        Switchboard.setBatch({
          country: {{ customer.default_address.country_code | json }},
          orders_count: {{ customer.orders_count | json }},
          total_spent_cents: {{ customer.total_spent | json }}
        });
      }
    });
  </script>
{%- endif -%}

Age gating — declarative (no JS to write)

In the cart section template:

<label>
  <input type="checkbox" data-switchboard-attr="age_confirmed" value="true">
  I confirm I am 18+ years of age.
</label>

The SDK's declarative mode syncs the checkbox state into the _switchboard.age_confirmed cart attribute on every change. Pair with a validation rule age_confirmed is not equals true to block checkout when unchecked.


Cart Transform recipes

VIP discount on every line

Pairs with Cart Transform. Set minPercentOfVariant to e.g. 0.7 in the rule's advanced settings so the discount never drops below 70% of the variant price.

{%- if customer.tags contains 'vip' -%}
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      if (!window.Switchboard) return;
      fetch('/cart.js').then(function (r) { return r.json(); }).then(function (cart) {
        cart.items.forEach(function (line) {
          var discounted = Math.round(line.price * 0.75); // 25% off
          Switchboard.signAndSetPrice(line.key, discounted);
        });
      });
    });
  </script>
{%- endif -%}

Shipping protection toggle (cart page)

Add to a section that renders on the cart page. Replace protection_variant_id with the variant ID of your hidden shipping-protection product.

<label>
  <input type="checkbox" id="ship-protect"> Add shipping protection ($2.99)
</label>

<script>
  document.getElementById('ship-protect').addEventListener('change', function (e) {
    if (!window.Switchboard) return;
    if (!e.target.checked) return;
    Switchboard.addLineItemWithProperties({{ section.settings.protection_variant_id }}, 1, {})
      .then(function (cart) {
        var line = cart.items[cart.items.length - 1];
        return Switchboard.signAndSetPrice(line.key, 299);
      });
  });
</script>

Combining recipes

Most production stores combine all three features. A typical theme.liquid snippet looks like:

{%- if customer -%}
  <script>
    document.addEventListener('DOMContentLoaded', function () {
      if (!window.Switchboard) return;

      // 1. Tag the cart with everything Validation rules might reference
      Switchboard.setBatch({
        country: {{ customer.default_address.country_code | json }},
        loyalty_tier: {%- if customer.tags contains 'vip' -%}"vip"{%- elsif customer.tags contains 'wholesale' -%}"wholesale"{%- else -%}"standard"{%- endif -%},
        orders_count: {{ customer.orders_count | json }}
      });

      // 2. Hint Pickup Reorder with the customer's last-used pickup spot (if any)
      {%- if customer.last_order -%}
        {%- for line in customer.last_order.line_items -%}
          {%- if line.fulfillment.location.name -%}
            Switchboard.setPickup({{ line.fulfillment.location.name | json }});
            {%- break -%}
          {%- endif -%}
        {%- endfor -%}
      {%- endif -%}
    });
  </script>
{%- endif -%}

That single block handles validation tagging + pickup hinting in one page load. Cart Transform usually lives in its own checkbox / button handlers per recipe.


What Liquid CAN'T do

  • No async / external API calls. Liquid runs server-side at render time. If you need to enrich from a CRM or fraud-scoring service, do that from JS at page load and call the SDK in the callback.
  • Limited order pagination. customer.orders typically returns the most-recent 50. For deeper history use the Storefront API from your storefront JS.
  • No fulfillment on unfulfilled orders. The "last pickup location" recipe only works after the previous order was actually fulfilled.
  • Customer object empty for guests. All of these recipes assume customer is set (i.e. the customer is logged in). Wrap in {%- if customer -%} guards.

See also