Sort collections by recent sales performance, with Mechanic.

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

Sort collections by recent sales performance

This task will run daily to sort the selected collections, moving products with sales in the lookback period to the top in descending order by the sort metric (e.g. "Net sales"). All other products in each collection will be left in the previously sorted order. Choose the length of the sales lookback period in days and the metric you wish to use for sorting.

Runs Occurs every day at midnight (in local time) and Occurs when a user manually triggers the task. Configuration includes collections to sort, lookback period in days, and sort metric.

15-day free trial – unlimited tasks

Documentation

This task will run daily to sort the selected collections, moving products with sales in the lookback period to the top in descending order by the sort metric (e.g. "Net sales"). All other products in each collection will be left in the previously sorted order. Choose the length of the sales lookback period in days and the metric you wish to use for sorting.

Notes:
- This task will skip any collections that are not configured for manual sorting.
- The sales lookback period is capped at 90 days to avoid potential Shopify Analytics query limits. Similarly, the sales data for each collection is limited to the top 1000 products by metric.
- The metrics offered for configuration directly match sales metrics available in Shopify Analytics.

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
mechanic/scheduler/daily
mechanic/user/trigger
Tasks use subscriptions to sign up for specific kinds of events. Learn more
Options
collections to sort (picker, collection, array, required) , lookback period in days (range, min7, max90, required) , sort metric (choice, o1, net, items, sold, o2, net, sales, o3, total, sales, required)
Code
{% assign collections_to_sort = options.collections_to_sort__picker_collection_array_required %}
{% assign lookback_period_in_days = options.lookback_period_in_days__range_min7_max90_required %}
{% assign sort_metric = options.sort_metric__choice_o1_net_items_sold_o2_net_sales_o3_total_sales_required %}

{% if event.preview %}
  {% comment %}
    -- only use a single collection for preview
  {% endcomment %}

  {% assign collections_to_sort = array | push: "gid://shopify/Collection/1234567890" %}
{% endif %}

