Mechanic is a development and ecommerce automation platform for Shopify. :)
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.
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:
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?)
shopify/inventory_levels/update mechanic/scheduler/hourly
{% 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 %}
["Online Store"]
true