Auto-tag customers having a rolling minimum total spend, with Mechanic.

Mechanic is a development and ecommerce automation platform for Shopify. :)

Auto-tag customers having a rolling minimum total spend

This task runs daily to maintain tags for any customers that have a certain spending threshold within a rolling period of order history. Useful for rewarding customers who keep a consistent spend total. Select to have the rolling spend calculated using either "Total sales" (net sales + additional fess + duties + shipping charges + taxes) or only "Net sales" (gross sales - discounts - returns).

Runs Occurs every day at midnight (in local time) and Occurs when a user manually triggers the task. Configuration includes sales calculation to use, minimum total spent, customer tag to apply, days of order history to consider, only monitor customers having this tag, run hourly instead of daily, and test mode.

15-day free trial – unlimited tasks

Documentation

This task runs daily to maintain tags for any customers that have a certain spending threshold within a rolling period of order history. Useful for rewarding customers who keep a consistent spend total. Select to have the rolling spend calculated using either "Total sales" (net sales + additional fess + duties + shipping charges + taxes) or only "Net sales" (gross sales - discounts - returns).

Optionally, choose to filter by customers who have a specific tag, or to run hourly instead of daily for increased tagging frequency, or to run the task in "Test mode".

The "Test mode" option is highly recommended if you expect the task to make a lot of tagging changes (e.g. during initial set up), and you'd like to review the task run log of what it would do without making the actual tag updates. Just remember to turn off the test mode when you are ready for the daily (or hourly) scheduled task runs to make updates.

Developer details

Mechanic is designed to benefit everybody: merchants, customers, developers, agencies, Shopifolks, everybody.

That’s why we make it easy to configure automation without code, why we make it easy to tweak the underlying code once tasks are installed, and why we publish it all here for everyone to learn from.

(By the way, have you seen our documentation? Have you joined the Slack community?)

Open source
View on GitHub to contribute to this task
Subscriptions
{% if options.run_hourly_instead_of_daily__boolean %}
  mechanic/scheduler/hourly
{% else %}
  mechanic/scheduler/daily
{% endif %}
mechanic/user/trigger
Tasks use subscriptions to sign up for specific kinds of events. Learn more
Options
sales calculation to use (choice, o1, total, sales, o2, net, sales, required) , minimum total spent (number, required) , customer tag to apply (required) , days of order history to consider (number, required) , only monitor customers having this tag , run hourly instead of daily (boolean) , test mode (boolean)
Code
{% assign sales_calculation_to_use = options.sales_calculation_to_use__choice_o1_total_sales_o2_net_sales_required %}
{% assign minimum_total_spent = options.minimum_total_spent__number_required %}
{% assign customer_tag_to_apply = options.customer_tag_to_apply__required %}
{% assign days_of_order_history = options.days_of_order_history_to_consider__number_required %}
{% assign monitor_customer_tag = options.only_monitor_customers_having_this_tag %}
{% comment %}{{ options.run_hourly_instead_of_daily__boolean }}{% endcomment %}
{% assign test_mode = options.test_mode__boolean %}

{% if test_mode %}
  {% comment %}
    -- keep the write customers scope active in test mode, to avoid app updates
  {% endcomment %}

  {% permissions %}
    write_customers
  {% endpermissions %}
{% endif %}

{% if monitor_customer_tag == customer_tag_to_apply %}
  {% error "The two customer tag values must be different. Please change either 'Customer tag to apply' or 'Only monitor customers having this tag'." %}
{% endif %}

