A Shopify order cancellation automation in n8n turns a cancelled order into three things that otherwise sit on somebody’s to-do list: a clear email to the customer, a row in a Google Sheets log you can actually audit, and an alert to your ops address when the order shipped before it was cancelled so inventory will not come back on its own. Six nodes, one webhook, no polling. Here is the whole build.
What it does
The workflow listens to the Shopify orders/cancelled webhook. The moment an order is cancelled, whether from the admin, the API, or an app, Shopify pushes the full order payload to n8n and the workflow does three things in parallel.
- Emails the customer a plain confirmation naming the order, the items removed, the amount refunded so far, and the reason on record.
- Appends one row to a Cancellations sheet with eleven columns, so you can count cancellations by reason and by month without exporting anything.
- Checks whether the order was already fulfilled or partially fulfilled at the moment of cancellation. If it was, Shopify will not put that stock back, so the workflow emails your ops address with the exact line items to check.
┌──────────────────────────────────────────────────────────────┐ │ SHOPIFY ORDER CANCELLATION AUTOMATION │ │ │ │ [Order Cancelled] → [Build Cancellation Data] │ │ │ │ │ ┌──────────────┼──────────────┐ │ │ ↓ ↓ ↓ │ │ [Email Customer] [Log To Sheets] [Needs Restock?] │ │ │ true │ │ ↓ │ │ [Alert Ops To Restock] │ └──────────────────────────────────────────────────────────────┘
Why it beats the default
Shopify’s built-in cancellation email is a receipt. It confirms the cancellation and stops there. Three gaps stay open.
The first is the restock gap. When you cancel an order in the Shopify admin you get a restock checkbox, and it only covers items that were never fulfilled. Cancel an order that already shipped and those units stay deducted from your inventory even though the goods may be on their way back to you. Nothing in Shopify tells you this happened. Two weeks later a product shows as out of stock while three units sit in a returns bin. This workflow catches that case on the spot and names the line items.
The second gap is the reason log. Shopify stores cancel_reason on the order, but there is no report that groups cancellations by reason over time. Once every cancellation lands in a sheet, “customer changed mind” versus “inventory” versus “fraud” becomes a number you can act on rather than a hunch.
The third gap is refund clarity. The default email does not tell a customer how much has actually been refunded or when it will land. That single missing sentence is what generates the follow-up support ticket. Saying “refunded so far, typically 5 to 10 business days” up front removes most of them.
What you need
- An n8n instance, cloud or self-hosted, version 1.0 or newer.
- A Shopify credential in n8n. Use the 2026 Dev Dashboard method described in how to connect Shopify to n8n in 2026. The old admin custom-app flow has been removed, so follow the current path. The trigger needs the
read_ordersscope. - A Gmail credential, used for both the customer email and the ops alert. A Google Workspace address on your own domain sends better than a free Gmail address.
- A Google Sheets credential and one spreadsheet with a tab named
Cancellations. - An internal address to receive restock alerts, for example
ops@yourstore.com.
The Cancellations sheet
Create the tab and put these exact headers in row 1. The Google Sheets node maps by column name, so a typo here is the single most common reason rows land blank.
| Column | Example | What it holds |
|---|---|---|
Cancelled At |
2026-07-20T14:22:09-04:00 | Shopify timestamp of the cancellation |
Order Number |
1043 | The customer-facing number |
Order ID |
5390214782033 | Internal id, useful for API lookups |
Customer Email |
emily.rodriguez@outlook.com | Who was notified |
Items |
2 x Cedar Candle, 1 x Linen Throw | Flattened line items |
Order Total |
128.00 | Order value before refunds |
Refund Total |
128.00 | Sum of refund transactions at webhook time |
Currency |
USD | Shop currency code |
Cancel Reason |
customer | customer, inventory, fraud, declined, other |
Fulfillment Status |
fulfilled | Status at the moment of cancellation |
Manual Restock |
TRUE | Whether stock needs a human check |
Node-by-node list
| # | Node name | Type | Job |
|---|---|---|---|
| 1 | Order Cancelled | Shopify Trigger | Subscribes to orders/cancelled and receives the order payload |
| 2 | Build Cancellation Data | Edit Fields (Set) v3.4 | Flattens twelve fields, including a computed refund total and a restock flag |
| 3 | Email Customer Confirmation | Gmail v2.1 | Sends the HTML cancellation email to the buyer |
| 4 | Log Cancellation To Sheets | Google Sheets v4.7 | Appends one row to the Cancellations tab |
| 5 | Needs Manual Restock? | IF v2.2 | Routes on the boolean restock flag |
| 6 | Alert Ops To Restock | Gmail v2.1 | Emails ops the line items to physically check |
Step-by-step build
- Create a new workflow and name it Shopify Order Cancellation Automation.
- Add a Shopify Trigger node, rename it to
Order Cancelled, set Authentication to Access Token, attach your Shopify credential, and set Topic toorders/cancelled. n8n registers the webhook with Shopify when you activate the workflow, so there is nothing to configure on the Shopify side. - Add an Edit Fields node named
Build Cancellation Data. Leave Mode on Manual Mapping and add twelve assignments. The straightforward ones map directly:order_numberto{{ $json.order_number }},order_idto{{ $json.id }},customer_emailto{{ $json.email }},cancelled_at,currency, andorder_totalto{{ $json.total_price }}. - Add
customer_nameas a string with a fallback, because guest checkouts can arrive with no customer object attached:{{ $json.customer && $json.customer.first_name ? $json.customer.first_name : 'there' }} - Add
cancel_reasonas{{ $json.cancel_reason || 'unspecified' }}. Shopify leaves this null when an order is cancelled through the API without a reason, and a null in the email body reads badly. - Add
items_list, which flattens the line items into one readable string used by both emails and the sheet:{{ ($json.line_items || []).map(i => i.quantity + ' x ' + i.title).join(', ') }} - Add
refund_total. Refunds live in a nested array of transactions, so sum them and fix to two decimals:{{ ($json.refunds || []).reduce((sum, r) => sum + (r.transactions || []).reduce((t, x) => t + parseFloat(x.amount || 0), 0), 0).toFixed(2) }} - Add
fulfillment_statusas{{ $json.fulfillment_status || 'unfulfilled' }}. Shopify sends null rather than the string “unfulfilled” when nothing has shipped, which would otherwise break the next expression. - Add
needs_manual_restockand set its type to Boolean, not string. This is the flag the IF node reads:{{ ['fulfilled', 'partial'].includes($json.fulfillment_status) }} - Add a Gmail node named
Email Customer Confirmation. Set To to{{ $json.customer_email }}, Subject toYour order #{{ $json.order_number }} has been cancelled, and Email Type to HTML. Write the body naming the items, the refunded amount, and the 5 to 10 business day window. Under Options, turn off Append n8n attribution. - Add a Google Sheets node named
Log Cancellation To Sheets. Operation is Append Row in Sheet. Pick your spreadsheet, choose theCancellationstab, set Mapping Column Mode to Map Each Column Manually, and fill the eleven columns from the table above. - Add an IF node named
Needs Manual Restock?. Left value{{ $json.needs_manual_restock }}, condition type Boolean, operator is true. Leave the right value empty since is true is a single-value operator. - Add a second Gmail node on the true branch named
Alert Ops To Restock. Send it to your ops address with subjectManual restock needed: order #{{ $json.order_number }}and a body namingitems_list, the fulfillment status, and the refunded amount. Leave the false branch unconnected. - Wire
Build Cancellation Datato all three of the customer email, the Sheets node, and the IF node from its single output. n8n runs the three branches in sequence but each receives the same item, so a Gmail hiccup does not block the sheet row. - Save, then toggle Active. Activation is what registers the webhook with Shopify.
Test with a real but cheap order rather than the n8n test button. Place a $1 order in your store, fulfil it, then cancel it from the admin. That single pass exercises both branches: you should receive the customer email, see a new sheet row, and get the ops alert because the order was already fulfilled.
Common mistakes
Storing the restock flag as a string
If needs_manual_restock is typed as a string, the value arrives as "true" and an IF node running strict type validation refuses to compare it to a boolean. Set the field type to Boolean in the Edit Fields node and the comparison works. This is the single most common failure in this build.
Assuming fulfillment_status is always a string
For an order where nothing shipped, Shopify sends "fulfillment_status": null. Call .includes() on that without the || 'unfulfilled' fallback and the node throws on roughly nine out of ten cancellations, since most cancellations happen before shipping.
Sheet headers that do not match exactly
Order number is not Order Number. The Google Sheets node matches on the exact header string, and a mismatch produces a row with blank cells rather than an error, so it fails quietly for weeks. Copy the headers from the table above.
Expecting a refund amount on every cancellation
Cancelling and refunding are separate operations. An order cancelled without a refund fires the webhook with an empty refunds array, so refund_total is 0.00. The email wording of “refunded so far” keeps that accurate instead of promising money that is not moving.
Deactivating and reactivating repeatedly
Each activation registers a fresh webhook subscription with Shopify. Toggling the workflow on and off many times while testing can leave orphaned subscriptions that deliver duplicate payloads. If you start seeing double emails, list your webhooks through the Admin API and delete the stale ones.
Cost at realistic volume
This workflow is webhook-driven, so it consumes nothing at all on days with no cancellations. That matters because cancellations are rare compared to orders.
| Store volume | Cancellations / month | n8n executions | Monthly cost |
|---|---|---|---|
| 500 orders | ~10 (2%) | 10 | $0 self-hosted, negligible on cloud |
| 2,000 orders | ~50 | 50 | $0 self-hosted |
| 10,000 orders | ~250 | 250 | $0 self-hosted, well inside the n8n Starter plan |
Gmail’s free sending limits are 500 messages a day for a personal account and 2,000 for Workspace, so even the busiest case here uses a fraction of one day’s quota. Google Sheets API quotas are far above this. The realistic cost is your n8n hosting, which you are already paying for if you run any other n8n Shopify automation.
Ready-to-import template
The full guide above is free to follow and the workflow is described exactly as it is built, node for node. If you would rather skip the twelve field mappings and the two email bodies, the ready-to-import JSON drops the finished workflow onto your canvas with credential placeholders already in place and the workflow set inactive. You attach three credentials, paste your spreadsheet id, and switch it on. Want it installed and tested on your store instead? That is what our done-for-you service is for.
Instant download · Works on n8n Cloud and self-hosted
FAQ
Does Shopify restock inventory automatically when an order is cancelled?
Only when you tick the restock box at cancellation time, and only for items that were not yet fulfilled. Anything already shipped stays deducted from inventory, which is exactly the case this workflow flags with a separate email to your ops address.
Will the orders/cancelled webhook fire for orders cancelled by an app?
Yes. The webhook is emitted by Shopify whenever the order moves to a cancelled state, whether that came from the admin, the Admin API, or a third party app acting on your store. The payload shape is identical in all three cases.
Why does refund_total sometimes show 0.00 on a cancelled order?
Cancellation and refund are separate events in Shopify. If you cancelled without refunding, or the refund is processed a moment later, the refunds array is still empty when the webhook fires. The email wording stays accurate because it reports what has been refunded so far.
Can I send the ops alert to Slack or Telegram instead of Gmail?
Yes. Replace the Alert Ops To Restock node with a Slack or Telegram node and map the same fields. Slack v2 needs the channel set as a resource locator rather than a plain string, so pick the channel from the dropdown instead of typing it.
How do I stop the customer email going out for test orders?
Add a Filter node after Build Cancellation Data that drops items where the order tags contain test, or where the customer email matches your own address. Test orders are rare enough that most stores skip this and simply delete the stray email.
Related guides
- How to connect Shopify to n8n in 2026, the Dev Dashboard credential setup this workflow assumes.
- Shopify refund tracker in Google Sheets, which covers the refund side that cancellation only partly overlaps.
- Shopify returns automation with n8n, for goods coming back after delivery rather than orders stopped mid-flight.
- Shopify failed payment recovery email, the other common reason an order never completes.
- The full n8n Shopify automation hub and every post in the Shopify category.