Mechanic is a development and ecommerce automation platform for Shopify. :)
Use this task to keep you and your team updated when products go out of stock - and, optionally, when they go back in stock. Filter with a simple product search to only track products you care about. Choose a custom stock threshold to get a heads up before the stock level reaches zero.
Runs Occurs whenever an inventory level is updated and Occurs when a user manually triggers the task. Configuration includes out of stock inventory quantity, only monitor products matching this search query, send email for out of stock products, send email for back in stock products, and stock update email recipients.
Use this task to keep you and your team updated when products go out of stock - and, optionally, when they go back in stock. Filter with a simple product search to only track products you care about. Choose a custom stock threshold to get a heads up before the stock level reaches zero.
Run this task manually to scan all products up front. While this task normally remembers which products were out (or in) stock, running this task manually will always result in an email summary of out of stock products.
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/user/trigger
{% assign out_of_stock_inventory_quantity = options.out_of_stock_inventory_quantity__number_required %} {% assign search_query = options.only_monitor_products_matching_this_search_query | default: "" %} {% assign metafield_key = task.id | append: search_query | append: out_of_stock_inventory_quantity | sha256 | slice: 0, 7 | append: "-out-of-stock" %} {% if event.topic contains "shopify/inventory_levels/" %} {% capture query %} query { inventoryLevel( id: {{ inventory_level.admin_graphql_api_id | json }} ) { item { variant { product { legacyResourceId } } } } } {% endcapture %} {% assign result = query | shopify %} {% comment %} -- send the product ID through the paginated query so any configured search query can be applied to it {% endcomment %} {%- capture search_query -%} {{ search_query }} id:{{ result.data.inventoryLevel.item.variant.product.legacyResourceId }} {%- endcapture -%} {% endif %} {% assign back_in_stock_email_list = array %} {% assign newly_back_in_stock_email_product_title = nil %} {% assign newly_back_in_stock_product_ids = array %} {% assign newly_out_of_stock_product_ids = array %} {% assign out_of_stock_email_list = array %} {% assign out_of_stock_email_product_title = nil %} {% assign cursor = nil %} {% for n in (0..100) %} {% capture query %} query { products( first: 250 query: {{ search_query | json }} after: {{ cursor | json }} ) { pageInfo { hasNextPage endCursor } nodes { id legacyResourceId title totalInventory metafield( namespace: "mechanic" key: {{ metafield_key | json }} ) { id } } } } {% endcapture %} {% assign result = query | shopify %} {% for product in result.data.products.nodes %} {% if product.totalInventory <= out_of_stock_inventory_quantity %} {% if product.metafield == blank or event.topic == "mechanic/user/trigger" %} {% log %} "{{ product.title }} is out of stock." {% endlog %} {% assign out_of_stock_email_product_title = product.title %} {% if product.metafield == blank %} {% comment %} For a manual run, we always send an email about out-of-stock products. But, we don't need to consider these products *newly* out of stock. {% endcomment %} {% assign newly_out_of_stock_product_ids[newly_out_of_stock_product_ids.size] = product.id %} {% endif %} {%- capture list_item -%} <li><a href="{{ shop.admin_url }}/products/{{ product.legacyResourceId }}">{{ product.title }}</a></li> {%- endcapture -%} {% assign out_of_stock_email_list = out_of_stock_email_list | push: list_item %} {% endif %} {% else %} {% if product.metafield %} {% log %} "{{ product.title }} is back in stock." {% endlog %} {% assign newly_back_in_stock_email_product_title = product.title %} {% assign newly_back_in_stock_product_ids[newly_back_in_stock_product_ids.size] = product.id %} {%- capture list_item -%} <li><a href="{{ shop.admin_url }}/products/{{ product.legacyResourceId }}">{{ product.title }}</a></li> {%- endcapture -%} {% assign back_in_stock_email_list = back_in_stock_email_list | push: list_item %} {% endif %} {% endif %} {% endfor %} {% if result.products.pageInfo.hasNextPage %} {% assign cursor = result.data.products.pageInfo.endCursor %} {% else %} {% break %} {% endif %} {% endfor %} {% if event.preview %} {% assign out_of_stock_email_list[back_in_stock_email_list.size] = '<li><a>Premium T-Shirt</a></li>' %} {% assign newly_out_of_stock_product_ids[newly_out_of_stock_product_ids.size] = 'gid://shopify/Product/1234567890' %} {% assign out_of_stock_email_product_title = 'Premium T-Shirt' %} {% endif %} {% log newly_out_of_stock_product_count: newly_out_of_stock_product_ids.size, newly_back_in_stock_product_count: newly_back_in_stock_product_ids.size %} {% comment %} -- save mutation inputs for setting and deleting metafields {% endcomment %} {% assign metafields_to_set = array %} {% assign metafields_to_delete = array %} {% for product_id in newly_out_of_stock_product_ids %} {% assign metafield_to_set = hash %} {% assign metafield_to_set["ownerId"] = product_id %} {% assign metafield_to_set["namespace"] = "mechanic" %} {% assign metafield_to_set["key"] = metafield_key %} {% assign metafield_to_set["type"] = "number_integer" %} {% assign metafield_to_set["value"] = "1" %} {% assign metafields_to_set = metafields_to_set | push: metafield_to_set %} {% endfor %} {% for product_id in newly_back_in_stock_product_ids %} {% assign metafield_to_delete = hash %} {% assign metafield_to_delete["ownerId"] = product_id %} {% assign metafield_to_delete["namespace"] = "mechanic" %} {% assign metafield_to_delete["key"] = metafield_key %} {% assign metafields_to_delete = metafields_to_delete | push: metafield_to_delete %} {% endfor %} {% if options.send_email_for_out_of_stock_products__boolean != true %} {% assign out_of_stock_email_list = array %} {% endif %} {% if options.send_email_for_back_in_stock_products__boolean != true %} {% assign back_in_stock_email_list = array %} {% endif %} {% if out_of_stock_email_list.size > 0 or back_in_stock_email_list.size > 0 %} {% capture email_subject %} {% if out_of_stock_email_list.size == 1 and back_in_stock_email_list.size == 0 %} Out of stock: {{ out_of_stock_email_product_title }} {% elsif back_in_stock_email_list.size == 1 and out_of_stock_email_list.size == 0 %} Back in stock: {{ newly_back_in_stock_email_product_title }} {% elsif out_of_stock_email_list.size > 1 and back_in_stock_email_list.size == 0 %} Out of stock: {{ out_of_stock_email_list.size }} products {% elsif back_in_stock_email_list.size > 1 and out_of_stock_email_list.size == 0 %} Back in stock: {{ out_of_stock_email_list.size }} products {% else %} Stock update: {{ out_of_stock_email_list.size }} out of stock, {{ back_in_stock_email_list.size }} back in stock {% endif %} {% endcapture %} {% capture email_body %} Hi there, <br><br> {% if out_of_stock_email_list.size > 0 %} We found <b>{{ out_of_stock_email_list.size }} out of stock {{ out_of_stock_email_list.size | pluralize: 'product', 'products' }}</b>: <br> <ul>{{ out_of_stock_email_list | join: "" }}</ul> <br> {% endif %} {% if back_in_stock_email_list.size > 0 %} We found <b>{{ back_in_stock_email_list.size }} back in stock {{ back_in_stock_email_list.size | pluralize: 'product', 'products' }}</b>: <br> <ul>{{ back_in_stock_email_list | join: "" }}</ul> <br> {% endif %} Thanks, <br>Mechanic (for {{ shop.name }}) {% endcapture %} {% action "email" %} { "to": {{ options.stock_update_email_recipients__email_array_required | join: ", " | json }}, "subject": {{ email_subject | strip | json }}, "body": {{ email_body | unindent | strip | json }}, "reply_to": {{ shop.customer_email | json }}, "from_display_name": {{ shop.name | json }} } {% endaction %} {% endif %} {% comment %} -- metafields may be set 25 at a time {% endcomment %} {% if metafields_to_set != blank %} {% assign groups_of_metafields_to_set = metafields_to_set | in_groups_of: 25, fill_with: false %} {% for group_of_metafields_to_set in groups_of_metafields_to_set %} {% action "shopify" %} mutation { metafieldsSet( metafields: {{ group_of_metafields_to_set | graphql_arguments }} ) { metafields { id namespace key type value owner { ... on Product { id } } } userErrors { code field message } } } {% endaction %} {% endfor %} {% endif %} {% comment %} -- metafields may be deleted in batches of 250 {% endcomment %} {% if metafields_to_delete != blank %} {% assign groups_of_metafields_to_delete = metafields_to_delete | in_groups_of: 250, fill_with: false %} {% for group_of_metafields_to_delete in groups_of_metafields_to_delete %} {% action "shopify" %} mutation { metafieldsDelete( metafields: {{ group_of_metafields_to_delete | graphql_arguments }} ) { deletedMetafields { ownerId namespace key } userErrors { field message } } } {% endaction %} {% endfor %} {% endif %}
10
shirt
true
true