{% if event.topic == "mechanic/user/trigger" or event.topic contains "mechanic/scheduler/" %}
  {% comment %}
    -- get count of customers in the shop to use as upper limit in queries
    -- support optional monitor tag
  {% endcomment %}

  {% assign monitor_tag_query = nil %}

  {% if monitor_customer_tag != blank %}
    {% assign monitor_tag_query = monitor_customer_tag | json | prepend: "tag:" %}
  {% endif %}

  {% capture query %}
    query {
      customersCount(
        query: {{ monitor_tag_query | json }}
        limit: null
      ) {
        count
        precision
      }
    }
  {% endcapture %}

  {% assign result = query | shopify %}

  {% if event.preview %}
    {% capture result_json %}
      {
        "data": {
          "customersCount": {
            "count": 20000,
            "precision": "EXACT"
          }
        }
      }
    {% endcapture %}

    {% assign result = result_json | parse_json %}
  {% endif %}

  {% assign customers_count = result.data.customersCount %}

  {% log customers_count: customers_count %}

  {% comment %}
    -- use shopifyql query to get IDs of all customers who meet the minimum spend criteria and have not yet been tagged
    -- support optional monitor tag
  {% endcomment %}

  {%- capture qualify_to_tag_shopifyql_query -%}
    FROM sales
    SHOW {{ sales_calculation_to_use }}, orders
    WHERE customer_tags NOT CONTAINS '{{ customer_tag_to_apply }}'
    {%- if monitor_customer_tag != blank %}
    AND customer_tags CONTAINS '{{ monitor_customer_tag }}'
    {%- endif %}
    GROUP BY customer_email, customer_id
    HAVING {{ sales_calculation_to_use }} >= {{ minimum_total_spent }}
    SINCE -{{ days_of_order_history }}d UNTIL today
    ORDER BY customer_id ASC
    LIMIT {{ customers_count.count }}
  {%- endcapture -%}

  {% log qualify_to_tag_shopifyql_query %}

  {% capture query %}
    {
      shopifyqlQuery(query: {{ qualify_to_tag_shopifyql_query | json }}) {
        tableData {
          rows
        }
        parseErrors
      }
    }
  {% endcapture %}

  {% assign result = query | shopify %}

  {% if event.preview %}
    {% capture result_json %}
      {
        "data": {
          "shopifyqlQuery": {
            "tableData": {
              "rows": [
                {
                  "customer_id": "1234567890",
                  "customer_email": "sample@example.com",
                  "total_sales": {{ minimum_total_spent | json }}
                }
              ]
            }
          }
        }
      }
    {% endcapture %}

    {% assign result = result_json | parse_json %}
  {% endif %}

  {% comment %}
    -- shopify returns 0 for customer ID when the account is deleted, so exclude it
  {% endcomment %}

  {% assign customers_to_tag = array %}

  {% for row in result.data.shopifyqlQuery.tableData.rows %}
    {% unless row.customer_id == "0" %}
      {% assign customers_to_tag = customers_to_tag | push: row %}
    {% endunless %}
  {% endfor %}

  {% unless event.preview %}
    {% log count_customers_to_tag: customers_to_tag.size %}
    {% log customers_to_tag: customers_to_tag %}
  {% endunless %}

  {% comment %}
    -- use shopifyql query to get IDs of all customers who meet the minimum spend criteria regardless of the tag to apply
    -- support optional monitor tag
  {% endcomment %}

  {%- capture qualifying_shopifyql_query -%}
    FROM sales
    SHOW total_sales, orders
    {%- if monitor_customer_tag != blank %}
    WHERE customer_tags CONTAINS '{{ monitor_customer_tag }}'
    {%- endif %}
    GROUP BY customer_email, customer_id
    HAVING total_sales >= {{ minimum_total_spent }}
    SINCE -{{ days_of_order_history }}d UNTIL today
    ORDER BY customer_id ASC
    LIMIT {{ customers_count.count }}
  {%- endcapture -%}

  {% capture query %}
    {
      shopifyqlQuery(query: {{ qualifying_shopifyql_query | json }}) {
        tableData {
          rows
        }
        parseErrors
      }
    }
  {% endcapture %}

  {% assign result = query | shopify %}

  {% if event.preview %}
    {% capture result_json %}
      {
        "data": {
          "shopifyqlQuery": {
            "tableData": {
              "rows": [
                {
                  "customer_id": "1234567890",
                  "customer_email": "sample@example.com",
                  "total_sales": {{ minimum_total_spent | json }}
                }
              ]
            }
          }
        }
      }
    {% endcapture %}

    {% assign result = result_json | parse_json %}
  {% endif %}

  {% assign qualifying_customer_ids = result.data.shopifyqlQuery.tableData.rows | map: "customer_id" | except: "0" %}

  {% unless event.preview %}
    {% log count_qualifying_customers: qualifying_customer_ids.size %}
    {% log qualifying_customer_ids: qualifying_customer_ids %}
  {% endunless %}

  {% comment %}
    -- get IDs of all customers in the shop with the tag to apply
    -- support optional monitor tag
    -- analytics does not have data for customers with no orders in the rolling period, so we need to query the customers node
  {% endcomment %}

  {% if monitor_customer_tag == blank %}
    {%- capture all_tagged_search_query -%}
      tag:{{ customer_tag_to_apply | json }}
    {%- endcapture -%}

  {% else %}
    {%- capture all_tagged_search_query -%}
      tag:{{ customer_tag_to_apply | json }} AND tag:{{ monitor_customer_tag | json }}
    {%- endcapture -%}
  {% endif %}

  {% assign loop_limit = customers_count.count | divided_by: 250.0 | ceil %}
  {% assign cursor = nil %}
  {% assign all_tagged_customer_ids = array %}

  {% for n in (1..loop_limit) %}
    {% capture query %}
      query {
        customers(
          first: 250
          after: {{ cursor | json }}
          query: {{ all_tagged_search_query | json }}
        ) {
          pageInfo {
            hasNextPage
            endCursor
          }
          nodes {
            legacyResourceId
          }
        }
      }
    {% endcapture %}

    {% assign result = query | shopify %}

    {% if event.preview %}
      {% capture result_json %}
        {
          "data": {
            "customers": {
              "nodes": [
                {
                  "legacyResourceId": "3456789012"
                }
              ]
            }
          }
        }
      {% endcapture %}

      {% assign result = result_json | parse_json %}
    {% endif %}

    {% assign tagged_customer_ids = result.data.customers.nodes | map: "legacyResourceId" %}
    {% assign all_tagged_customer_ids = all_tagged_customer_ids | concat: tagged_customer_ids %}

    {% if result.data.customers.pageInfo.hasNextPage %}
      {% assign cursor = result.data.customers.pageInfo.endCursor %}
    {% else %}
      {% break %}
    {% endif %}
  {% endfor %}

  {% comment %}
    -- identify customers to untag by confirming their ID is not in the qualifying customer IDs list
  {% endcomment %}

  {% assign customer_ids_to_untag = array %}

  {% for customer_id in all_tagged_customer_ids %}
    {% unless qualifying_customer_ids contains customer_id %}
      {% assign customer_ids_to_untag = customer_ids_to_untag | push: customer_id %}
    {% endunless %}
  {% endfor %}

  {% unless event.preview %}
    {% log
      all_tagged_search_query: all_tagged_search_query,
      count_all_tagged_customers: all_tagged_customer_ids.size,
      count_customers_to_untag: customer_ids_to_untag.size
    %}
    {% log customer_ids_to_untag: customer_ids_to_untag %}
  {% endunless %}

  {% if test_mode %}
    {% log "Test mode was enabled for this task run; no customer tag changes will be made." %}
    {% break %}
  {% endif %}

  {% for customer in customers_to_tag %}
    {% action "shopify" %}
      mutation {
        tagsAdd(
          id: {{ customer.customer_id | prepend: "gid://shopify/Customer/" | json }}
          tags: {{ customer_tag_to_apply | json }}
        ) {
          userErrors {
            field
            message
          }
        }
      }
    {% endaction %}
  {% endfor %}

  {% for customer_id in customer_ids_to_untag %}
    {% action "shopify" %}
      mutation {
        tagsRemove(
          id: {{ customer_id | prepend: "gid://shopify/Customer/" | json }}
          tags: {{ customer_tag_to_apply | json }}
        ) {
          userErrors {
            field
            message
          }
        }
      }
    {% endaction %}
  {% endfor %}
{% endif %}
Task code is written in Mechanic Liquid, an extension of open-source Liquid enhanced for automation. Learn more
Defaults
Sales calculation to use
total_sales
Days of order history to consider
30