In this tutorial, we'll set up automations to track delivery statuses and prevent chargebacks. This upgrade can save you time, money, and your store’s reputation by keeping customers happy and proactively addressing shipping issues, all without the need for expensive apps.
Compatible Themes: This code should work on all free Shopify themes (Dawn, Refresh, Craft, Studio, Publisher, Crave, Origin, Taste, Colorblock, Sense, Ride, Spotlight).
Shopify Flow Workflow
- Update number of days to desired threshold
- Update Send internal email
Subject (adjust X to your desired number of days):
{{shop.name}} - You have {{count}} unfulfilled orders older than X days
Message:
The unfulfilled orders (totaling ${{sum}} in value):
<table border="1" cellpadding="5" cellspacing="0" style="margin: 0 auto; text-align: center;">
<thead>
<tr>
<th>Order</th>
<th>Created Date</th>
<th>Items Quantity</th>
<th>Value</th>
<th>Days Since Ordered</th>
</tr>
</thead>
<tbody>
{% for order_item in getOrderData reversed %}
{% assign created_at_sec = order_item.createdAt | date: "%s" %}
{% assign now_sec = scheduledAt | date: "%s" %}
{% assign days_old = now_sec | minus: created_at_sec | divided_by: 86400 %}
{% assign total_items = 0 %}
{% for line_item in order_item.lineItems %}
{% assign total_items = total_items | plus: line_item.quantity %}
{% endfor %}
<tr>
<td>{{ order_item.name }}</td>
<td>{{ order_item.createdAt | date: "%Y-%m-%d at %H:%M" }}</td>
<td>{{ total_items }}</td>
<td>${{ order_item.currentTotalPriceSet.shopMoney.amount }}</td>
<td>{{ days_old }}</td>
</tr>
{% endfor %}
</tbody>
</table>
Mechanic Task
Import Task
{"docs":"WHAT THIS TASK DOES\n===================\nThis task searches your store’s orders based on your chosen filters \n(order status, fulfillment statuses, days since fulfillment, etc.), \nand only includes fulfillments that have at least one tracking number. \nIt then sends you an email with a table of results, every midnight in your \nstore’s local time, or whenever you manually run the task in Mechanic.\n\n\nCONFIGURATION\n===================\nOrder status\n - Set this to “open”, “closed”, “cancelled”, “not_closed”, or “any”.\n\nDelivery statuses\n - Provide a comma-separated list of fulfillment display statuses, such as:\n FULFILLED, IN_TRANSIT, CONFIRMED, DELIVERED, CANCELED, MARKED_AS_FULFILLED,\n and so on.\n\nMinimum days after fulfillment\n - Only include fulfillments that were created at least this many days ago.\n\nSend to email address\n - Specify the email address that will receive the report.\n\nNumber of orders to fetch\n - Sets how many orders you want to pull in total (Mechanic fetches 100 orders\n per “page” internally).\n\n\nEMAIL OUTPUT\n===================\nThe email shows:\n - How many total orders were fetched\n - How many fulfillments matched your criteria\n - A table of each matching fulfillment\n\nFor each row, you’ll see:\n - Order name\n - Order and fulfillment creation dates\n - Days since fulfillment was created\n - Delivery status\n - Tracking number(s) (clickable if a URL is present)\n\n\nHOW IT RUNS\n===================\n1. Mechanic runs this task automatically at midnight (store time), due to\n 'mechanic/scheduler/daily' in the task’s subscriptions.\n2. You can also manually run the task by clicking “Run task” in Mechanic.\n\nIf you want to refine or broaden what gets included, \nadjust the options in the task’s “Options” section.","halt_action_run_sequence_on_error":false,"name":"Orders fulfilled at least X days ago, filtered by selected delivery status","online_store_javascript":null,"order_status_javascript":null,"perform_action_runs_in_sequence":false,"script":"{%- assign orders_to_fetch = options.orders_to_fetch | default: 5000 -%}\n{%- assign pages_to_fetch = orders_to_fetch | divided_by: 100.0 | ceil -%}\n\n{%- assign order_status_option = options.order_status | default: \"any\" -%}\n{%- capture orders_query -%}\n status:{{ order_status_option }}\n{%- endcapture -%} \n\n{%- assign fulfillment_display_status_filter = options.fulfillment_display_status_filter | default: \"ATTEMPTED_DELIVERY,CANCELED,CONFIRMED,DELIVERED,FAILURE,FULFILLED,IN_TRANSIT,LABEL_PRINTED,LABEL_PURCHASED,LABEL_VOIDED,MARKED_AS_FULFILLED,NOT_DELIVERED,OUT_FOR_DELIVERY,PICKED_UP,READY_FOR_PICKUP,SUBMITTED\" -%}\n{%- assign valid_statuses = fulfillment_display_status_filter | replace: \" \", \"\" | split: \",\" -%}\n\n{%- assign now_s = \"now\" | date: \"%s\" | plus: 0 -%}\n{%- assign days_ago = options.minimum_days_after_fulfillment | default: 0 -%}\n{%- assign days_ago_s = days_ago | times: 86400 -%}\n{%- assign days_ago_s = now_s | minus: days_ago_s | plus: 0 -%}\n\n{%- assign cursor = nil -%}\n{%- assign total_orders_retrieved = 0 -%} \n{%- assign matching_orders_count = 0 -%} \n\n{%- assign newest_order_createdAt = \"\" -%}\n{%- assign oldest_order_createdAt = \"\" -%}\n\n {% log \n message: \"Fulfillment display\",\n order_status: order_status_option,\n ordersQuery: orders_query,\n displayStatus: fulfillment_display_status_filter,\n validStatus: valid_statuses,\n daysAgoSeconds: days_ago_s,\n ordersToFetch: orders_to_fetch,\n pagesToFetch: pages_to_fetch\n %}\n\n{%- capture matching_orders_html -%}\n <table border=\"1\" cellpadding=\"5\" cellspacing=\"0\" style=\"margin: 0 auto; text-align: center;\">\n <thead>\n <tr>\n <th>Order</th>\n <th>Order Created</th> \n <th>Fulfillment Created</th>\n <th>Days Since Fulfilled</th>\n <th>Delivery Status</th>\n <th>Tracking Number</th>\n </tr>\n </thead>\n <tbody>\n{%- endcapture -%}\n\n{%- assign last_page = pages_to_fetch | minus: 1 -%}\n{%- for n in (0..last_page) %}\n\n {% capture query %}\n query {\n orders(\n first: 100,\n after: {{ cursor | json }},\n query: {{ orders_query | json }},\n sortKey: CREATED_AT,\n reverse: true\n ) {\n pageInfo {\n hasNextPage\n }\n edges {\n cursor\n node {\n id\n name\n createdAt\n fulfillments {\n createdAt\n displayStatus\n status\n trackingInfo {\n company\n number\n url\n }\n }\n }\n }\n }\n }\n {% endcapture %}\n\n {% assign result = query | shopify %}\n\n {% if result.data == null or result.data.orders == null %}\n {% log message: \"No orders or null data at page \" | append: n %}\n {% break %}\n {% endif %}\n\n {% assign edges = result.data.orders.edges %}\n {% assign total_orders_retrieved = total_orders_retrieved | plus: edges.size %}\n\n {% log \n message: \"Page fetched\",\n page_number: n,\n order_count: edges.size\n %}\n\n {% if n == 0 and edges.size > 0 %}\n {% assign newest_order_createdAt = edges.first.node.createdAt %}\n {% endif %}\n {% if edges.size > 0 %}\n {% assign oldest_order_createdAt = edges.last.node.createdAt %}\n {% endif %}\n\n {% for edge in edges %}\n {% assign order = edge.node %}\n\n {% for f in order.fulfillments %}\n {% assign f_created_s = f.createdAt | date: \"%s\" | plus: 0 %}\n {% assign days_since_fulfilled_s = now_s | minus: f_created_s %}\n {% assign days_since_fulfilled = days_since_fulfilled_s | divided_by: 86400 %}\n\n {% if f_created_s <= days_ago_s \n and valid_statuses contains f.displayStatus\n and f.trackingInfo \n and f.trackingInfo.size > 0 %}\n\n {%- capture track_numbers -%}\n {%- for t in f.trackingInfo %}\n {%- if t.url -%}\n <a href=\"{{ t.url }}\">{{ t.number }}</a>\n {%- else -%}\n {{ t.number }}\n {%- endif -%}\n {%- unless forloop.last %}<br>{% endunless %}\n {%- endfor %}\n {%- endcapture -%}\n\n\n {%- capture matching_orders_html -%}\n {{ matching_orders_html }}\n <tr>\n <td>{{ order.name }}</td>\n <td>{{ order.createdAt | date: \"%Y-%m-%d at %H:%M:%S\" }}</td>\n <td>{{ f.createdAt | date: \"%Y-%m-%d at %H:%M:%S\" }}</td>\n <td>{{ days_since_fulfilled }}</td>\n <td>{{ f.displayStatus }}</td>\n <td>{{ track_numbers }}</td>\n </tr>\n {%- endcapture -%}\n\n {% assign matching_orders_count = matching_orders_count | plus: 1 %}\n\n {% log \n message: \"Found fulfillment\",\n order_name: order.name,\n order_createdAt: order.createdAt, \n fulfillment_displayStatus: f.displayStatus,\n fulfillment_status: f.status,\n fulfillment_createdAt: f.createdAt\n %}\n\n {% break %}\n {% endif %}\n {% endfor %}\n {% endfor %}\n\n {% if result.data.orders.pageInfo.hasNextPage %}\n {% assign cursor = edges.last.cursor %}\n {% else %}\n {% log message: \"No more pages; stopping pagination.\" %}\n {% break %}\n {% endif %}\n{% endfor %}\n\n{%- capture matching_orders_html -%}\n {{ matching_orders_html }}\n </tbody>\n </table>\n{%- endcapture -%}\n\n{% if matching_orders_html contains \"<tr>\" %}\n\n {%- assign today_str = \"now\" | date: \"%Y-%m-%d\" -%} \n {%- capture final_subject -%}\n {{ shop.name }} ({{ today_str }}): {{ matching_orders_count }} orders fulfilled at least {{ days_ago }} days ago filtered by delivery status\n {%- endcapture -%}\n\n {%- if newest_order_createdAt == \"\" -%}\n {%- assign newest_formatted = \"N/A\" -%}\n {%- else -%}\n {%- assign newest_formatted = newest_order_createdAt | date: \"%Y-%m-%d at %H:%M:%S\" -%}\n {%- endif -%}\n\n {%- if oldest_order_createdAt == \"\" -%}\n {%- assign oldest_formatted = \"N/A\" -%}\n {%- else -%}\n {%- assign oldest_formatted = oldest_order_createdAt | date: \"%Y-%m-%d at %H:%M:%S\" -%}\n {%- endif -%}\n\n {%- capture email_body -%}\n Fetched {{ total_orders_retrieved }} total orders, from {{ newest_formatted }} down to {{ oldest_formatted }},\n and {{ matching_orders_count }} fulfillments matched the criteria (only orders that had at least one tracking number):\n <ul>\n <li>Order status: <strong>{{ order_status_option }}</strong></li>\n <li>Delivery statuses: <strong>{{ fulfillment_display_status_filter }}</strong></li>\n <li>Minimum days since fulfilled: <strong>{{ days_ago }}</strong></li>\n </ul>\n <br>\n {{ matching_orders_html }}\n {%- endcapture -%}\n\n {% action \"email\" %}\n {\n \"to\": {{ options.send_to_email_address__required | json }},\n \"subject\": {{ final_subject | json }},\n \"body\": {{ email_body | json }}\n }\n {% endaction %}\n{% else %}\n {% log message: \"No orders matched the criteria.\" %}\n{% endif %}\n","shopify_api_version":"2024-10","subscriptions_template":"mechanic/user/trigger\nmechanic/scheduler/daily","subscriptions":["mechanic/user/trigger","mechanic/scheduler/daily"],"preview_event_definitions":[],"options":{"orders_to_fetch":"5000","order_status":"any","fulfillment_display_status_filter":"FULFILLED,CONFIRMED","minimum_days_after_fulfillment":"3","send_to_email_address__required":"youremail@yourdomain.com"}}
Modify Options:
- Minimum days after fulfillment
- Sent to email address