Unpublish products that have been out of stock for x days, with Mechanic.

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

Unpublish products that have been out of stock for x days

This task monitors for inventory changes, and records the time when a product's inventory falls to 0 or less. Then, on an hourly basis, the task will unpublish any products with a recorded out-of-stock time of at least x days ago.

Runs Occurs whenever an inventory level is updated and Occurs every hour. Configuration includes number of days to wait before unpublishing, sales channel names, only include products matching this search query, and test mode.

15-day free trial – unlimited tasks

Documentation

This task monitors for inventory changes, and records the time when a product's inventory falls to 0 or less. Then, on an hourly basis, the task will unpublish any products with a recorded out-of-stock time of at least x days ago.

This task watches for inventory updates. When a product's total inventory becomes 0 or less, the current time will be recorded for that product. Then, on an hourly basis, this task will unpublish any products with a recorded out-of-stock time of at least x days ago.

Notes:

  • The first time the hourly scan runs, it may encounter products that were already out of stock, before this task was installed. For those products, the current time will be recorded as their out-of-stock time. This means that these products will wait for another x days before being automatically unpublished.
  • If a product that this task unpublishes is manually published, and its total inventory is still 0 or less, this task will unpublish it again during its next hourly scan.
  • This task includes a test mode. Enable it to have the task report what it would do, if test mode was disabled.

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
shopify/inventory_levels/update
mechanic/scheduler/hourly
Tasks use subscriptions to sign up for specific kinds of events. Learn more
Options
number of days to wait before unpublishing (number, required), sales channel names (required, array), only include products matching this search query, test mode (boolean)
Code
{% comment %}
  Preferred option order:

  {{ options.number_of_days_to_wait_before_unpublishing__number_required }}
  {{ options.sales_channel_names__required_array }}
  {{ options.only_include_products_matching_this_search_query }}
  {{ options.test_mode__boolean }}
{% endcomment %}

{% comment %}
  1. Watch for updates to inventory levels.
  2. Look up the associated product, when an update comes in.
  3. If the product's total inventory is 0, record the current time in a metafield. If the
     total inventory is not 0, delete that metafield, if it exists.
  4. Scan all products, on a schedule, retrieving each product's out-of-stock time metafield.
     For products found with a total inventory of 0, with a recorded time that's at least a
     configurable distance in days from the current time, unpublish the product.
{% endcomment %}

{% assign time_format = "%FT%T%z" %}

