Mechanic is a development and ecommerce automation platform for Shopify. :)
This advanced task allows you to schedule price change events for your store, useful for seasonal and flash sales. To use this task, configure a price change event using the task options, save the task and run it manually, and then enter a keyword of schedule in the dialog that appears. An email summary with the price change event ID will be sent upon successful scheduling.
Runs Occurs when a user manually triggers the task with text, Occurs whenever user/price_changes/start is triggered, Occurs whenever user/price_changes/end is triggered, and Occurs whenever user/price_changes/reset is triggered. Configuration includes notification email recipients, event start datetime, event end datetime, set compare at prices to original price during event, collection handles and discounts, skus to include, sku discount, exclude products tagged with, skus to exclude, and do not restore prices on reset.
This advanced task allows you to schedule price change events for your store, useful for seasonal and flash sales. To use this task, configure a price change event using the task options, save the task and run it manually, and then enter a keyword of schedule in the dialog that appears. An email summary with the price change event ID will be sent upon successful scheduling.
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?)
mechanic/user/text user/price_changes/start user/price_changes/end user/price_changes/reset
{% assign email_recipients = options.notification_email_recipients__array_required %} {% assign event_start_datetime = options.event_start_datetime__required %} {% assign event_end_datetime = options.event_end_datetime__required %} {% assign set_compare_at_prices = options.set_compare_at_prices_to_original_price_during_event__boolean %} {% assign collection_handles_and_discounts = options.collection_handles_and_discounts__keyval %} {% assign skus_to_include = options.skus_to_include__array %} {% assign sku_discount = options.sku_discount %} {% assign exclude_products_tagged_with = options.exclude_products_tagged_with__array %} {% assign skus_to_exclude = options.skus_to_exclude__array %} {% assign do_not_restore_prices_on_reset = options.do_not_restore_prices_on_reset__boolean %} {%- capture task_admin_link -%} <a href="https://{{ shop.myshopify_domain }}/admin/apps/mechanic/~/tasks/edit/{{ task.id }}">{{ task.name | remove: "ADVANCED: " }}</a> {%- endcapture -%} {% comment %} -- query for shop data on every task run, since it will be needed for each valid action keyword and custom event {% endcomment %} {% capture query %} query { shop { id name contactEmail metafield(key: "mechanic.price_change_events") { jsonValue } } } {% endcapture %} {% assign result = query | shopify %} {% if event.preview %} {% capture result_json %} { "data": { "shop": { "id": "gid://shopify/Shop/1234567890", "name": "Shoplify", "contactEmail": "info@example.com", "metafield": { "jsonValue": { "price_change_event__1234567890": { "status": "scheduled", "start": "2025-12-30 08:00", "end": "2025-12-31 20:00", "set_compare_at_prices": true, "collection_handles_and_discounts": { "collection-alpha": "20%", "collection-beta": "-10", "collection-gamma": "20" }, "skus_to_include": [], "sku_discount": "", "skus_to_exclude": [], "exclude_products_tagged_with": [ "clearance" ] } } } } } } {% endcapture %} {% assign result = result_json | parse_json %} {% endif %} {% assign shop = result.data.shop %} {% assign price_change_events = shop.metafield.jsonValue | default: hash %} {% if event.topic == "mechanic/user/text" %} {% comment %} -- check text entry to see what action to take {% endcomment %} {% assign action_keyword = event.data | downcase %} {% if event.preview %} {% assign action_keyword = "schedule" %} {% endif %} {% case action_keyword %} {% when "schedule" %} {% comment %} -- NOTE: run scheduling logic here instead of in custom event like most other keywords, so user gets immediate feedback on any configuration errors {% endcomment %} {% assign valid_start_datetime = event_start_datetime | parse_date: "%Y-%m-%d %H:%M" %} {% if valid_start_datetime == blank %} {% assign valid_start_datetime = event_start_datetime | parse_date: "%Y-%m-%d" %} {% if valid_start_datetime == blank %} {% unless event.preview %} {% error "The event start date is not a valid date. Please re-enter it (per the task documentation) and try scheduling the event again." %} {% break %} {% endunless %} {% endif %} {% endif %} {% assign valid_end_datetime = event_end_datetime | parse_date: "%Y-%m-%d %H:%M" %} {% if valid_end_datetime == blank %} {% assign valid_end_datetime = event_end_datetime | parse_date: "%Y-%m-%d" %} {% if valid_end_datetime == blank %} {% unless event.preview %} {% error "The event end date is not a valid date. Please re-enter it (per the task documentation) and try scheduling the event again." %} {% break %} {% endunless %} {% endif %} {% endif %} {% assign now_s = "now" | date: "%s" %} {% assign start_datetime_s = event_start_datetime | date: "%s" %} {% assign end_datetime_s = event_end_datetime | date: "%s" %} {% if start_datetime_s <= now_s or start_datetime_s >= end_datetime_s %} {% unless event.preview %} {% error "The event start and end dates must be future dates and the start date must be before the end date. Please re-enter them (per the task documentation) and try scheduling the event again." %} {% break %} {% endunless %} {% endif %} {% comment %} -- validate discount entry formats: XX% (percentage discount), X.XX (fixed price), or -X.XX (fixed discount) {% endcomment %} {% assign discounts = collection_handles_and_discounts | values | default: array %} {% if skus_to_include != blank and sku_discount == blank %} {% unless event.preview %} {% error "A SKU discount must be configured when specific SKUs are included." %} {% break %} {% endunless %} {% endif %} {% if sku_discount != blank %} {% assign discounts = discounts | push: sku_discount %} {% endif %} {% for discount in discounts %} {% if discount contains "%" %} {% assign discount_percentage = discount | times: 1 %} {% assign discount_string = discount_percentage | append: "%" %} {% if discount != discount_string %} {% unless event.preview %} {% error message: "Percentage discount entry incorrect. Please update the task configuration (per the task documentation) and try scheduling the event again.", discount: discount %} {% break %} {% endunless %} {% endif %} {% if discount_percentage <= 0 or discount_percentage >= 100 %} {% unless event.preview %} {% error message: "Percentage discount entries should contain an number between 0 and 100. Please update the task configuration (per the task documentation) and try scheduling the event again.", discount: discount %} {% break %} {% endunless %} {% endif %} {% else %} {% assign discount_absolute = discount | times: 1 | abs %} {% if discount_absolute == 0 %} {% unless event.preview %} {% error message: "Fixed price and fixed discount entries must contain an absolute number > 0. Please update the task configuration (per the task documentation) and try scheduling the event again.", discount: discount %} {% break %} {% endunless %} {% endif %} {% endif %} {% endfor %} {% assign price_change_event = hash %} {% assign price_change_event["status"] = "scheduled" %} {% assign price_change_event["start"] = start_datetime_s | date: "%FT%H:%M%z" %} {% assign price_change_event["end"] = end_datetime_s | date: "%FT%H:%M%z" %} {% assign price_change_event["set_compare_at_prices"] = set_compare_at_prices %} {% assign price_change_event["collection_handles_and_discounts"] = collection_handles_and_discounts %} {% assign price_change_event["skus_to_include"] = skus_to_include %} {% assign price_change_event["sku_discount"] = sku_discount %} {% assign price_change_event["skus_to_exclude"] = skus_to_exclude %} {% assign price_change_event["exclude_products_tagged_with"] = exclude_products_tagged_with %} {% log task_options: task.options, price_change_event: price_change_event %} {% assign price_change_event_id = event.id | default: task.id %} {% assign price_change_events[price_change_event_id] = price_change_event %} {% action "shopify" %} mutation { metafieldsSet( metafields: [ { ownerId: {{ shop.id | json }} namespace: "mechanic" key: "price_change_events" value: {{ price_change_events | json | json }} type: "json" } ] ) { metafields { id namespace key type jsonValue owner { ... on Shop { id name myshopifyDomain } } } userErrors { code field message } } } {% endaction %} {% comment %} -- schedule start and end events {% endcomment %} {% action "event" %} { "topic": "user/price_changes/start", "task_id": {{ task.id | json }}, "run_at": {{ start_datetime_s | json }}, "data": { "price_change_event_id": {{ price_change_event_id | json }} } } {% endaction %} {% action "event" %} { "topic": "user/price_changes/end", "task_id": {{ task.id | json }}, "run_at": {{ end_datetime_s | json }}, "data": { "price_change_event_id": {{ price_change_event_id | json }} } } {% endaction %} {% capture email_subject %}New price change event scheduled ({{ price_change_event_id }}){% endcapture %} {% capture email_body %} A new price change event has been scheduled, from the {{ task_admin_link }} task within the Mechanic app. <strong>Price change event ID:</strong> {{ price_change_event_id }} <strong>Status:</strong> {{ price_change_event["status"] }} <strong>Event start:</strong> {{ price_change_event["start"] | date: "%FT%H:%M%z" }} <strong>Event end:</strong> {{ price_change_event["end"] | date: "%FT%H:%M%z" }} <strong>Set compare at price to original price during event:</strong> {{ price_change_event["set_compare_at_prices"] }} <strong>Collection handles and discounts:</strong> {% for keyval in price_change_event["collection_handles_and_discounts"] -%} - {{ keyval[0] }}: {{ keyval[1] }} {% else -%} n/a {%- endfor %} <strong>SKUs to include:</strong> {{ price_change_event["skus_to_include"] | join: ", " | default: "n/a" }} <strong>SKU discount:</strong> {{ price_change_event["sku_discount"] | default: "n/a" }}, <strong>SKUs to exclude:</strong> {{ price_change_event["skus_to_exclude"] | join: ", " | default: "n/a" }} <strong>Exclude products tagged with:</strong> {{ price_change_event["exclude_products_tagged_with"] | join: ", " | default: "n/a" }} <em><strong>Note:</strong> To cancel this event while it is still scheduled or ongoing, use the "cancel" keyword along with the price change event ID when running the task.</em> {% endcapture %} {% action "email" %} { "to": {{ email_recipients | json }}, "subject": {{ email_subject | json }}, "body": {{ email_body | newline_to_br | json }}, "reply_to": {{ shop.contactEmail | json }}, "from_display_name": {{ shop.name | json }} } {% endaction %} {% when "list" %} {% action "echo" price_change_events: price_change_events %} {% when "email" %} {% if price_change_events == blank %} {% assign email_body = "There are currently no configured price change events" %} {% else %} {% capture email_body %} Currently configured price change events, using the {{ task_admin_link }} task within the Mechanic app. {% for price_change_event in price_change_events -%} {%- assign price_change_event_id = price_change_event[0] -%} {%- assign price_change_event_data = price_change_event[1] -%} <strong>Price change event ID:</strong> {{ price_change_event_id }} <strong>Status:</strong> {{ price_change_event_data["status"] }} <strong>Event start:</strong> {{ price_change_event_data["start"] | date: "%FT%H:%M%z" }} <strong>Event end:</strong> {{ price_change_event_data["end"] | date: "%FT%H:%M%z" }} <strong>Set compare at price to original price during event:</strong> {{ price_change_event_data["set_compare_at_prices"] }} <strong>Collection handles and discounts:</strong> {% for keyval in price_change_event_data["collection_handles_and_discounts"] -%} - {{ keyval[0] }}: {{ keyval[1] }} {% else -%} n/a {%- endfor %} <strong>SKUs to include:</strong> {{ price_change_event_data["skus_to_include"] | join: ", " | default: "n/a" }} <strong>SKU discount:</strong> {{ price_change_event_data["sku_discount"] | default: "n/a" }}, <strong>SKUs to exclude:</strong> {{ price_change_event_data["skus_to_exclude"] | join: ", " | default: "n/a" }} <strong>Exclude products tagged with:</strong> {{ price_change_event_data["exclude_products_tagged_with"] | join: ", " | default: "n/a" }} <hr /> {%- endfor %} {% endcapture %} {% endif %} {% action "email" %} { "to": {{ email_recipients | json }}, "subject": "All price change events as of {{ "now" | date: "%F" }}", "body": {{ email_body | newline_to_br | json }}, "reply_to": {{ shop.contactEmail | json }}, "from_display_name": {{ shop.name | json }} } {% endaction %} {% when "reset" %} {% action "event" %} { "topic": "user/price_changes/reset", "task_id": {{ task.id | json }} } {% endaction %} {% else %} {% if action_keyword contains "cancel" %} {% assign price_change_event_id = action_keyword | remove: "cancel " %} {% assign price_change_event = price_change_events[price_change_event_id] %} {% if price_change_event == blank %} {% error "Keyword of 'cancel' entered with an invalid price change event ID. Run task with 'list' keyword to see a list of all configured price change events." %} {% break %} {% endif %} {% if price_change_event.status == "scheduled" %} {% comment %} -- price change event has not started, so can just update it to cancelled and when the start/end events run they will ignore this event {% endcomment %} {% assign price_change_event["status"] = "cancelled" %} {% assign price_change_events[price_change_event_id] = price_change_event %} {% action "shopify" %} mutation { metafieldsSet( metafields: [ { ownerId: {{ shop.id | json }} namespace: "mechanic" key: "price_change_events" value: {{ price_change_events | json | json }} type: "json" } ] ) { metafields { id namespace key type jsonValue owner { ... on Shop { id name myshopifyDomain } } } userErrors { code field message } } } {% endaction %} {% capture email_subject %}Scheduled price change event has been cancelled ({{ price_change_event_id }}){% endcapture %} {% capture email_body %} A scheduled price change event has been cancelled, from the {{ task_admin_link }} task within the Mechanic app. <strong>Price change event ID:</strong> {{ price_change_event_id }} <strong>Status:</strong> {{ price_change_event["status"] }} <strong>Event start:</strong> {{ price_change_event["start"] | date: "%FT%H:%M%z" }} <strong>Event end:</strong> {{ price_change_event["end"] | date: "%FT%H:%M%z" }} <strong>Set compare at price to original price during event:</strong> {{ price_change_event["set_compare_at_prices"] }} <strong>Collection handles and discounts:</strong> {% for keyval in price_change_event["collection_handles_and_discounts"] -%} - {{ keyval[0] }}: {{ keyval[1] }} {% else -%} n/a {%- endfor %} <strong>SKUs to include:</strong> {{ price_change_event["skus_to_include"] | join: ", " | default: "n/a" }} <strong>SKU discount:</strong> {{ price_change_event["sku_discount"] | default: "n/a" }}, <strong>SKUs to exclude:</strong> {{ price_change_event["skus_to_exclude"] | join: ", " | default: "n/a" }} <strong>Exclude products tagged with:</strong> {{ price_change_event["exclude_products_tagged_with"] | join: ", " | default: "n/a" }} {% endcapture %} {% action "email" %} { "to": {{ email_recipients | json }}, "subject": {{ email_subject | json }}, "body": {{ email_body | newline_to_br | json }}, "reply_to": {{ shop.contactEmail | json }}, "from_display_name": {{ shop.name | json }} } {% endaction %} {% elsif price_change_event.status == "ongoing" %} {% comment %} -- price change event in progress, so we have to revert the price changes to properly cancel it {% endcomment %} {% action "event" %} { "topic": "user/price_changes/end", "task_id": {{ task.id | json }}, "data": { "price_change_event_id": {{ price_change_event_id | json }}, "cancel": true } } {% endaction %} {% else %} {% action "echo" message: "Price change event cannot be cancelled because it does not have a status of 'scheduled' or 'ongoing'", price_change_event: price_change_event %} {% endif %} {% else %} {% error message: "Unrecognized action keyword. Action keyword must be one of 'schedule', 'list', 'email', or 'cancel'", action_keyword: action_keyword %} {% endif %} {% endcase %} {% elsif event.topic == "user/price_changes/start" %} {% assign price_change_event_id = event.data.price_change_event_id %} {% assign price_change_event = price_change_events[price_change_event_id] %} {% if event.preview %} {% assign price_change_event_id = "01234567-89ab-cdef" %} {% capture price_change_event_json %} { "status": "scheduled", "start": {{ "now + 1 day" | date: "%FT%H:%M%z" | json }}, "end": {{ "now + 2 days" | date: "%FT%H:%M%z" | json }}, "set_compare_at_prices": false, "collection_handles_and_discounts": { "alpha-beta-gamma": "10%", "sticks-and-stones": "-5.50", "lorem-ipsum": "3" }, "skus_to_include": [ "SKU-123", "SKU-456" ], "sku_discount": "15%", "skus_to_exclude": [ "SKU-987" ], "exclude_products_tagged_with": [ "do-not-discount" ] } {% endcapture %} {% assign price_change_event = price_change_event_json | parse_json %} {% endif %} {% assign status = price_change_event.status %} {% assign set_compare_at_prices = price_change_event.set_compare_at_prices %} {% assign collection_handles_and_discounts = price_change_event.collection_handles_and_discounts %} {% assign skus_to_include = price_change_event.skus_to_include %} {% assign sku_discount = price_change_event.sku_discount %} {% assign exclude_products_tagged_with = price_change_event.exclude_products_tagged_with %} {% assign skus_to_exclude = price_change_event.skus_to_exclude %} {% if status != "scheduled" %} {% log message: "This price change event does not have a status of scheduled, and thus will not start.", price_change_event: price_change_event %} {% break %} {% endif %} {% comment %} -- get collection ids for products query {% endcomment %} {% assign in_collection_checks = array %} {% for keyval in collection_handles_and_discounts %} {% capture query %} query { collectionByHandle(handle: {{ keyval[0] | json }}) { id handle } } {% endcapture %} {% assign result = query | shopify %} {% assign collection = result.data.collectionByHandle %} {% if collection != blank %} {% capture in_collection_check -%} inCollection_{{ collection.handle | replace: "-", "_" }}: inCollection(id: {{ collection.id | json }}) {%- endcapture %} {% assign in_collection_checks = in_collection_checks | push: in_collection_check %} {% endif %} {% endfor %} {% comment %} -- get all products in the shop (up to 50K), unless excluded by tag {% endcomment %} {% assign search_query = "gift_card:false" %} {% for tag in exclude_products_tagged_with %} {% assign search_query = tag | json | prepend: " tag_not:" | prepend: search_query %} {% endfor %} {% log search_query: search_query %} {% assign products = array %} {% assign cursor = nil %} {% for n in (1..200) %} {% capture query %} query { products( first: 250 after: {{ cursor | json }} query: {{ search_query | json }} ) { pageInfo { hasNextPage endCursor } nodes { id title tags variants(first: 100) { nodes { id displayName sku price compareAtPrice metafield(key: "mechanic.price_change_event") { jsonValue } } } {{ in_collection_checks | join: newline }} } } } {% endcapture %} {% assign result = query | shopify %} {% if event.preview %} {% capture result_json %} { "data": { "products": { "nodes": [ { "id": "gid://shopify/Product/1234567890", "variants": { "nodes": [ { "id": "gid://shopify/ProductVariant/1234567890", "sku": "ACME-BRICK-RED", "price": "10.00", "compareAtPrice": "15.00", "metafield": null } ] }, "inCollection_{{ collection_handles_and_discounts.first.first | replace: "-", "_" | default: "sample" }}": true } ] } } } {% endcapture %} {% assign result = result_json | parse_json %} {% endif %} {% assign products = products | concat: result.data.products.nodes %} {% if result.data.products.pageInfo.hasNextPage %} {% assign cursor = result.data.products.pageInfo.endCursor %} {% else %} {% break %} {% endif %} {% endfor %} {% comment %} -- process products to see which qualify for metafield and variant updates {% endcomment %} {% assign metafield_set_inputs = array %} {% assign variant_inputs_by_product = hash %} {% for product in products %} {% comment %} -- For each product, match to a collection discount if applicable, then, if needed, loop through variants to see if there are any overrides {% endcomment %} {% assign product_level_discount = nil %} {% for keyval in collection_handles_and_discounts %} {% assign in_collection_label = "inCollection_" | append: keyval[0] | replace: "-", "_" %} {% if product[in_collection_label] %} {% assign product_level_discount = keyval[1] %} {% break %} {% endif %} {% endfor %} {% if product_level_discount == blank and skus_to_include == blank and skus_to_exclude == blank %} {% comment %} -- skip the product if it is not in a configured collection and there are no inclusion/exclusion SKUs configured {% endcomment %} {% continue %} {% endif %} {% for variant in product.variants.nodes %} {% if skus_to_exclude != blank and skus_to_exclude contains variant.sku %} {% log message: "This variant was excluded by SKU", variant: variant %} {% continue %} {% endif %} {% if variant.metafield != blank %} {% log message: "This variant is already part of a price change event; skipping.", variant: variant %} {% continue %} {% endif %} {% assign discount_to_apply = product_level_discount %} {% if skus_to_include != blank and skus_to_include contains variant.sku %} {% assign discount_to_apply = sku_discount %} {% endif %} {% if discount_to_apply == blank %} {% continue %} {% endif %} {% comment %} -- make sure prices are strings when being passed in GraphQL mutations {% endcomment %} {% assign price_to_set = nil %} {% assign compare_at_price_to_set = nil %} {% if discount_to_apply contains "%" %} {% assign price_to_set = discount_to_apply | remove: "%" | minus: 100 | abs | times: variant.price | divided_by: 100 | round: 2 | append: "" %} {% elsif discount_to_apply contains "-" %} {% assign price_to_set = variant.price | plus: discount_to_apply | at_least: 0.0 | append: "" %} {% else %} {% assign price_to_set = discount_to_apply | append: "" %} {% endif %} {% if set_compare_at_prices %} {% assign compare_at_price_to_set = variant.price %} {% endif %} {% log product: product.title, product_level_discount: product_level_discount, variant: variant.displayName, discount_to_apply: discount_to_apply, price_to_set: price_to_set, compare_at_price_to_set: compare_at_price_to_set %} {% assign price_change_event_data = hash %} {% assign price_change_event_data["price_change_event_id"] = price_change_event_id %} {% assign price_change_event_data["discount_to_apply"] = discount_to_apply %} {% assign price_change_event_data["original_price"] = variant.price %} {% assign price_change_event_data["price_to_set"] = price_to_set %} {% if set_compare_at_prices %} {% assign price_change_event_data["original_compare_at_price"] = variant.compareAtPrice %} {% assign price_change_event_data["compare_at_price_to_set"] = compare_at_price_to_set %} {% endif %} {% assign metafield_set_input = hash %} {% assign metafield_set_input["ownerId"] = variant.id %} {% assign metafield_set_input["namespace"] = "mechanic" %} {% assign metafield_set_input["key"] = "price_change_event" %} {% assign metafield_set_input["type"] = "json" %} {% assign metafield_set_input["value"] = price_change_event_data | json %} {% assign metafield_set_inputs = metafield_set_inputs | push: metafield_set_input %} {% assign variant_input = hash %} {% assign variant_input["id"] = variant.id %} {% assign variant_input["price"] = price_to_set %} {% if set_compare_at_prices %} {% assign variant_input["compareAtPrice"] = variant.price %} {% endif %} {% assign variant_inputs_by_product[product.id] = variant_inputs_by_product[product.id] | default: array | push: variant_input %} {% endfor %} {% endfor %} {% comment %} -- set metafields and then update variants in bulk {% endcomment %} {% if metafield_set_inputs != blank %} {% assign groups_of_metafield_set_inputs = metafield_set_inputs | in_groups_of: 25, fill_with: false %} {% for group_of_metafield_set_inputs in groups_of_metafield_set_inputs %} {% action "shopify" %} mutation { metafieldsSet( metafields: {{ group_of_metafield_set_inputs | graphql_arguments }} ) { metafields { id namespace key type jsonValue owner { ... on ProductVariant { id displayName } } } userErrors { code field message } } } {% endaction %} {% endfor %} {% endif %} {% for keyval in variant_inputs_by_product %} {% action "shopify" %} mutation { productVariantsBulkUpdate( productId: {{ keyval[0] | json }} variants: {{ keyval[1] | graphql_arguments }} ) { product { id title tags } productVariants { id displayName sku price compareAtPrice } userErrors { field message } } } {% endaction %} {% endfor %} {% comment %} -- update the price change event {% endcomment %} {% assign price_change_event["status"] = "ongoing" %} {% assign price_change_events[price_change_event_id] = price_change_event %} {% action "shopify" %} mutation { metafieldsSet( metafields: [ { ownerId: {{ shop.id | json }} namespace: "mechanic" key: "price_change_events" value: {{ price_change_events | json | json }} type: "json" } ] ) { metafields { id namespace key type jsonValue owner { ... on Shop { id name myshopifyDomain } } } userErrors { code field message } } } {% endaction %} {% capture email_subject %}A scheduled price change event has started ({{ price_change_event_id }}){% endcapture %} {% capture email_body %} A scheduled price change event has started, using the {{ task_admin_link }} task within the Mechanic app. <strong>Price change event ID:</strong> {{ price_change_event_id }} <strong>Status:</strong> {{ price_change_event["status"] }} <strong>Event start:</strong> {{ price_change_event["start"] | date: "%FT%H:%M%z" }} <strong>Event end:</strong> {{ price_change_event["end"] | date: "%FT%H:%M%z" }} <strong>Set compare at price to original price during event:</strong> {{ price_change_event["set_compare_at_prices"] }} <strong>Collection handles and discounts:</strong> {% for keyval in price_change_event["collection_handles_and_discounts"] -%} - {{ keyval[0] }}: {{ keyval[1] }} {% else -%} n/a {%- endfor %} <strong>SKUs to include:</strong> {{ price_change_event["skus_to_include"] | join: ", " | default: "n/a" }} <strong>SKU discount:</strong> {{ price_change_event["sku_discount"] | default: "n/a" }}, <strong>SKUs to exclude:</strong> {{ price_change_event["skus_to_exclude"] | join: ", " | default: "n/a" }} <strong>Exclude products tagged with:</strong> {{ price_change_event["exclude_products_tagged_with"] | join: ", " | default: "n/a" }} <em><strong>Note:</strong> To cancel this event while it is ongoing, use the "cancel" keyword along with the price change event ID when running the task.</em> {% endcapture %} {% action "email" %} { "to": {{ email_recipients | json }}, "subject": {{ email_subject | json }}, "body": {{ email_body | newline_to_br | json }}, "reply_to": {{ shop.contactEmail | json }}, "from_display_name": {{ shop.name | json }} } {% endaction %} {% elsif event.topic == "user/price_changes/end" %} {% assign price_change_event_id = event.data.price_change_event_id %} {% assign price_change_event = price_change_events[price_change_event_id] %} {% if event.preview %} {% assign price_change_event_id = "01234567-89ab-cdef" %} {% assign price_change_event = hash %} {% assign price_change_event["status"] = "ongoing" %} {% endif %} {% if event.data.cancel %} {% comment %} -- go ahead and cancel, as status was already checked to be "scheduled" prior to custom event call {% endcomment %} {% assign price_change_event["status"] = "cancelled" %} {% else %} {% comment %} -- since this was a scheduled run, need to make sure the event status is "ongoing" before reverting changes {% endcomment %} {% if price_change_event.status != "ongoing" %} {% log message: "This price change event does not have a status of 'ongoing', and thus will not be reverted.", price_change_event: price_change_event %} {% break %} {% endif %} {% assign price_change_event["status"] = "completed" %} {% endif %} {% comment %} -- To revert the price change event, check every variant in the shop to see if the metafield exists and contains this price change event ID {% endcomment %} {% assign products = array %} {% assign cursor = nil %} {% for n in (1..200) %} {% capture query %} query { products( first: 250 after: {{ cursor | json }} query: "gift_card:false" ) { pageInfo { hasNextPage endCursor } nodes { id title tags variants(first: 100) { nodes { id displayName sku price compareAtPrice metafield(key: "mechanic.price_change_event") { jsonValue } } } } } } {% endcapture %} {% assign result = query | shopify %} {% if event.preview %} {% capture result_json %} { "data": { "products": { "nodes": [ { "id": "gid://shopify/Product/1234567890", "variants": { "nodes": [ { "id": "gid://shopify/ProductVariant/1234567890", "sku": "ACME-BRICK-RED", "price": "7.50", "compareAtPrice": "10.00", "metafield": { "id": "gid://shopify/Metafield/9876543210", "jsonValue": { "price_change_event_id": "01234567-89ab-cdef", "discount_to_apply": "25%", "original_price": "10.00", "price_to_set": "7.50", "original_compare_at_price": "15.00", "compare_at_price_to_set": "7.50" } } } ] } } ] } } } {% endcapture %} {% assign result = result_json | parse_json %} {% endif %} {% assign products = products | concat: result.data.products.nodes %} {% if result.data.products.pageInfo.hasNextPage %} {% assign cursor = result.data.products.pageInfo.endCursor %} {% else %} {% break %} {% endif %} {% endfor %} {% comment %} -- process products to see which qualify for metafield deletion and variant price reversion {% endcomment %} {% assign metafield_delete_inputs = array %} {% assign variant_inputs_by_product = hash %} {% for product in products %} {% assign variants_with_metafield = product.variants.nodes | where: "metafield" %} {% for variant in variants_with_metafield %} {% assign price_change_event_data = variant.metafield.jsonValue %} {% unless price_change_event_data.price_change_event_id == price_change_event_id %} {% log message: "This variant is part of a separate price change event; skipping.", variant: variant %} {% continue %} {% endunless %} {% assign metafield_delete_input = hash %} {% assign metafield_delete_input["ownerId"] = variant.id %} {% assign metafield_delete_input["namespace"] = "mechanic" %} {% assign metafield_delete_input["key"] = "price_change_event" %} {% assign metafield_delete_inputs = metafield_delete_inputs | push: metafield_delete_input %} {% assign variant_input = hash %} {% assign variant_input["id"] = variant.id %} {% assign original_price = price_change_event_data.original_price | append: "" %} {% assign original_compare_at_price = price_change_event_data.original_compare_at_price | append: "" %} {% if original_price != variant.price %} {% assign variant_input["price"] = original_price %} {% endif %} {% if original_compare_at_price != blank and original_compare_at_price != variant.compareAtPrice %} {% assign variant_input["compareAtPrice"] = original_compare_at_price %} {% endif %} {% if variant_input["price"] or variant_input["compareAtPrice"] %} {% assign variant_inputs_by_product[product.id] = variant_inputs_by_product[product.id] | default: array | push: variant_input %} {% endif %} {% endfor %} {% endfor %} {% comment %} -- update variants in bulk and then delete the price change event metafields {% endcomment %} {% for keyval in variant_inputs_by_product %} {% action "shopify" %} mutation { productVariantsBulkUpdate( productId: {{ keyval[0] | json }} variants: {{ keyval[1] | graphql_arguments }} ) { product { id title tags } productVariants { id displayName sku price compareAtPrice } userErrors { field message } } } {% endaction %} {% endfor %} {% if metafield_delete_inputs != blank %} {% assign groups_of_metafield_delete_inputs = metafield_delete_inputs | in_groups_of: 250, fill_with: false %} {% for group_of_metafield_delete_inputs in groups_of_metafield_delete_inputs %} {% action "shopify" %} mutation { metafieldsDelete( metafields: {{ group_of_metafield_delete_inputs | graphql_arguments }} ) { deletedMetafields { ownerId namespace key } userErrors { field message } } } {% endaction %} {% endfor %} {% endif %} {% comment %} -- update the price change event {% endcomment %} {% assign price_change_events[price_change_event_id] = price_change_event %} {% action "shopify" %} mutation { metafieldsSet( metafields: [ { ownerId: {{ shop.id | json }} namespace: "mechanic" key: "price_change_events" value: {{ price_change_events | json | json }} type: "json" } ] ) { metafields { id namespace key type value owner { ... on Shop { id name myshopifyDomain } } } userErrors { code field message } } } {% endaction %} {% comment %} -- send email notification about the price change event status change {% endcomment %} {% capture email_subject %}A price change event has been {{ price_change_event["status"] }} ({{ price_change_event_id }}){% endcapture %} {% capture email_body %} A price change event has been {{ price_change_event["status"] }}, using the {{ task_admin_link }} task within the Mechanic app. <strong>Price change event ID:</strong> {{ price_change_event_id }} <strong>Status:</strong> {{ price_change_event["status"] }} <strong>Event start:</strong> {{ price_change_event["start"] | date: "%FT%H:%M%z" }} <strong>Event end:</strong> {{ price_change_event["end"] | date: "%FT%H:%M%z" }} <strong>Set compare at price to original price during event:</strong> {{ price_change_event["set_compare_at_prices"] }} <strong>Collection handles and discounts:</strong> {% for keyval in price_change_event["collection_handles_and_discounts"] -%} - {{ keyval[0] }}: {{ keyval[1] }} {% else -%} n/a {%- endfor %} <strong>SKUs to include:</strong> {{ price_change_event["skus_to_include"] | join: ", " | default: "n/a" }} <strong>SKU discount:</strong> {{ price_change_event["sku_discount"] | default: "n/a" }}, <strong>SKUs to exclude:</strong> {{ price_change_event["skus_to_exclude"] | join: ", " | default: "n/a" }} <strong>Exclude products tagged with:</strong> {{ price_change_event["exclude_products_tagged_with"] | join: ", " | default: "n/a" }} {% endcapture %} {% action "email" %} { "to": {{ email_recipients | json }}, "subject": {{ email_subject | json }}, "body": {{ email_body | newline_to_br | json }}, "reply_to": {{ shop.contactEmail | json }}, "from_display_name": {{ shop.name | json }} } {% endaction %} {% elsif event.topic == "user/price_changes/reset" %} {% comment %} -- To reset price change events, check every variant in the shop to see if the price change event metafield exists {% endcomment %} {% assign products = array %} {% assign cursor = nil %} {% for n in (1..200) %} {% capture query %} query { products( first: 250 after: {{ cursor | json }} query: "gift_card:false" ) { pageInfo { hasNextPage endCursor } nodes { id title tags variants(first: 100) { nodes { id displayName sku price compareAtPrice metafield(key: "mechanic.price_change_event") { jsonValue } } } } } } {% endcapture %} {% assign result = query | shopify %} {% if event.preview %} {% capture result_json %} { "data": { "products": { "nodes": [ { "id": "gid://shopify/Product/1234567890", "variants": { "nodes": [ { "id": "gid://shopify/ProductVariant/1234567890", "sku": "ACME-BRICK-RED", "price": "7.50", "compareAtPrice": "10.00", "metafield": { "id": "gid://shopify/Metafield/9876543210", "jsonValue": { "price_change_event_id": "01234567-89ab-cdef", "discount_to_apply": "25%", "original_price": "10.00", "price_to_set": "7.50", "original_compare_at_price": "15.00", "compare_at_price_to_set": "7.50" } } } ] } } ] } } } {% endcapture %} {% assign result = result_json | parse_json %} {% endif %} {% assign products = products | concat: result.data.products.nodes %} {% if result.data.products.pageInfo.hasNextPage %} {% assign cursor = result.data.products.pageInfo.endCursor %} {% else %} {% break %} {% endif %} {% endfor %} {% comment %} -- process products to see which qualify for metafield deletion and variant price reversion {% endcomment %} {% assign metafield_delete_inputs = array %} {% assign variant_inputs_by_product = hash %} {% assign variants_with_prices_to_not_reset = array %} {% for product in products %} {% assign variants_with_metafield = product.variants.nodes | where: "metafield" %} {% for variant in variants_with_metafield %} {% assign price_change_event_data = variant.metafield.jsonValue %} {% assign metafield_delete_input = hash %} {% assign metafield_delete_input["ownerId"] = variant.id %} {% assign metafield_delete_input["namespace"] = "mechanic" %} {% assign metafield_delete_input["key"] = "price_change_event" %} {% assign metafield_delete_inputs = metafield_delete_inputs | push: metafield_delete_input %} {% if do_not_restore_prices_on_reset %} {% assign variants_with_prices_to_not_reset = variants_with_prices_to_not_reset | push: variant %} {% continue %} {% endif %} {% assign variant_input = hash %} {% assign variant_input["id"] = variant.id %} {% assign original_price = price_change_event_data.original_price | append: "" %} {% assign original_compare_at_price = price_change_event_data.original_compare_at_price | append: "" %} {% if original_price != variant.price %} {% assign variant_input["price"] = original_price %} {% endif %} {% if original_compare_at_price != blank and original_compare_at_price != variant.compareAtPrice %} {% assign variant_input["compareAtPrice"] = original_compare_at_price %} {% endif %} {% if variant_input["price"] or variant_input["compareAtPrice"] %} {% assign variant_inputs_by_product[product.id] = variant_inputs_by_product[product.id] | default: array | push: variant_input %} {% endif %} {% endfor %} {% endfor %} {% comment %} -- update variant prices in bulk (unless opting not to) and then delete the price change event metafields {% endcomment %} {% if do_not_restore_prices_on_reset %} {% log do_not_restore_prices_on_reset: do_not_restore_prices_on_reset, variants_with_prices_to_not_reset: variants_with_prices_to_not_reset %} {% else %} {% for keyval in variant_inputs_by_product %} {% action "shopify" %} mutation { productVariantsBulkUpdate( productId: {{ keyval[0] | json }} variants: {{ keyval[1] | graphql_arguments }} ) { product { id title tags } productVariants { id displayName sku price compareAtPrice } userErrors { field message } } } {% endaction %} {% endfor %} {% endif %} {% if metafield_delete_inputs != blank %} {% assign groups_of_metafield_delete_inputs = metafield_delete_inputs | in_groups_of: 250, fill_with: false %} {% for group_of_metafield_delete_inputs in groups_of_metafield_delete_inputs %} {% action "shopify" %} mutation { metafieldsDelete( metafields: {{ group_of_metafield_delete_inputs | graphql_arguments }} ) { deletedMetafields { ownerId namespace key } userErrors { field message } } } {% endaction %} {% endfor %} {% endif %} {% comment %} -- delete the price change events shop metafield if needed {% endcomment %} {% if shop.metafield != blank %} {% action "shopify" %} mutation { metafieldsDelete( metafields: [ { ownerId: {{ shop.id | json }} namespace: "mechanic" key: "price_change_events" } ] ) { deletedMetafields { ownerId namespace key } userErrors { field message } } } {% endaction %} {% endif %} {% comment %} -- send email notification about the price change events reset {% endcomment %} {% capture email_subject %}All price change events have been cleared{% endcapture %} {% capture email_body %} {% if do_not_restore_prices_on_reset %} All price change events have been cleared, using the {{ task_admin_link }} task within the Mechanic app. {% else %} All price change events have been cleared and related prices reset, using the {{ task_admin_link }} task within the Mechanic app. {% endif %} {% endcapture %} {% action "email" %} { "to": {{ email_recipients | json }}, "subject": {{ email_subject | json }}, "body": {{ email_body | newline_to_br | json }}, "reply_to": {{ shop.contactEmail | json }}, "from_display_name": {{ shop.name | json }} } {% endaction %} {% endif %}