HomeShopify & E-commerceShopify order cancellation automation with n8n

Shopify order cancellation automation with n8n

Shopify order cancellation automation with n8n







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.

  1. Emails the customer a plain confirmation naming the order, the items removed, the amount refunded so far, and the reason on record.
  2. Appends one row to a Cancellations sheet with eleven columns, so you can count cancellations by reason and by month without exporting anything.
  3. 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_orders scope.
  • 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

  1. Create a new workflow and name it Shopify Order Cancellation Automation.
  2. Add a Shopify Trigger node, rename it to Order Cancelled, set Authentication to Access Token, attach your Shopify credential, and set Topic to orders/cancelled. n8n registers the webhook with Shopify when you activate the workflow, so there is nothing to configure on the Shopify side.
  3. Add an Edit Fields node named Build Cancellation Data. Leave Mode on Manual Mapping and add twelve assignments. The straightforward ones map directly: order_number to {{ $json.order_number }}, order_id to {{ $json.id }}, customer_email to {{ $json.email }}, cancelled_at, currency, and order_total to {{ $json.total_price }}.
  4. Add customer_name as 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' }}
  5. Add cancel_reason as {{ $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.
  6. 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(', ') }}
  7. 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) }}
  8. Add fulfillment_status as {{ $json.fulfillment_status || 'unfulfilled' }}. Shopify sends null rather than the string “unfulfilled” when nothing has shipped, which would otherwise break the next expression.
  9. Add needs_manual_restock and set its type to Boolean, not string. This is the flag the IF node reads:
    {{ ['fulfilled', 'partial'].includes($json.fulfillment_status) }}
  10. Add a Gmail node named Email Customer Confirmation. Set To to {{ $json.customer_email }}, Subject to Your 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.
  11. Add a Google Sheets node named Log Cancellation To Sheets. Operation is Append Row in Sheet. Pick your spreadsheet, choose the Cancellations tab, set Mapping Column Mode to Map Each Column Manually, and fill the eleven columns from the table above.
  12. 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.
  13. Add a second Gmail node on the true branch named Alert Ops To Restock. Send it to your ops address with subject Manual restock needed: order #{{ $json.order_number }} and a body naming items_list, the fulfillment status, and the refunded amount. Leave the false branch unconnected.
  14. Wire Build Cancellation Data to 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.
  15. 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.

Download the template ($13) →

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