{% if event.topic == "shopify/inventory_levels/update" %}
  {% capture query %}
    query {
      inventoryLevel(
        id: {{ inventory_level.admin_graphql_api_id | json }}
      ) {
        id
        item {
          variant {
            product {
              id
              title
              totalInventory
              metafield(
                namespace: "mechanic"
                key: "out_of_stock_at"
              ) {
                id
                value
              }
            }
          }
        }
      }
    }
  {% endcapture %}

  {% assign result = query | shopify %}

  {% if event.preview %}
    {% capture result_json %}
      {
        "data": {
          "inventoryLevel": {
            "id": "gid://shopify/InventoryLevel/1234567890?inventory_item_id=1234567890",
            "item": {
              "variant": {
                "product": {
                  "id": "gid://shopify/Product/1234567890",
                  "title": "Short sleeve t-shirt",
                  "totalInventory": 0,
                  "metafield": null
                }
              }
            }
          }
        }
      }
    {% endcapture %}

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

  {% assign product = result.data.inventoryLevel.item.variant.product %}

  {% if product.totalInventory <= 0 and product.metafield == nil %}
    {% if options.test_mode__boolean %}
      {% action "echo" %}
        {% capture message %}Product {{ product.title | json }} ({{ product.id }}) is out of stock. Its out-of-stock time should be recorded.{% endcapture %}
        {{ message | json }}
      {% endaction %}
    {% else %}
      {% action "shopify" %}
        mutation {
          productUpdate(
            input: {
              id: {{ product.id | json }}
              metafields: [
                {
                  namespace: "mechanic"
                  key: "out_of_stock_at"
                  type: "date_time"
                  value: {{ "now" | date: time_format | json }}
                }
              ]
            }
          ) {
            userErrors {
              field
              message
            }
          }
        }
      {% endaction %}
    {% endif %}
  {% elsif product.totalInventory > 0 and product.metafield != nil %}
    {% if options.test_mode__boolean %}
      {% action "echo" %}
        {% capture message %}Product {{ product.title | json }} ({{ product.id }}) is back in stock. Its out-of-stock time should be cleared.{% endcapture %}
        {{ message | json }}
      {% endaction %}
    {% else %}
      {% action "shopify" %}
        mutation {
          metafieldDelete(
            input: {
              id: {{ product.metafield.id | json }}
            }
          ) {
            userErrors {
              field
              message
            }
          }
        }
      {% endaction %}
    {% endif %}
  {% endif %}
{% elsif event.topic contains "mechanic/scheduler/" or event.topic == "mechanic/user/trigger" %}
  {% assign now_s = "now" | date: "%s" | times: 1 %}
  {% assign minimum_out_of_stock_at_distance_s = options.number_of_days_to_wait_before_unpublishing__number_required | times: 24 | times: 60 | times: 60 %}

  {% capture query %}
    query {
      publications(first: 250) {
        edges {
          node {
            id
            name
          }
        }
      }
    }
  {% endcapture %}

  {% assign result = query | shopify %}

  {% assign publications = array %}

  {% for publication_edge in result.data.publications.edges %}
    {% if options.sales_channel_names__required_array contains publication_edge.node.name %}
      {% assign publications[publications.size] = publication_edge.node %}
    {% endif %}
  {% endfor%}

  {% if event.preview %}
    {% assign publications[0] = hash %}
    {% assign publications[0]["id"] = "gid://shopify/Publication/1234567890" %}
  {% elsif publications.size != options.sales_channel_names__required_array.size %}
    {% log publications_named: options.sales_channel_names__required_array, publications_available: result.data.publications.edges,  publications_matched: publications %}
    {% error "Unable to find all named publications. Double-check your task configuration." %}
  {% endif %}

  {% assign products_query = "inventory_total:<=0" %}

  {% if options.only_include_products_matching_this_search_query != blank %}
    {% assign products_query = products_query | append: " AND (" | append: options.only_include_products_matching_this_search_query | append: ")" %}
    {% log products_query: products_query %}
  {% endif %}

  {% assign cursor = nil %}

  {% for n in (0..100) %}
    {% capture query %}
      query {
        products(
          first: 250
          query: {{ products_query | json }}
          after: {{ cursor | json }}
        ) {
          pageInfo {
            hasNextPage
          }
          edges {
            cursor
            node {
              id
              title
              totalInventory
              {% for publication in publications %}
                publishedOnPublication{{ forloop.index }}: publishedOnPublication(publicationId: {{ publication.id | json }})
              {% endfor %}
              metafield(
                namespace: "mechanic"
                key: "out_of_stock_at"
              ) {
                id
                value
              }
            }
          }
        }
      }
    {% endcapture %}

    {% assign result = query | shopify %}

    {% if event.preview %}
      {% capture result_json %}
        {
          "data": {
            "products": {
              "pageInfo": {
                "hasNextPage": false
              },
              "edges": [
                {
                  "cursor": "eyJsYXN0X2lkIjo0MzQzOTQxNDMxMzMxLCJsYXN0X3ZhbHVlIjo0MzQzOTQxNDMxMzMxfQ==",
                  "node": {
                    "id": "gid://shopify/Product/1234567890",
                    "title": "Short sleeve t-shirt",
                    "totalInventory": 0,
                    {% for publication in publications %}
                      {{ "publishedOnPublication" | append: forloop.index | json }}: true,
                    {% endfor %}
                    "metafield": {
                      "value": {{ now_s | minus: minimum_out_of_stock_at_distance_s | minus: 1 | append: "" | json }}
                    }
                  }
                }
              ]
            }
          }
        }
      {% endcapture %}

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

    {% for product_edge in result.data.products.edges %}
      {% assign product = product_edge.node %}

      {% if product.metafield == nil %}
        {% log %}
          {% capture message %}Product {{ product.id }} is out of stock, but its out-of-stock time was not recorded. This product will not be unpublished now, but its out-of-stock time will be set to the current time.{% endcapture %}
          {{ message | json }}
        {% endlog %}

        {% unless options.test_mode__boolean %}
          {% action "shopify" %}
            mutation {
              productUpdate(
                input: {
                  id: {{ product.id | json }}
                  metafields: [
                    {
                      namespace: "mechanic"
                      key: "out_of_stock_at"
                      type: "date_time"
                      value: {{ "now" | date: time_format | json }}
                    }
                  ]
                }
              ) {
                userErrors {
                  field
                  message
                }
              }
            }
          {% endaction %}
        {% endunless %}

        {% continue %}
      {% endif %}

      {% assign out_of_stock_at_s = product.metafield.value | date: "%s" | times: 1 %}
      {% assign out_of_stock_at_distance_s = now_s | minus: out_of_stock_at_s %}

      {% assign unpublishings = array %}

      {% for publication in publications %}
        {% assign key = "publishedOnPublication" | append: forloop.index %}
        {% if product[key] %}
          {% assign unpublishing = array %}
          {% assign unpublishing[0] = product.id %}
          {% assign unpublishing[1] = publication.id %}
          {% assign unpublishings[unpublishings.size] = unpublishing %}
        {% endif %}
      {% endfor %}

      {% if unpublishings == empty %}
        {% log %}
          {% capture message %}Product {{ product.title | json }} ({{ product.id }}) has been out of stock for {{ out_of_stock_at_distance_s | divided_by: 60 | divided_by: 60 | divided_by: 24 | round: 2 }} day(s), but is not published - nothing to do.{% endcapture %}
          {{ message | json }}
        {% endlog %}
        {% continue %}
      {% endif %}

      {% if out_of_stock_at_distance_s < minimum_out_of_stock_at_distance_s %}
        {% log %}
          {% capture message %}Product {{ product.title | json }} ({{ product.id }}) is out of stock, and is published, but has only been out of stock for {{ out_of_stock_at_distance_s | divided_by: 60 | divided_by: 60 | divided_by: 24 | round: 2 }} day(s).{% endcapture %}
          {{ message | json }}
        {% endlog %}
        {% continue %}
      {% endif %}

      {% if options.test_mode__boolean %}
        {% action "echo" %}
          {% capture message %}Product {{ product.title | json }} ({{ product.id }}) is out of stock, and has been out of stock for {{ out_of_stock_at_distance_s | divided_by: 60 | divided_by: 60 | divided_by: 24 | round: 2 }} day(s). It should be unpublished.{% endcapture %}
          {{ message | json }}
        {% endaction %}
      {% else %}
        {% action "shopify" %}
          mutation {
            {% for unpublishing in unpublishings %}
              publishableUnpublish{{ forloop.index }}: publishableUnpublish(
                id: {{ unpublishing[0] | json }}
                input: {
                  publicationId: {{ unpublishing[1] | json }}
                }
              ) {
                userErrors {
                  field
                  message
                }
              }
            {% endfor %}
          }
        {% endaction %}
      {% endif %}
    {% endfor %}

    {% if result.data.products.pageInfo.hasNextPage %}
      {% assign cursor = result.data.products.edges.last.cursor %}
    {% else %}
      {% break %}
    {% endif %}
  {% endfor %}
{% endif %}
Task code is written in Mechanic Liquid, an extension of open-source Liquid enhanced for automation. Learn more
Defaults
Sales channel names
["Online Store"]
Test mode
true