{% if event.topic == "mechanic/user/trigger" or event.topic contains "mechanic/scheduler/" %}
  {% comment %}
    -- process each configured collection that is sorted manually
  {% endcomment %}

  {% assign skipped_collections = array %}

  {% for collection_id in collections_to_sort %}
    {% comment %}
      -- get the product IDs by the current sort
    {% endcomment %}

    {% assign cursor = nil %}
    {% assign collection = nil %}
    {% assign products_current_sort = array %}

    {% for n in (1..100) %}
      {% capture query %}
        query {
          collection(id: {{ collection_id | json }}) {
            id
            title
            sortOrder
            products(
              first: 250
              after: {{ cursor | json }}
              sortKey: COLLECTION_DEFAULT
            ) {
              pageInfo {
                hasNextPage
                endCursor
              }
              nodes {
                legacyResourceId
                title
              }
            }
          }
        }
      {% endcapture %}

      {% assign result = query | shopify %}

      {% if event.preview %}
        {% capture result_json %}
          {
            "data": {
              "collection": {
                "id": "gid://shopify/Collection/1234567890",
                "title": "Everyday Essentials",
                "sortOrder": "MANUAL",
                "products": {
                  "nodes": [
                    {
                      "legacyResourceId": "1234567890",
                      "title": "Sprocket"
                    },
                    {
                      "legacyResourceId": "2345678901",
                      "title": "Widget"
                    },
                    {
                      "legacyResourceId": "3456789012",
                      "title": "Gadget"
                    }
                  ]
                }
              }
            }
          }
        {% endcapture %}

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

      {% assign collection = result.data.collection | except: "products" %}
      {% assign products_current_sort = products_current_sort | concat: result.data.collection.products.nodes %}

      {% if result.data.collection.products.pageInfo.hasNextPage and collection.sortOrder == "MANUAL" %}
        {% assign cursor = result.data.collection.products.pageInfo.endCursor %}
      {% else %}
        {% break %}
      {% endif %}
    {% endfor %}

    {% if collection == blank %}
      {% log
        message: "Configured collection not found. As it was likely deleted, it should be removed from the task configuration.",
        collection_id: collection_id
      %}
      {% continue %}

    {% elsif collection.sortOrder != "MANUAL" %}
      {% assign skipped_collections = skipped_collections | push: collection %}
      {% continue %}
    {% endif %}

    {% comment %}
      -- get first 1000 products in collection that had sales in the lookback period
    {% endcomment %}

    {%- capture shopifyql_query -%}
      FROM sales
      SHOW {{ sort_metric }}
      WHERE product_collections CONTAINS '{{ collection.title }}'
      GROUP BY product_id, product_title
      SINCE -{{ lookback_period_in_days }}d UNTIL today
      ORDER BY {{ sort_metric }} DESC
      LIMIT 1000
    {%- endcapture -%}

    {% unless event.preview %}
      {% log shopifyql_query %}
    {% endunless %}

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

    {% assign result = query | shopify %}

    {% if event.preview %}
      {% capture result_json %}
        {
          "data": {
            "shopifyqlQuery": {
              "tableData": {
                "rows": [
                  {
                    "product_id": "2345678901",
                    "product_title": "Widget",
                    "net_items_sold": "20",
                    "net_sales": "2000.00",
                    "total_sales": "2200.00"
                  },
                  {
                    "product_id": "3456789012",
                    "product_title": "Gadget",
                    "net_items_sold": "10",
                    "net_sales": "1000.00",
                    "total_sales": "1100.00"
                  }
                ]
              }
            }
          }
        }
      {% endcapture %}

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

    {% assign products_with_sales = result.data.shopifyqlQuery.tableData.rows %}

    {% comment %}
      -- add products with sales to new sorted array if they still exist in collection
    {% endcomment %}

    {% assign product_ids_current_sort = products_current_sort | map: "legacyResourceId" %}
    {% assign products_new_sort = array %}

    {% for product in products_with_sales %}
      {% if product_ids_current_sort contains product.product_id %}
        {% assign products_new_sort = products_new_sort | push: product %}
      {% endif %}
    {% endfor %}

    {% comment %}
      -- append collection products that did not have sales in the lookback period
    {% endcomment %}

    {% assign product_ids_new_sort = products_new_sort | map: "product_id" %}

    {% for product in products_current_sort %}
      {% unless product_ids_new_sort contains product.legacyResourceId %}
        {% assign product_data = hash %}
        {% assign product_data["product_id"] = product.legacyResourceId %}
        {% assign product_data["product_title"] = product.title %}
        {% assign products_new_sort = products_new_sort | push: product_data %}
      {% endunless %}
    {% endfor %}

    {% unless event.preview %}
      {% log
        collection: collection,
        products_new_sort: products_new_sort
      %}
    {% endunless %}

    {% comment %}
      -- determine if any moves are necessary by comparing new and current sort
    {% endcomment %}

    {% assign moves = array %}

    {% for product in products_new_sort %}
      {% if product_ids_current_sort[forloop.index0] != product.product_id %}
        {% assign move = hash %}
        {% assign move["id"] = product.product_id | prepend: "gid://shopify/Product/" %}
        {% assign move["newPosition"] = "" | append: forloop.index0 %}
        {% assign moves[moves.size] = move %}
      {% endif %}
    {% endfor %}

    {% comment %}
      -- NOTE: using reverse filter here to circumvent Shopify bug on collection reordering
      -- Each move contains the new position, so using reverse will NOT change the sort order determined in above logic
    {% endcomment %}

    {% assign move_groups = moves | reverse | in_groups_of: 250, fill_with: false %}

    {% for move_group in move_groups %}
      {% action "shopify" %}
        mutation {
          collectionReorderProducts(
            id: {{ collection.id | json }}
            moves: {{ move_group | graphql_arguments }}
          ) {
            job {
              id
            }
            userErrors {
              field
              message
            }
          }
        }
      {% endaction %}

    {% else %}
      {% log
        message: "No position moves necessary for this collection, everything is already in its appropriate sort order.",
        collection: collection
      %}
    {% endfor %}
  {% endfor %}

  {% if skipped_collections != blank %}
    {% log skipped_collections_which_did_not_have_manual_sort: skipped_collections %}
  {% endif %}
{% endif %}
Task code is written in Mechanic Liquid, an extension of open-source Liquid enhanced for automation. Learn more
Defaults
Lookback period in days
7