Shopify returns automation in n8n (RMA request to Sheets log)

Shopify returns automation workflow in n8n









This Shopify returns automation with n8n turns a messy returns inbox into a clean, trackable process. A hosted return request form verifies the order against the Shopify Admin API, tags the order Return requested, appends the request to a Google Sheets RMA log, and emails the customer an acknowledgment. No returns app subscription, no manual order lookups, and every request recorded in a sheet you own. Build time is about 40 minutes.

Prefer to skip the build? Grab the ready-made template from the CTA below and be running in under 10 minutes.

What it does

Returns are the part of running a store that quietly eats hours. A customer emails “I want to send this back,” and now someone has to find the order, confirm it exists, decide if it qualifies, tag it so fulfillment knows, write the reply, and note it somewhere so nothing slips. Do that thirty times a week and you have lost most of a workday to copy-paste.

This workflow takes the intake off your plate. A customer fills in a short return request form. n8n looks the order up in Shopify to confirm it is real, then tags the matching order with Return requested so it shows up in a saved Shopify view for your fulfillment team. It writes a row to a Google Sheets RMA log with a return number, the reason, and a status of Requested. Finally it emails the customer a friendly acknowledgment with their RMA number and what happens next.

What it deliberately does not do is issue money. Refunds and replacements stay a human decision inside Shopify. The automation removes the busywork around that decision, not the decision itself. This sits in the same family as the refund tracker, which logs refunds after the fact; this one runs at the front of the process, when the request first arrives.

Why it beats the default

The default is one of two things: a shared inbox where return requests get lost between order confirmations, or a paid returns app that charges a monthly fee and still keeps your data locked inside its own dashboard.

A plain inbox has no structure. There is no single place to see every open return, no automatic tagging, and no guarantee the customer even got a reply. Requests fall through when things get busy, and busy is exactly when returns spike.

A dedicated returns app fixes the structure but adds a recurring cost and another login, and the return records live in a system you do not control. If you cancel the app, you often lose easy access to the history.

This n8n approach gives you the structure without the lock-in. Tagging happens automatically, every request lands in a spreadsheet you can filter and export, the customer always gets a same-minute acknowledgment, and the whole thing runs on tools most stores already use. You own the data and the logic end to end. For the wider picture of what you can wire into your store this way, see the n8n Shopify automation guide.

What you need

  • An n8n instance (Cloud or self-hosted), version 1.0 or newer.
  • A Shopify store with Admin API access. Set this up with the 2026 Dev Dashboard method described in connect Shopify to n8n. The custom app needs read_orders and write_orders scopes.
  • A Google account for Google Sheets, connected in n8n by OAuth2.
  • A Gmail account for the customer acknowledgment email, also connected by OAuth2.
  • A blank Google Sheet with a header row for the RMA log (columns listed further down).

The workflow uses the Shopify Admin API version 2026-04. No returns app, no third-party service, no blocked messaging channels.

Node-by-node list

Eight nodes, one clean path with a single branch for unmatched orders.

# Node Type Job
1 Return request form Form Trigger Hosted form that collects order number, email, item, and reason
2 Find the order HTTP Request (GET) Looks up the order in Shopify by name to confirm it exists
3 Order found? If Routes to the return flow only if an order matched
4 Build the RMA record Edit Fields Creates the return number and merges the new tag with existing tags
5 Tag the order HTTP Request (PUT) Writes the merged tag list back to the order in Shopify
6 Log the request Google Sheets (Append) Adds one row to the RMA log with status Requested
7 Email the customer Gmail (Send) Sends the acknowledgment with the RMA number
8 Flag unmatched request Gmail (Send) False branch: alerts support when no order matched
┌──────────────────────────────────────────────────────────────────┐
│  SHOPIFY RETURNS AUTOMATION (n8n)                                   │
│                                                                    │
│  [Return request form] → [Find the order] → [Order found?]         │
│                                                  │ true             │
│                                                  ▼                  │
│                           [Build RMA] → [Tag order] → [Sheets log]  │
│                                                          │          │
│                                                          ▼          │
│                                                  [Email customer]   │
│                                                  │ false            │
│                                                  ▼                  │
│                                          [Flag unmatched → support] │
└──────────────────────────────────────────────────────────────────┘
  

Step-by-step build

1 Return request form (Form Trigger)

Add a Form Trigger node. This gives you a hosted URL you can link from your store. Set the form title to “Request a return” and add these fields: Order number (text, required), Email (email, required), Product name (text), Reason (dropdown: Damaged, Wrong item, Changed mind, Wrong size, Other), and Comments (textarea). When a customer submits, the node outputs one item shaped like this:

{
  "Order number": "1042",
  "Email": "emily.rodriguez@gmail.com",
  "Product name": "Cedar Trail Running Jacket",
  "Reason": "Wrong size",
  "Comments": "Ordered medium, need large."
}
💡

Ask customers to enter the order number without the #. The Admin API matches on the numeric name, so 1042 is cleaner than #1042.

2 Find the order (HTTP Request, GET)

Add an HTTP Request node set to GET. This confirms the order is real and pulls the existing tags so you do not overwrite them. Point it at your store:

GET https://YOUR-STORE.myshopify.com/admin/api/2026-04/orders.json
      ?name={{ $json["Order number"] }}&status=any&fields=id,name,email,tags

Use Header Auth for the credential, with header X-Shopify-Access-Token set to your Admin API access token. The response returns an orders array. A match looks like this:

{
  "orders": [
    { "id": 5218844733, "name": "#1042", "email": "emily.rodriguez@gmail.com", "tags": "VIP" }
  ]
}

3 Order found? (If)

Add an If node. Create one condition using the number comparison “is not empty” or “larger than 0” on the array length:

{{ $json.orders.length }}   →   larger than   →   0

The true output carries on to the return flow. The false output goes to the support-alert email in step 8.

4 Build the RMA record (Edit Fields)

Add an Edit Fields (Set) node. This is where you assemble everything the later nodes need. Add four assignments:

  • order_id (number): {{ $json.orders[0].id }}
  • rma_number (string): {{ "RMA-" + $json.orders[0].name.replace("#","") }}
  • merged_tags (string): {{ ($json.orders[0].tags ? $json.orders[0].tags + ", " : "") + "Return requested" }}
  • customer_email (string): {{ $('Return request form').item.json.Email }}
📌

The merged_tags expression keeps any existing tags (like VIP) and appends Return requested. Sending only the new tag would erase the order’s current tags.

5 Tag the order (HTTP Request, PUT)

Add a second HTTP Request node set to PUT. Send the merged tag list back to the order. In the body section, set Body Content Type to JSON and paste this into the JSON body field:

{
  "order": {
    "id": {{ $json.order_id }},
    "tags": "{{ $json.merged_tags }}"
  }
}
PUT https://YOUR-STORE.myshopify.com/admin/api/2026-04/orders/{{ $json.order_id }}.json

Reuse the same Header Auth credential. After this runs, the order in Shopify shows the Return requested tag, and you can build a saved order view filtered on that tag for your team.

6 Log the request (Google Sheets, Append)

Add a Google Sheets node, operation Append. Point it at your RMA log sheet and map the columns to the fields you built. Set Status to the literal Requested and Date to {{ $now.format('yyyy-MM-dd HH:mm') }}. This gives you a single filterable record of every return that has ever come in.

7 Email the customer (Gmail, Send)

Add a Gmail node, Send operation. Set To to {{ $('Build the RMA record').item.json.customer_email }}, a subject like Your return request {{ $('Build the RMA record').item.json.rma_number }}, and a short friendly body confirming you received the request and will follow up with next steps. Same-minute acknowledgment is what stops the “did you get my email?” follow-ups.

8 Flag unmatched request (Gmail, Send)

Connect a second Gmail node to the false output of the If node. Send it to your support address with the submitted order number and email so a person can reach out. This catches typos and orders from a different sales channel without polluting the returns log.

The RMA log columns

Column Example Description
Date 2026-07-18 14:30 When the request came in
RMA number RMA-1042 Reference given to the customer
Order #1042 The Shopify order name
Email emily.rodriguez@gmail.com Customer contact
Product Cedar Trail Running Jacket Item being returned
Reason Wrong size Why the customer is returning
Status Requested Requested, Approved, Received, or Refunded

Common mistakes

  • Overwriting tags. The single most common error is sending only Return requested in the PUT body, which wipes existing tags. Always merge with the current tags from step 2.
  • Wrong order lookup field. The Admin API matches the storefront number on name, not id. Searching by id with a customer-facing number returns nothing.
  • Skipping the If node. Without the order-exists check, a typo creates a phantom RMA row and a confusing customer email. The branch keeps your data honest.
  • Trusting the form for money. Do not wire an automatic refund onto this. Keep a human between the request and the refund. The tag and the sheet exist so that review is fast, not skipped.
  • Forgetting the API scope. A PUT that returns 403 almost always means the app is missing write_orders. Add the scope and reinstall the app.

Cost at realistic volume

Say your store handles 300 return requests a month, which is a healthy volume for a mid-size store.

  • Shopify Admin API: free. Each request makes two calls (one GET, one PUT), so 600 calls a month, far under the rate limits.
  • Google Sheets and Gmail: free at this scale on a standard Google account. 300 appends and 300 emails is nowhere near any cap.
  • n8n: free on self-hosted. On n8n Cloud, 300 runs a month sits comfortably inside the Starter execution allowance.

So the running cost is effectively zero, versus a dedicated returns app that typically starts around ten to thirty dollars a month and scales up with volume. Over a year that is a few hundred dollars saved, plus the hours you get back from not doing manual order lookups.

🚀 Get the Shopify returns automation template

The guide above is free to follow. If you would rather skip the build, download the ready-to-import workflow, then just add your credentials. Prefer it installed and tuned for you? See our done-for-you services.

Download the template ($14) →

Instant download · Works on n8n Cloud and self-hosted

Frequently asked questions

Does this Shopify returns automation approve or reject refunds?

No. It handles the intake side: it captures the request, confirms the order is real, tags the order, logs it, and emails the customer. A human reviews each tagged order and issues the refund or replacement inside Shopify, so money decisions stay with a person.

Do I need a paid Shopify returns app for this?

No. The workflow uses the standard Shopify Admin API to look up and tag orders, plus an n8n form, Google Sheets, and Gmail. You avoid the monthly app fee and keep every return record in a spreadsheet you own and can export at any time.

How does the customer submit a return request?

The n8n Form Trigger gives you a hosted URL you link from your store footer, order confirmation email, or a help page. The customer enters their order number, email, item, and reason. No account or login is required, so it takes under a minute.

What happens if the order number does not match?

An If node checks whether the Admin API returned an order. If none matched, the false branch emails your support inbox so a person can follow up. Nothing gets tagged or logged as a valid return, which keeps your returns sheet clean and trustworthy.

Can I route returns to different teams by reason?

Yes. Swap the single If node for a Switch node keyed on the reason field. Damaged items can email your warehouse, wrong-item requests can go to fulfillment, and change-of-mind requests can go to support. The rest of the tag-log-email chain stays the same.

Related guides

n8n
Shopify
Google Sheets
Gmail
returns
automation

Shopify Inventory Reorder Point Forecast With n8n and Google Sheets

Shopify inventory reorder point forecast workflow in n8n











A shopify inventory reorder point forecast google sheets n8n workflow turns 60 days of paid orders into a sales velocity for every SKU, compares that against stock on hand, and tells you which products to reorder this week and how many units to buy. Eight nodes, one Google Sheet, one Monday morning email. No forecasting app and no per-SKU fee. Below: the node map, the validated build, the mistakes that quietly produce wrong numbers, and the real cost.

Prefer to skip the setup? Grab the ready-made template and be running in under 10 minutes.

What it does

Every Monday at 7am the workflow pulls your paid orders from the last 60 days, counts the units sold per SKU, and divides by the window to get a daily sales velocity. It then reads current stock for every tracked variant and asks one question per SKU: at this rate of sale, will the shelf empty before a new delivery could arrive?

The answer comes from the reorder point, which is the velocity multiplied by your supplier lead time plus a safety buffer. If stock has fallen to or below that number, the SKU gets flagged with a suggested order quantity. Everything lands in a Google Sheet, one row per SKU, and the flagged rows arrive in your inbox as a table you can forward to a supplier.

Take a black tee that sold 60 units in 60 days, so one a day. Your supplier takes 14 days and you want a 7 day buffer, putting the reorder point at 21 units. Stock reads 14, already past the line, so the workflow tells you to order 14 units.

Why it beats the default

Shopify tells you how many units you have. It does not tell you how long they will last, and that is the number a purchase order actually needs.

The common substitute is a low stock alert on a fixed threshold, set once and never revisited. A fixed number means different things for different products. Ten units of something selling five a day is a two day emergency. Ten units of something selling five a year is nine years of dead capital. One threshold cannot describe both, so alerts cry wolf on slow movers while fast movers sell out in silence. The Shopify low stock alert is the right tool when you just want to know a shelf is thin. This is the other job: knowing when to buy.

Forecasting apps solve it for 30 to 200 dollars a month, often billed per SKU, and hide the arithmetic behind a dashboard. The arithmetic is division. Once your lead time and buffer are two numbers in one node, you can change the assumptions in ten seconds and watch the whole catalog rerank.

What you need

  • An n8n instance, cloud or self-hosted. A free self-hosted install handles this comfortably.
  • A Shopify store connected to n8n the 2026 way, with read access to orders and products. See the 2026 connection guide.
  • A Google account for Sheets and Gmail, connected to n8n by OAuth.
  • One empty Google Sheet with a header row, described in step 2 below.
  • Inventory tracking switched on in Shopify for the products you care about. Untracked variants are skipped, because a variant with no stock number cannot have a reorder point.
  • Your real supplier lead time in days. The workflow cannot derive this, and it decides whether the output is useful.
  • Roughly 25 minutes.

No AI model is involved. A trailing average is arithmetic, and arithmetic does not need a language model. Anything selling you an LLM to divide units by days is selling you a calculator at a markup.

Node-by-node list

Eight nodes in one straight line, no branches.

Every Monday 7am    (Schedule Trigger)
       ↓
Set planning inputs (Edit Fields, v3.4)   lookback, lead time, review, safety, email
       ↓
Get paid orders     (Shopify, order:getAll)   returnAll, financialStatus=paid
       ↓
Get products        (Shopify, product:getAll) returnAll, Execute Once = ON
       ↓
Compute reorder points (Code, run once for all items)
       ↓
Upsert forecast sheet  (Google Sheets, appendOrUpdate, match on sku)
       ↓
Build reorder summary  (Code, run once for all items)
       ↓
Email the reorder list (Gmail, send)
Node Type Job
Every Monday 7am Schedule Trigger Fires weekly. The only entry point.
Set planning inputs Edit Fields v3.4 Holds the five settings you tune, so you never edit an expression to change a policy.
Get paid orders Shopify Pulls every paid order created since the lookback date.
Get products Shopify Pulls the catalog with current stock per variant.
Compute reorder points Code Counts units per SKU, derives velocity, reorder point, days of cover and order quantity.
Upsert forecast sheet Google Sheets v4.5 Writes one row per SKU, matched on sku so reruns update in place.
Build reorder summary Code Keeps only the flagged SKUs and builds the email table.
Email the reorder list Gmail Sends you the buy list.

Step-by-step build

1 Add the schedule trigger

Create a new workflow and add a Schedule Trigger. Set the interval to Weeks, trigger day Monday, hour 7, minute 0. Weekly matches how purchasing actually works: you place orders in batches, not continuously, and the review period is part of the math in step 5.

2 Create the Google Sheet

Make a new sheet, name the tab Reorder, and put these headers in row 1, spelled exactly like this because the mapping matches on them:

sku | product | variant | stock | units_sold | velocity_per_day |
days_of_cover | reorder_point | suggested_qty | status | updated_at

Keep the sheet ID from the URL. It is the long string between /d/ and /edit.

3 Add the Set planning inputs node

Add an Edit Fields node and create five assignments: lookbackDays as a Number set to 60, leadTimeDays as a Number set to your supplier’s real turnaround, reviewPeriodDays as a Number set to 7 to match the weekly schedule, safetyDays as a Number set to 7, and reportEmail as a String set to your address.

💡

Tip: Keep the buffer in days rather than units. Seven days of safety stock means seven days for every product, whether that is 70 units of a bestseller or one unit of a slow mover. A buffer in units would have to be retuned per SKU forever.

4 Pull the orders and the catalog

Add a Shopify node, resource Order, operation Get Many, Return All on. Under Options add Status any, Financial Status paid, and Created At Min set to this expression:

{{ new Date(Date.now() - $json.lookbackDays * 86400000).toISOString() }}

Then add a second Shopify node, resource Product, operation Get Many, Return All on. Open its Settings tab and turn on Execute Once. This matters more than it looks: an n8n node runs once per incoming item, and the orders node hands it thousands. Without Execute Once you would fetch the entire catalog thousands of times and hit the rate limit in seconds.

📌

Filtering to paid orders at the source is what keeps the velocity honest. Abandoned checkouts and unpaid drafts are not sales, and letting them into the count inflates demand for products nobody bought.

5 Compute the reorder points

Add a Code node named exactly Compute reorder points, because two later nodes reference it by name. Set mode to Run Once for All Items. It counts units per SKU from the orders, then walks the catalog and joins the two.

const cfg = $('Set planning inputs').first().json;
const { lookbackDays, leadTimeDays, reviewPeriodDays, safetyDays } = cfg;

// 1. Units sold per SKU over the lookback window
const sold = {};
for (const o of $('Get paid orders').all()) {
  for (const li of (o.json.line_items || [])) {
    const sku = (li.sku || '').trim();
    if (!sku) continue;
    sold[sku] = (sold[sku] || 0) + (li.quantity || 0);
  }
}

// 2. Join to current stock, one row per variant
const rows = [];
const stamp = new Date().toISOString().slice(0, 10);
for (const p of $input.all()) {
  for (const v of (p.json.variants || [])) {
    const sku = (v.sku || '').trim();
    if (!sku) continue;
    if (v.inventory_management !== 'shopify') continue;

    const stock = v.inventory_quantity || 0;
    const units = sold[sku] || 0;
    const velocity = units / lookbackDays;

    const reorderPoint = Math.ceil(velocity * (leadTimeDays + safetyDays));
    const daysOfCover = velocity > 0 ? Math.round(stock / velocity) : null;
    const target = velocity * (leadTimeDays + reviewPeriodDays + safetyDays);
    const suggestedQty = Math.max(0, Math.ceil(target - stock));
    const needsReorder = velocity > 0 && stock <= reorderPoint;

    rows.push({ json: {
      sku,
      product: p.json.title,
      variant: v.title === 'Default Title' ? '' : v.title,
      stock,
      units_sold: units,
      velocity_per_day: Number(velocity.toFixed(3)),
      days_of_cover: daysOfCover,
      reorder_point: reorderPoint,
      suggested_qty: needsReorder ? suggestedQty : 0,
      status: velocity === 0 ? 'no sales' : (needsReorder ? 'REORDER NOW' : 'ok'),
      updated_at: stamp
    }});
  }
}

rows.sort((a, b) => (a.json.days_of_cover ?? 99999) - (b.json.days_of_cover ?? 99999));
return rows;

Three decisions are worth naming. Variants without inventory_management set to shopify are skipped, because a reorder point on an untracked quantity is fiction. Blank SKUs are skipped, since there is nothing to join them to. And the suggested quantity covers lead time plus review period plus safety, not just lead time, because the next chance to order is seven days away.

Here is what the black tee looks like coming out of this node:

{
  "sku": "TEE-BLK-M",
  "product": "Black Tee",
  "variant": "M",
  "stock": 14,
  "units_sold": 60,
  "velocity_per_day": 1,
  "days_of_cover": 14,
  "reorder_point": 21,
  "suggested_qty": 14,
  "status": "REORDER NOW",
  "updated_at": "2026-07-17"
}

6 Upsert the forecast sheet

Add a Google Sheets node, operation Append or Update Row. Pick your document by ID, sheet Reorder, mapping mode Map Automatically, and set Column to Match On to sku. Matching on SKU is what makes the workflow idempotent: every Monday the same row updates in place instead of appending a duplicate, so the sheet stays one row per SKU and sorts by days_of_cover into a catalog ranked by urgency.

7 Build the summary and email it

Add a second Code node, Run Once for All Items, that filters to the flagged rows and builds an HTML table:

const rows = $('Compute reorder points').all().map(i => i.json);
const flagged = rows.filter(r => r.status === 'REORDER NOW');

let html;
if (flagged.length === 0) {
  html = '<p>Nothing crosses its reorder point this week. ' + rows.length + ' SKUs checked.</p>';
} else {
  const cells = flagged.map(r =>
    '<tr><td>' + r.sku + '</td><td>' + r.product + ' ' + r.variant + '</td><td>' + r.stock +
    '</td><td>' + r.days_of_cover + '</td><td>' + r.reorder_point + '</td><td><b>' + r.suggested_qty + '</b></td></tr>'
  ).join('');
  html = '<p>' + flagged.length + ' of ' + rows.length + ' SKUs need a purchase order.</p>' +
    '<table border="1" cellpadding="6" cellspacing="0">' +
    '<tr><th>SKU</th><th>Product</th><th>Stock</th><th>Days left</th><th>Reorder at</th><th>Order</th></tr>' +
    cells + '</table>';
}

const label = flagged.length === 1 ? ' SKU' : ' SKUs';
return [{ json: {
  subject: flagged.length ? 'Reorder ' + flagged.length + label : 'Reorder check: all clear',
  html,
  count: flagged.length
}}];

Then add a Gmail node, operation Send. Set To to {{ $('Set planning inputs').first().json.reportEmail }}, Subject to {{ $json.subject }}, and Message to {{ $json.html }} with Email Type set to HTML. The all clear email is deliberate. A workflow that only writes when something is wrong is indistinguishable from one that has quietly stopped running.

Save, run it once by hand, and check the sheet fills before you switch Active on. To have the list land in a channel instead of an inbox, swap the Gmail node for Telegram or Slack; the summary node already hands over a finished string, so nothing upstream changes. See Shopify alerts on Telegram.

Common mistakes

Mistake What happens Fix
Leaving Execute Once off on the products node The catalog is fetched once per order, thousands of times, and Shopify rate limits you Turn on Execute Once in the node’s Settings tab
A wholesale order inside the window One 500 unit order makes a trailing average think you sell 8 a day forever Sanity check the velocity column; exclude the order or shorten the lookback
Guessing the lead time Every reorder point is wrong by the size of the guess, in the same direction, for every SKU Use the real number from your last three purchase orders, including customs
Products launched mid-window Units get divided by 60 days even though the product existed for 10, so velocity reads low Check anything with a recent first sale by eye until it has a full window
Renaming the Compute reorder points node The summary node and the Gmail node break, since both reference it by name Keep the name, or update every reference
Duplicate SKUs across variants Two variants sharing a SKU collide on the sheet and overwrite each other Keep SKUs unique per variant, which Shopify does not enforce for you

The wholesale spike is the one that catches people. A mean has no idea that one of its inputs was unusual, and it will happily tell you to buy 500 more of something you sold once. If bulk orders are normal for you, the honest fix is a median instead of a mean, which is a two line change in the Code node.

Cost at realistic volume

Nothing here bills per SKU, which is the whole argument against the app version.

Piece Cost Notes
n8n (self-hosted) $0 Four executions a month
Shopify Admin API $0 Order and product reads are included in your plan
Google Sheets $0 Free, well inside the write limits
Gmail $0 Four emails a month to yourself

The only real resource is the two API pulls. A store doing 500 orders a month has about 1,000 orders in a 60 day window and maybe 300 variants, which the Shopify nodes page through in well under a minute. At 5,000 orders a month and 2,000 variants it takes a few minutes, which is unremarkable for something that runs while you sleep.

On n8n Cloud this is four executions a month against your plan allowance. Against 30 to 200 dollars a month for a forecasting app, the sheet wins on cost before you count the part that matters more: your lead time and your buffer are yours to change, and the numbers recompute on the next run.

🚀 Get the Shopify reorder point forecast template

The guide above is free to follow. If you would rather skip the build, download the ready-to-import workflow, then just add your credentials. Prefer it installed and tuned for you? See our done-for-you services.

Download the template ($19) →

Instant download · Works on n8n Cloud and self-hosted

FAQ

What is a reorder point in ecommerce?

A reorder point is the stock level at which you place a purchase order. It equals your daily sales velocity multiplied by your supplier lead time, plus a safety buffer. Hit that level and you order, because waiting any longer means the shelf empties before the delivery lands.

How is this different from a low stock alert?

A low stock alert fires at a fixed number you picked by hand, so 10 units means the same thing for a product selling 5 a day and one selling 5 a year. This workflow derives the threshold from actual sales velocity and your lead time, so it answers when to reorder rather than announcing that stock is low.

How much order history does the forecast need?

Sixty days is the default and works for steady sellers. Products launched inside the window show an inflated velocity because the units sold get divided by the full 60 days. Treat anything with less than a few weeks of history as directional and check it by eye before ordering.

Does this workflow change my Shopify inventory?

No. It only reads orders and products. Every number lands in your Google Sheet and in one email, so a wrong lead time cannot corrupt stock records or push a purchase order to a supplier. You stay the approval step between the forecast and the money.

Can it handle seasonal products?

Not on its own. A trailing average assumes next month looks like the last two, which is wrong going into a peak. Shorten the lookback to 30 days when demand climbs so the average reacts faster, and override the suggested quantity by hand for known seasonal spikes.

Related guides

The sheet this workflow produces is a planning input. These are the automations around it.

n8n
Shopify
Google Sheets
Gmail
inventory
automation

Shopify RFM Customer Segmentation With n8n and Google Sheets

Shopify RFM customer segmentation workflow in n8n











A shopify rfm customer segmentation google sheets n8n workflow pulls a year of paid orders every Monday, scores each customer from 1 to 5 on recency, frequency and monetary value, sorts them into named segments, and upserts the table into a Google Sheet. No analytics app, no monthly fee, no CSV exports. Seven nodes and a sheet that tells you who to email this week. Below: the node map, the build, the mistakes, and the real cost.

Prefer to skip the setup? Grab the ready-made template and be running in under 10 minutes.

What it does

Shopify Analytics shows you total sales and a list of top spenders. It will not tell you that the customer who spent 1,400 dollars has not been back in ten months, or that the person who ordered four times in six weeks is about to become your best account. Those are different people who need different emails, and a revenue leaderboard hides both.

RFM is the oldest reliable answer to that. You rank every customer on three things: how recently they bought, how often they buy, and how much they have spent. Each becomes a score from 1 to 5, relative to everyone else on your list. A 555 is a Champion. A 255 is someone who used to be a Champion and is drifting away, which is the most valuable email you will send all month. A 111 is gone.

This workflow builds that table for you on a schedule and keeps it current.

  • Reads paid orders from a rolling window you set, 365 days by default.
  • Groups them by customer email, merging case variants and skipping guest checkouts with no email.
  • Scores recency, frequency and monetary value into quintiles, then labels each customer.
  • Upserts every row into a Google Sheet keyed on email, so the sheet updates rather than duplicating.
  • Emails you a segment breakdown when it finishes.

Why it beats the default

The default is a customer analytics app from the Shopify App Store, at anywhere from 30 to 200 dollars a month depending on list size. Those apps compute RFM competently. The trade is that your segmentation logic lives inside someone else’s product, priced per contact, and you cannot see or change the thresholds. The other default is worse: exporting orders to a spreadsheet by hand every quarter, which works exactly once because nobody keeps it up.

Building it in n8n gives you the part the apps hide. The scoring lives in one Code node you can read in a minute, so a threshold that does not match your store is a one line change rather than a support ticket. The output is a plain Google Sheet, which means anyone on your team can filter it and any other workflow can read it. And because it is your own instance, the price does not move when your customer list doubles.

The honest caveat: an app gives you a dashboard, and this gives you a sheet. If a chart is what you want, this is not that. If acting on the segments is what you want, a sheet is the better input anyway.

💡

Tip: Before you connect n8n to Shopify, set up access the modern way. Our guide on how to connect Shopify to n8n in 2026 walks through the Dev Dashboard method that this workflow relies on. The old admin custom-app flow no longer exists.

What you need

  • An n8n instance, cloud or self-hosted. A free self-hosted install handles this comfortably.
  • A Shopify store connected to n8n the 2026 way, with read access to orders and customers. See the 2026 connection guide.
  • A Google account for Sheets and Gmail, connected to n8n by OAuth.
  • One empty Google Sheet with a header row, described in step 2 below.
  • Roughly 20 minutes, and enough order history that the scoring has something to divide. See the FAQ on list size.

No AI model is needed here. RFM is arithmetic, and arithmetic does not need a language model. Anything that sells you an LLM for this is selling you a sorting function at a markup.

Node-by-node list

Seven nodes in one straight line, no branches.

Every Monday 6am  (Schedule Trigger)
       ↓
Set window        (Edit Fields, v3.4)      lookbackDays = 365, reportEmail
       ↓
Get paid orders   (Shopify, order:getAll)  returnAll, financialStatus=paid
       ↓
Compute RFM scores (Code, run once for all items)
       ↓
Upsert RFM sheet  (Google Sheets, appendOrUpdate, match on email)
       ↓
Build summary     (Code, run once for all items)
       ↓
Email the summary (Gmail, send)
Node Type Job
Every Monday 6am Schedule Trigger Fires weekly. The only entry point.
Set window Edit Fields v3.4 Holds the two settings you tune: lookback days and the report address.
Get paid orders Shopify Pulls every paid order created since the lookback date.
Compute RFM scores Code Groups by email, computes R, F and M, assigns quintiles and a segment label.
Upsert RFM sheet Google Sheets v4.5 Writes one row per customer, matched on email so reruns update in place.
Build summary Code Counts customers and revenue per segment for the email.
Email the summary Gmail Sends you the breakdown.

Step-by-step build

1 Add the schedule trigger

Create a new workflow and add a Schedule Trigger. Set the interval to Weeks, trigger day Monday, hour 6, minute 0. Weekly is the right cadence: RFM bands move slowly, and a daily run mostly rewrites identical rows.

2 Create the Google Sheet

Make a new sheet, name the tab RFM, and put these headers in row 1, spelled exactly like this because the mapping matches on them:

email | name | recency_days | frequency | monetary | last_order_date |
r_score | f_score | m_score | rfm_score | segment | updated_at

Keep the sheet ID from the URL. It is the long string between /d/ and /edit.

3 Add the Set window node

Add an Edit Fields node and create two assignments: lookbackDays as a Number set to 365, and reportEmail as a String set to your own address. Putting these in their own node means you tune the workflow in one place instead of hunting through expressions later.

4 Pull the orders

Add a Shopify node, resource Order, operation Get Many, and turn on Return All. Under Options add Status any, Financial Status paid, and Created At Min set to this expression:

{{ new Date(Date.now() - $json.lookbackDays * 86400000).toISOString() }}

Filtering to paid orders at the source matters. Unpaid and abandoned checkouts are not purchases, and letting them into the scoring inflates frequency for people who never actually bought.

5 Score the customers

Add a Code node named exactly Compute RFM scores, since a later node references it by name. Set mode to Run Once for All Items. The logic does four things in order: group orders by lowercased email, derive recency, frequency and monetary value per customer, rank each of the three into quintiles, then map the scores to a label.

The quintile ranking is the part worth understanding. It sorts the whole customer list on one measure and cuts it into five equal bands, so scores are always relative to your store rather than to a number someone guessed. Recency is inverted, because a low recency in days is good:

function scoreByQuintile(list, key, target, higherIsBetter) {
  const sorted = [...list].sort((a, b) => a[key] - b[key]);
  const n = sorted.length;
  sorted.forEach((c, i) => {
    let q = Math.floor((i / n) * 5) + 1;
    if (q > 5) q = 5;
    c[target] = higherIsBetter ? q : 6 - q;
  });
}

scoreByQuintile(customers, 'recencyDays', 'r', false);
scoreByQuintile(customers, 'frequency',   'f', true);
scoreByQuintile(customers, 'monetary',    'm', true);

Then the labels. These are the rules to argue with once you know your own store, and they are deliberately readable so you can:

function labelFor(r, f, m) {
  if (r >= 4 && f >= 4 && m >= 4) return 'Champions';
  if (r >= 3 && f >= 3)           return 'Loyal';
  if (r >= 4 && f <= 2)           return 'New and promising';
  if (r <= 2 && f >= 4 && m >= 4) return 'At risk, was valuable';
  if (r <= 2 && f >= 3)           return 'At risk';
  if (r <= 2 && f <= 2 && m >= 4) return 'Big spender, lapsed';
  if (r === 1 && f === 1)         return 'Lost';
  if (r === 3)                    return 'Needs attention';
  return 'Hibernating';
}

Order matters in that chain, because the first match wins. The node returns one item per customer, sorted by revenue, each carrying the twelve fields your sheet expects.

6 Upsert into the sheet

Add a Google Sheets node, operation Append or Update Row. Pick your document and the RFM tab, set mapping to Map Each Column Manually, and choose email as the column to match on. Map each remaining field to its matching value from the Code node.

Append or Update is what makes this rerunnable. Plain Append would give you a fresh set of duplicate rows every Monday until the sheet is unusable. Matching on email means week two updates week one’s rows in place.

7 Summarise and email

Add a second Code node named Build summary, also Run Once for All Items. It reads back from the scoring node with $('Compute RFM scores').all(), tallies customers and revenue per segment, and builds a small HTML list. Then add a Gmail node, Send operation, with the address pulled from your config node:

To:      {{ $('Set window').first().json.reportEmail }}
Subject: RFM refresh: {{ $json.total_customers }} customers scored ({{ $json.generated_on }})
Body:    {{ $json.summary_html }}

Run the workflow manually once. You should get an email like this, and a populated sheet behind it:

Champions: 2 customers, 2310 in revenue
At risk, was valuable: 1 customer, 1860 in revenue
Big spender, lapsed: 1 customer, 1400 in revenue
Loyal: 2 customers, 260 in revenue
New and promising: 3 customers, 160 in revenue
Lost: 1 customer, 15 in revenue

That second line is the whole point of the exercise. Activate the workflow and it runs every Monday.

Common mistakes

  • Using Append instead of Append or Update. Your sheet grows by a full copy of your customer list every week and the segments become impossible to read. Match on email.
  • Leaving unpaid orders in the pull. Abandoned checkouts are not purchases. Without the paid filter, frequency scores drift upward for people who never gave you money.
  • Reading recency backwards. A recency of 4 days is excellent and should score 5, not 1. This is the single most common RFM bug, which is why the scoring function takes an explicit flag rather than assuming higher is better.
  • Running it on a tiny list. Quintiles need a spread to divide. With fifteen customers the bands are three people wide and one order swings someone from Loyal to Champion. The workflow runs fine, the labels just mean less.
  • Renaming the scoring node. The summary node calls it by name, so a rename in the editor breaks the reference. Rename in both places or leave it alone.
  • Expecting guest checkouts to appear. Orders with no email cannot be grouped to a person, so they are skipped by design. If a large share of your orders are guest orders, your RFM covers less of your revenue than you think.
  • Treating the labels as gospel on day one. Look at fifteen customers in the sheet and check the labels match your instinct. The thresholds are a starting point, not a verdict.

Cost at realistic volume

Nothing here bills per customer, which is the entire argument against the app version.

Piece Cost Notes
n8n (self-hosted) $0 Four executions a month
Shopify Admin API $0 Order reads are included in your plan
Google Sheets $0 Free, well inside the write limits
Gmail $0 Four emails a month to yourself

The only real resource is the order pull. A store doing 500 orders a month has about 6,000 orders in a 365 day window, which the Shopify node pages through in a couple of minutes on a weekly schedule. A store doing 5,000 orders a month will want to trim the lookback to 180 days or run it overnight, but the arithmetic itself stays trivial: it is a sort and a division, on data that fits in memory.

On n8n Cloud this is four executions a month against your plan allowance, which is a rounding error. Compare that to 30 to 200 dollars a month for a segmentation app, and the sheet wins on cost before you count the fact that you can change the rules.

🚀 Get the Shopify RFM segmentation template

The guide above is free to follow. If you would rather skip the build, download the ready-to-import workflow, then just add your credentials. Prefer it installed and tuned for you? See our done-for-you services.

Download the template ($19) →

Instant download · Works on n8n Cloud and self-hosted

FAQ

What is RFM segmentation in ecommerce?

RFM stands for recency, frequency and monetary value. You score every customer from 1 to 5 on how recently they bought, how often they buy, and how much they have spent. The three digits together sort your list into groups such as Champions, At risk, and Lost, so each group can get different messaging.

How many customers do I need for RFM scoring to be useful?

The scoring is relative, so it needs a real spread to divide. Below roughly 50 buying customers the quintiles get thin and a single order can move someone two bands. The workflow still runs and still returns labels, but treat the output as directional until your list grows.

Does this workflow change anything in my Shopify store?

No. It only reads orders. Every score and label is written to your Google Sheet and nothing is pushed back to Shopify, so a wrong threshold cannot damage customer records. Once you trust the output you can add a tagging step as a separate workflow.

Why score against orders instead of the Shopify customer record?

The customer object gives you totals but not the order dates you need for recency, and it counts orders you may want to exclude. Rebuilding from paid orders lets you control the window, drop unpaid checkouts, and recompute the whole picture from one clean source each week.

How often should the segmentation run?

Weekly suits most stores. RFM bands move slowly, a daily run mostly rewrites identical rows, and a monthly run lets a Champion drift into At risk before you notice. The schedule lives in one trigger node, so changing the cadence is a single edit.

Related guides

The sheet this workflow produces is an input. These are the automations that consume it.

n8n
Shopify
Google Sheets
Gmail
RFM
segmentation

Shopify birthday discount code email with n8n

Shopify birthday discount code email workflow in n8n











A shopify birthday discount code email n8n workflow reads customer birthdays from a Google Sheet every morning, finds whoever is celebrating today, generates a unique single-use discount code inside Shopify, and emails it through Gmail. No paid loyalty app, no manual list checking, no shared coupon that ends up on a deal site. You build it once with five nodes and it runs on autopilot. Below you get the node-by-node map, the exact build steps, the common mistakes, and the real cost at volume.

Prefer to skip the setup? Grab the ready-made template and be running in under 10 minutes.

What it does

Birthday emails are one of the highest-converting messages an ecommerce store ever sends, because they land when the customer already feels like treating themselves. The problem is that Shopify has no birthday field and no built-in way to trigger something on a date each year. So most owners either pay for a loyalty app or forget the whole idea.

This automation closes that gap with parts you already have. You keep a simple sheet of names, emails, and birthdays. Every morning n8n wakes up, checks the sheet against today’s date, and for each match it asks Shopify to mint a fresh discount code and then sends that code to the customer by email. The customer opens their inbox on their birthday and finds a personal, one-time offer waiting.

  • Runs on a daily schedule, fully unattended.
  • Creates a unique code per customer, capped at one use.
  • Sends a warm, personal Gmail message with the code inside.
  • Costs nothing beyond tools you likely already run.

Why it beats the default

The default option is a birthday app from the Shopify App Store. Those work, but they add a recurring bill, they park your customer data inside another vendor, and they usually hand out one static coupon code that anyone can reuse once it leaks. A single shared code on a birthday campaign is a margin risk, because a deal-hunting forum can drain it in hours.

Building it in n8n flips all three problems. There is no monthly fee. The customer list stays in your own Google Sheet and your own Shopify store. And because every code is generated on the fly with a usage limit of one, a leaked code is worthless to anyone but the person it was meant for. You also get to shape the email exactly how you want, in your own brand voice, instead of squeezing into an app’s template editor.

💡

Tip: Before you connect n8n to Shopify, set up access the modern way. Our guide on how to connect Shopify to n8n in 2026 walks through the Dev Dashboard custom-app method that this workflow relies on.

What you need

  • An n8n instance, cloud or self-hosted (a free self-hosted install is fine).
  • A Shopify store connected to n8n the 2026 way (Developer Dashboard). See the 2026 connection guide.
  • A Google account for Google Sheets and Gmail.
  • A birthday sheet with three columns: Name, Email, Birthday (stored as MM-DD).

Build time: about 30 minutes from scratch, or under 10 minutes if you import the template and drop in your credentials.

Node-by-node list

The workflow is a clean straight line of five nodes. Nothing branches, which keeps it easy to reason about and easy to fix if a birthday email ever fails to arrive.

┌─────────────────────────────────────────────────────────────┐
│  SHOPIFY BIRTHDAY DISCOUNT CODE EMAIL                        │
│                                                             │
│  [Schedule Trigger]  daily at 08:00                         │
│         ↓                                                   │
│  [Google Sheets]  read all birthday rows                    │
│         ↓                                                   │
│  [Code]  keep today's birthdays, build unique code + dates  │
│         ↓                                                   │
│  [HTTP Request]  Shopify GraphQL: create discount code      │
│         ↓                                                   │
│  [Gmail]  send the birthday email with the code             │
└─────────────────────────────────────────────────────────────┘
  
# Node Type Job
1 Schedule Trigger scheduleTrigger Fire once a day at 8am
2 Google Sheets googleSheets Read the birthday list
3 Code code Filter to today, build code and dates
4 Create Discount httpRequest Call Shopify GraphQL to mint the code
5 Gmail gmail Email the customer their code

Step-by-step build

1 Schedule Trigger

Add a Schedule Trigger node. Set it to a fixed interval of once per day and choose an hour that suits your audience, for example 8:00 in your store’s timezone. This node is the only entry point, so nothing else runs until the schedule fires.

💡

Tip: Set the workflow timezone under Settings so 8am means 8am for your customers, not for the server. A morning email lands better than one that arrives at 2am.

2 Google Sheets — read the list

Add a Google Sheets node, operation Get Row(s) in Sheet. Point it at your birthday spreadsheet and the tab that holds the list. Leave the filters empty so it returns every row; the next node does the date matching. A returned row looks like this:

{
  "Name": "Emily Rodriguez",
  "Email": "emily.rodriguez@gmail.com",
  "Birthday": "07-15"
}

3 Code — find today’s birthdays

Add a Code node (run once for all items). This is the brain of the workflow. It compares each row’s birthday to today, and for every match it builds a unique code plus the start and end dates for the offer.

const today = new Date();
const mmdd = `${String(today.getMonth() + 1).padStart(2, '0')}-${String(today.getDate()).padStart(2, '0')}`;
const out = [];

for (const item of $input.all()) {
  const row = item.json;
  const bday = String(row.Birthday || '').replace(/\//g, '-').slice(-5);
  if (bday !== mmdd) continue;

  const rand = Math.random().toString(36).slice(2, 7).toUpperCase();
  const first = String(row.Name || 'FRIEND').split(' ')[0].toUpperCase().replace(/[^A-Z0-9]/g, '');

  const starts = new Date(today); starts.setHours(0, 0, 0, 0);
  const ends = new Date(starts); ends.setDate(ends.getDate() + 7);

  out.push({ json: {
    name: row.Name,
    email: row.Email,
    code: `BDAY-${first}-${rand}`,
    title: `Birthday reward - ${row.Name}`,
    startsAt: starts.toISOString(),
    endsAt: ends.toISOString()
  } });
}

return out;

If nobody has a birthday today, the node returns zero items and the rest of the workflow simply does not run, which is exactly what you want. On a match, one item leaves this node per customer:

{
  "name": "Emily Rodriguez",
  "email": "emily.rodriguez@gmail.com",
  "code": "BDAY-EMILY-7K2QP",
  "title": "Birthday reward - Emily Rodriguez",
  "startsAt": "2026-07-15T00:00:00.000Z",
  "endsAt": "2026-07-22T00:00:00.000Z"
}
📌

Note: Store birthdays as MM-DD so the year never matters. The slice(-5) also accepts a full YYYY-MM-DD value, so either format in the sheet works.

4 HTTP Request — create the Shopify code

Add an HTTP Request node. This calls the Shopify GraphQL Admin API to create a real, working discount code with the values the Code node produced. Configure it like this:

  • Method: POST
  • URL: https://YOUR_STORE.myshopify.com/admin/api/2026-04/graphql.json
  • Authentication: Generic credential, Header Auth, header name X-Shopify-Access-Token with your Admin API access token as the value
  • Send Body: on, Body Content Type: JSON

Paste this into the JSON body field. It sends the GraphQL mutation with variables pulled from the current item:

={{ JSON.stringify({
  query: "mutation ($basic: DiscountCodeBasicInput!) { discountCodeBasicCreate(basicCodeDiscount: $basic) { codeDiscountNode { id } userErrors { field message } } }",
  variables: { basic: {
    title: $json.title,
    code: $json.code,
    startsAt: $json.startsAt,
    endsAt: $json.endsAt,
    customerSelection: { all: true },
    customerGets: { value: { percentage: 0.15 }, items: { all: true } },
    appliesOncePerCustomer: true,
    usageLimit: 1
  } }
}) }}

That creates a 15 percent code, valid for seven days, that works exactly once. Change 0.15 to set a different discount, or swap percentage for a fixed discountAmount if you prefer a flat sum off.

💡

Tip: GraphQL returns a 200 status even when the discount is rejected. Watch the userErrors array in the response during testing; an empty array means the code was created cleanly.

5 Gmail — send the birthday email

Add a Gmail node, operation Send. Map the fields from the current item so each customer gets their own code:

  • To: ={{ $json.email }}
  • Subject: =Happy birthday, {{ $json.name }}! A gift from us 🎉
  • Message: your birthday copy, with ={{ $json.code }} dropped in where the code should appear

A short, warm message converts best. Something like: “Happy birthday from all of us. Here is 15 percent off anything in the store as our gift. Use code BDAY-EMILY-7K2QP at checkout within the next seven days.” Keep it personal and get the code above the fold.

Common mistakes

  • Storing the birth year in the match. If you compare full dates including the year, nobody ever matches. Match on MM-DD only, which the Code node already does.
  • Leaving the timezone unset. Without a workflow timezone, the 8am trigger and the “today” check both run in server time, and codes can be built for the wrong calendar day. Set the timezone once in Settings.
  • Using a stale API version. Point the URL at a current version such as 2026-04. An old version can silently drop fields from the mutation.
  • Trusting the HTTP status alone. Shopify GraphQL answers 200 even on a rejected discount. Always check userErrors before you assume the code exists.
  • One shared code for everyone. That defeats the whole point. Let the Code node mint a fresh code per customer with a usage limit of one.

Cost at realistic volume

This is one of the cheapest automations you can run, because none of the moving parts bill per birthday.

Piece Cost Notes
n8n (self-hosted) $0 One execution per day, tiny footprint
Shopify Admin API $0 Discount creation is included in your plan
Google Sheets $0 Free, well within read limits
Gmail $0 Free tier sends up to 500 emails a day

Even a store with 20,000 customers rarely sees more than a few dozen birthdays on a single day, so you stay far below every free limit. If you run n8n Cloud instead of self-hosting, this is a single scheduled run per day, which barely dents the starter plan’s execution allowance. For most stores the true cost is zero.

🚀 Get the Shopify birthday email template

The guide above is free to follow. If you would rather skip the build, download the ready-to-import workflow, then just add your credentials. Prefer it installed and tuned for you? See our done-for-you services.

Download the template ($12) →

Instant download · Works on n8n Cloud and self-hosted

FAQ

Does Shopify store customer birthdays by default?

No. Shopify has no native birthday field on the customer record. This workflow keeps birthdays in a Google Sheet that you fill from a signup form or a customer metafield, so the automation always has a reliable date to check each morning.

Is each birthday discount code unique?

Yes. The Code node builds a distinct code per customer, such as BDAY-EMILY-7K2QP, and the Shopify mutation sets a usage limit of one. A code cannot be shared, reused, or leaked to a coupon site because it only works once for one order.

Do I need a paid loyalty app for this?

No. The whole flow runs on Shopify, a free Google Sheet, n8n, and Gmail. There is no monthly app fee. Most birthday apps in the Shopify App Store charge fifteen to forty dollars a month for the same outcome you build here once.

What if two customers share the same birthday?

The workflow handles as many birthdays as land on a given day. The Code node returns one item per matching customer, and each item flows through the discount and email nodes separately, so everyone gets their own code and their own message.

Can I change the discount amount or expiry?

Yes. The percentage lives in the HTTP Request node as 0.15 for fifteen percent, and the expiry is set in the Code node as seven days from today. Edit either value once and every future birthday code follows the new rule automatically.

Related guides

n8n
Shopify
Google Sheets
Gmail
automation

Shopify AI Product Recommendation Email With n8n and Gemini

Shopify AI product recommendation email workflow in n8n









You already know that repeat and add-on sales are where the margin lives, yet most stores stop talking to a customer the moment the order confirmation goes out. A Shopify AI product recommendation email built in n8n fixes that gap without a paid upsell app. Every new order triggers a workflow that reads what the shopper bought, gathers other products from the same category, and lets Google Gemini choose the three that pair best, then emails them a clean “you might also like” pick.

Prefer to skip the setup? Grab the ready-made template below and be running in under ten minutes.

A Shopify AI product recommendation email built in n8n turns every new order into a personalized cross-sell. When an order arrives, the workflow reads what the customer bought, pulls other products from the same category, and lets Google Gemini pick three complementary items to feature in a follow-up email. It runs on free tiers, sends through Gmail, and never invents products, because it can only choose from your real catalog.

What it does

The workflow watches your store for new orders. The instant one is placed, it looks up the product that was purchased, finds sibling products in the same product_type, and hands that shortlist to Gemini with one job: pick the three items that complement the purchase and write a one-line reason for each. Those three products get dropped into an email template and sent to the customer through Gmail.

The result is a recommendation email that feels hand-picked rather than random. Because the candidate list is scoped to a single category and comes straight from your Shopify catalog, every suggestion is a product you actually sell and can actually ship.

┌──────────────────────────────────────────────────────────────┐
│  SHOPIFY AI PRODUCT RECOMMENDATION EMAIL                      │
│                                                              │
│  [Order placed]                                              │
│       ↓                                                      │
│  [Shopify Trigger] → [Get purchased product] →               │
│       [Get same-category products] → [Build candidates] →    │
│            [Gemini picks 3] → [Build email] → [Gmail send]   │
└──────────────────────────────────────────────────────────────┘
  

Why it beats the default

Shopify’s built-in “related products” block on a product page is passive. It only helps a shopper who is already browsing, and it disappears the moment they check out. Paid recommendation apps close that gap but add a monthly fee and often a percentage of revenue on top.

This n8n approach reaches the customer in their inbox, right after a purchase, when intent is highest and the brand is fresh in mind. You control the category logic, the email copy, and the send timing. There is no per-order fee, no shared revenue, and no third-party script slowing your storefront. For the broader picture of what else you can wire up this way, see our guide to Shopify automation with n8n.

What you need

  • A running n8n instance, either n8n Cloud or self-hosted.
  • A Shopify store with Admin API access. If you have not connected n8n to Shopify yet, follow our 2026 Shopify to n8n connection guide, which covers the current Dev Dashboard method.
  • A Google Gemini API key on the free tier, using the gemini-2.5-flash model.
  • A Gmail account connected to n8n for sending the emails.

Build time is around 30 to 45 minutes from scratch, or under ten minutes if you import the template.

Node-by-node list

Seven nodes carry an order from placed to recommendation email.

  1. Shopify Trigger fires on the orders/create topic the moment a new order is placed.
  2. An HTTP Request node reads the purchased product so the workflow knows its product_type.
  3. A second HTTP Request node pulls other active products in that same product_type.
  4. A Code node trims the list into clean candidates, removes the item that was just bought, and attaches the customer name and email.
  5. An HTTP Request node calls Gemini and asks it to pick the three best matches as JSON.
  6. A Code node parses Gemini’s answer and builds the email HTML.
  7. A Gmail node sends the recommendation email to the customer.

Step-by-step build

1 Shopify Trigger (orders/create)

Add a Shopify Trigger node and set the topic to Order Created. Attach your Shopify Access Token credential. This node hands the full order object to the rest of the workflow. The fields that matter here are the first line item’s product_id and the customer block.

{
  "id": 5123400012345,
  "customer": { "first_name": "Emily", "email": "emily.rodriguez@gmail.com" },
  "line_items": [
    { "product_id": 7781234500001, "title": "Cedar & Sage Candle", "price": "28.00" }
  ]
}
📌

The old Shopify admin custom-app screen is gone. Create your app in the Shopify Dev Dashboard, grant the read_products and read_orders scopes, and paste the access token into n8n. The connection guide has the exact clicks.

2 Get purchased product (HTTP Request)

Order line items do not include the product category, so fetch the product directly. Use an HTTP Request node with the GET method and your Shopify credential as a predefined credential type.

GET https://YOUR_STORE.myshopify.com/admin/api/2026-04/products/{{ $json.line_items[0].product_id }}.json

The response gives you product.product_type, for example Candles, which becomes the filter for the next step.

3 Get same-category products (HTTP Request)

Now pull a shortlist of sibling products. Another GET request filters the catalog by the category you just found and limits the pool so Gemini has a focused set to rank.

GET https://YOUR_STORE.myshopify.com/admin/api/2026-04/products.json?product_type={{ encodeURIComponent($json.product.product_type) }}&status=active&limit=15
💡

Scoping candidates to one product_type is what keeps recommendations sensible. A candle buyer sees wick trimmers and matches, not phone cases. If your catalog uses collections instead of product types, swap this call for a collection products endpoint.

4 Build candidates (Code)

Add a Code node to turn the raw product payload into a tidy list. It strips each product down to the fields Gemini needs, drops the item that was just purchased, and carries the customer details forward.

const purchasedId = $('Shopify Trigger').item.json.line_items[0].product_id;
const products = $json.products || [];

const candidates = products
  .filter(p => p.id !== purchasedId)
  .slice(0, 12)
  .map(p => ({
    title: p.title,
    handle: p.handle,
    price: p.variants?.[0]?.price || "",
    type: p.product_type
  }));

const c = $('Shopify Trigger').item.json.customer;

return [{ json: {
  firstName: c.first_name,
  email: c.email,
  purchased: $('Shopify Trigger').item.json.line_items[0].title,
  candidates
} }];

5 Gemini picks three (HTTP Request)

This is where the AI earns its keep. Add an HTTP Request node with the POST method pointing at the Gemini endpoint. Set the body type to JSON and paste a JSON body that includes the candidate list and a strict instruction. Attach your Gemini key as an HTTP Header Auth credential using the header x-goog-api-key.

POST https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-flash:generateContent
{
  "contents": [{
    "parts": [{
      "text": "The customer bought: {{ $json.purchased }}. From ONLY this JSON list of products, choose the 3 that best complement the purchase. Never invent products. Return JSON array of {title, handle, reason}. Products: {{ JSON.stringify($json.candidates) }}"
    }]
  }],
  "generationConfig": { "responseMimeType": "application/json", "temperature": 0.4 }
}
📌

In the HTTP Request node, set Specify Body to Using JSON and place the object above in the JSON field. Setting responseMimeType to application/json forces Gemini to answer with parseable JSON instead of prose.

6 Build the email (Code)

Gemini returns its answer nested inside the API response. This Code node digs it out, parses it, and assembles simple product cards into an HTML email body.

const raw = $json.candidates[0].content.parts[0].text;
const picks = JSON.parse(raw);
const store = "https://your-store.com/products/";

const cards = picks.map(p =>
  `<p><a href="${store}${p.handle}">${p.title}</a> - ${p.reason}</p>`
).join("");

const first = $('Build candidates').item.json.firstName;

return [{ json: {
  to: $('Build candidates').item.json.email,
  subject: `${first}, three things that pair with your order`,
  html: `<p>Hi ${first},</p><p>Thanks for your order. You might also like:</p>${cards}`
} }];

7 Send with Gmail

Finish with a Gmail node set to Send. Map To to {{ $json.to }}, Subject to {{ $json.subject }}, and the message to {{ $json.html }} with the email type set to HTML. Save the workflow and switch it on.

Common mistakes

Symptom Likely cause Fix
Email lists a product you do not sell Gemini was allowed to free-write Keep responseMimeType as JSON and parse the array; never send raw model text into the email
No candidates found The purchased product has no product_type set Add a fallback that queries your best-sellers collection when the type is empty
401 from the Shopify calls Missing read_products scope on the app Add the scope in the Dev Dashboard and reinstall the token
Gemini returns text, not JSON responseMimeType not set on the request Add it under generationConfig in the JSON body
Email never arrives Gmail node using the wrong content type Set the email type to HTML so the product links render

Cost at realistic volume

Take a store doing 600 orders a month. That is 600 recommendation emails, each making one Gemini call on gemini-2.5-flash. The Gemini free tier comfortably covers that volume, so the AI cost is zero. Self-hosted n8n is free and one order triggers a handful of lightweight executions. Gmail sends within its normal daily limits at this scale.

Component Cost at 600 orders/month
n8n (self-hosted) Server time only, effectively free
Google Gemini (gemini-2.5-flash) Free tier
Gmail sending Included
Shopify Admin API calls Free, well within rate limits

Compare that to a paid recommendation app charging a monthly fee plus a slice of attributed revenue, and the workflow pays for itself on the first recovered add-on sale.

🚀 Get the ready-to-import template

The guide above is free to follow. If you would rather skip the build, download the ready-to-import workflow, then just add your credentials. Prefer it installed and tuned for you? See our done-for-you services.

Download the template ($14) →

Instant download · Works on n8n Cloud and self-hosted

Frequently asked questions

Will the AI ever recommend products I do not sell?

No. The workflow only sends Gemini a list of real products pulled from your own Shopify catalog, and the prompt tells it to choose only from that list. A parsing step also drops any item outside the candidate set, so the email can never feature a product that does not exist in your store.

Does this cost anything to run?

For most small and mid-size stores it runs on free tiers. Self-hosted n8n is free, Google Gemini has a free tier that covers thousands of recommendation emails a month, and Gmail sends the messages. Your only real cost is the small amount of server time to keep n8n running.

How does it pick complementary items?

It fetches other products from the same product_type as the item that was purchased, then asks Gemini to rank the three that pair best with the order. Because the candidate pool is scoped to one category, the suggestions stay relevant instead of drifting into unrelated parts of your catalog.

Can I send it a few days after purchase instead of instantly?

Yes. Add a Wait node after the Gmail step, or switch the trigger to a daily Schedule that queries orders from three days ago. A delayed send often performs better because the customer has received the first product and is ready to consider a companion item.

Do I need the Shopify custom app flow to connect n8n?

No. Shopify retired the old admin custom-app screen. In 2026 you create an app in the Shopify Dev Dashboard, assign Admin API scopes, and paste the access token into n8n. Our connection guide walks through the exact steps and required scopes.

Related guides

n8n
Shopify
Gemini
Gmail
cross-sell
automation

How to Automate Shopify Order Status Lookups With n8n

Shopify order status lookup workflow in n8n









A Shopify order status lookup automation in n8n turns the endless stream of where-is-my-order emails into instant, hands-off replies. A customer submits their order number and email on a simple form, n8n queries the Shopify Admin API, and within seconds they receive a message with the exact fulfillment status and live tracking link. No support agent, no copy-pasting tracking numbers, and your inbox is freed for real problems.

Prefer to skip the setup? Grab the ready-made template → and be answering order status requests automatically in under 10 minutes.

What it does

Where-is-my-order requests, often shortened to WISMO, are the single most common message a Shopify store receives. Industry surveys put them at 40 to 60 percent of all support tickets, and every one of them has the same answer already sitting in your Shopify admin. Answering them by hand is pure repetition: open the order, read the fulfillment status, copy the tracking link, paste it into a reply.

This workflow removes that loop entirely. It is a reactive lookup, which makes it different from a shipping confirmation email that fires once when an order ships. Here the customer asks at any moment, and the automation answers on demand with whatever the current status is. It sits neatly alongside your other Shopify n8n automations as the front line of self-service support.

┌──────────────────────────────────────────────────────────────┐
│  SHOPIFY ORDER STATUS LOOKUP (WISMO AUTO-REPLY)               │
│                                                              │
│  [Lookup Form]                                               │
│       │  order # + email                                     │
│       ▼                                                      │
│  [Webhook] → [Shopify Admin API lookup] → [Order found?]     │
│                                              │        │       │
│                                     yes ◄────┘        └──► no │
│                                      ▼                    ▼   │
│                          [Build status reply]     [Email:    │
│                                      ▼             not found] │
│                          [Email customer status]             │
│                                      ▼                        │
│                          [Telegram ping to merchant]         │
└──────────────────────────────────────────────────────────────┘
  

Why it beats the default

The default Shopify experience does give buyers an order status page and shipping emails. So why do they still email you? Because they lost the email, the link expired from their point of view, or they simply want a human to confirm. The out-of-the-box tools are passive; they wait to be found. This workflow is active: the moment a customer reaches out, they get a personal reply with the live answer.

Compared to a paid help-desk app with a WISMO add-on, the n8n version costs nothing per ticket and stays entirely under your control. You decide the wording, the branding, and which channel the reply goes out on. And because it reads directly from the Shopify Admin API at request time, the status is never stale, unlike a cached export or a nightly sync.

What you need

  • A Shopify store with Admin API access. If you have not connected Shopify to n8n yet, follow the 2026 Shopify to n8n connection guide first, it uses the current Dev Dashboard method to create your access token.
  • An n8n instance (cloud or self-hosted), version 1.0 or later.
  • A Gmail account (or Outlook / SMTP) to send the reply from.
  • A Telegram bot and chat ID for the optional merchant ping. This is a nice-to-have, not required.
  • A simple lookup form on your store that POSTs the order number and email to the n8n webhook. A plain HTML form or any form app that can send a webhook works.

Estimated build time: 30 to 45 minutes from scratch, or under 10 minutes with the template.

Node-by-node list

Seven nodes, one clean branch. Here is the full map before we build it:

# Node Type Job
1 Where Is My Order? Webhook Receives the order number and email from the lookup form
2 Look Up Order HTTP Request Queries the Shopify Admin API for the order by name
3 Order Found? IF Branches on whether a matching order came back
4 Build Status Reply Edit Fields Extracts status and tracking into clean fields
5 Email Order Status Gmail Sends the customer their live status and tracking link
6 Ping Merchant Telegram Optional heads-up to you that a lookup was answered
7 Email Not Found Gmail Politely tells the customer the order was not matched

Step-by-step build

1 Where Is My Order? (Webhook)

This node is the entry point. It gives you a URL that your lookup form posts to.

  1. Add a Webhook node and set the HTTP Method to POST.
  2. Set a path such as order-status.
  3. Set Respond to Immediately so the form gets a fast acknowledgement while the rest of the flow runs.
  4. Point your store form at the production webhook URL, sending two fields: order_number and email.

After a submission, the data arriving looks like this:

{
  "order_number": "1042",
  "email": "james.carter@gmail.com"
}
💡

Tip: Ask for the email as well as the order number. Matching on both is what stops the workflow from ever sending one customer another customer’s details.

2 Look Up Order (HTTP Request)

This node calls the Shopify Admin API and asks for the order by its name.

  1. Add an HTTP Request node, Method GET.
  2. Set the URL to your store’s orders endpoint on API version 2026-04:

    https://YOUR_STORE.myshopify.com/admin/api/2026-04/orders.json
  3. Turn on Send Query Parameters and add:
    • name = ={{ $json.order_number }}
    • status = any
    • fields = name,email,financial_status,fulfillment_status,fulfillments,customer
  4. Under Authentication, choose Generic Credential Type → Header Auth and set the header name to X-Shopify-Access-Token with your Admin API token as the value.

Shopify returns an orders array. A matched order carries everything you need:

{
  "orders": [
    {
      "name": "#1042",
      "email": "james.carter@gmail.com",
      "financial_status": "paid",
      "fulfillment_status": "fulfilled",
      "fulfillments": [
        { "tracking_number": "9400111899223197428490",
          "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490" }
      ],
      "customer": { "first_name": "James" }
    }
  ]
}
📌

Note: The name query matches the human order number the customer sees, including the #. If your form strips the hash, Shopify still matches on the bare number, so 1042 and #1042 both work.

3 Order Found? (IF)

This node splits the flow depending on whether Shopify returned a match.

  1. Add an IF node.
  2. Create one condition. Left value: ={{ $json.orders.length }}, type Number.
  3. Operation: is greater than. Right value: 0.

The true output goes to Build Status Reply. The false output goes to Email Not Found.

💡

Tip: For stronger safety, add a second condition on the true path that checks orders[0].email equals the submitted email. If it does not match, route it to the not-found reply so details are never emailed to the wrong address.

4 Build Status Reply (Edit Fields)

This node pulls the raw Shopify response into a handful of clean, named fields your email can reference.

  1. Add an Edit Fields (Set) node on the true branch.
  2. Add these string assignments:
    • first_name = ={{ $json.orders[0].customer.first_name }}
    • order_name = ={{ $json.orders[0].name }}
    • status = ={{ $json.orders[0].fulfillment_status || "unfulfilled" }}
    • tracking_url = ={{ $json.orders[0].fulfillments[0]?.tracking_url || "" }}

The result is a tidy object that is easy to drop into an email template:

{
  "first_name": "James",
  "order_name": "#1042",
  "status": "fulfilled",
  "tracking_url": "https://tools.usps.com/go/TrackConfirmAction?tLabels=9400111899223197428490"
}

5 Email Order Status (Gmail)

This node sends the customer their answer.

  1. Add a Gmail node, operation Send.
  2. To: ={{ $('Where Is My Order?').item.json.email }}
  3. Subject: Your order {{ $json.order_name }} status
  4. Message body, for example:
Hi {{ $json.first_name }},

Here is the latest on order {{ $json.order_name }}:

Status: {{ $json.status }}
Tracking: {{ $json.tracking_url }}

Thanks for shopping with us!
💡

Tip: When status is unfulfilled the tracking line will be blank. Reword it with a short expression so the customer reads “Your order is confirmed and being prepared” instead of an empty tracking field.

6 Ping Merchant (Telegram, optional)

A quiet log so you can see the automation working.

  1. Add a Telegram node, operation Send Message.
  2. Chat ID: your YOUR_TELEGRAM_CHAT_ID.
  3. Text: WISMO answered for {{ $json.order_name }} ({{ $json.status }}).

7 Email Not Found (Gmail)

On the false branch, reassure the customer rather than leaving them with silence.

  1. Add a second Gmail node, operation Send.
  2. To: ={{ $('Where Is My Order?').item.json.email }}
  3. Body: tell them you could not match that order number and a team member will follow up, and ask them to double-check the number from their confirmation email.

Common mistakes

Problem Likely cause Fix
Every lookup returns not found The name query is missing or the API version is wrong Confirm the URL uses 2026-04 and the name parameter is wired to the form field
401 Unauthorized from Shopify Access token missing or pasted with a trailing space Recreate the Header Auth credential, header X-Shopify-Access-Token, no spaces
Tracking link is always empty Order has no fulfillment yet, or the wrong array index Read fulfillments[0].tracking_url and handle the unfulfilled case in the email text
Reply goes to the wrong person Matching only on order number Add the email-match condition described in step 3
Form submits but nothing happens Webhook still in Test mode Save and activate the workflow so the production URL is live

Cost at realistic volume

The appeal of this build is that it runs on tools you already pay for, or free tiers. Assume a store fielding 600 WISMO requests a month, a busy small brand.

Component Cost at 600 lookups / month
Shopify Admin API calls Free, well inside rate limits (2 calls per request)
Gmail send Free on a standard account; Workspace has generous daily limits
Telegram ping Free
n8n (self-hosted) Server cost only, effectively 0 marginal per lookup
n8n Cloud Starter 600 executions is a small slice of the monthly quota

Against a help-desk app that charges per resolved ticket, answering 600 requests automatically each month is the difference between a recurring bill and effectively zero. The time saved is the bigger prize: at two minutes of manual handling per ticket, 600 lookups is 20 hours a month you get back.

🚀 Get the Shopify Order Status Lookup Template

The guide above is free to follow. If you would rather skip the build, download the ready-to-import workflow, then just add your credentials. Prefer it installed and tuned for you? See our done-for-you services.

Download the template ($12) →

Instant download · Works on n8n Cloud and self-hosted

FAQ

Does this replace my Shopify support inbox?

No. It handles the single most repetitive question, where is my order, and leaves genuine issues for a human. Customers who submit a lookup get an instant, accurate status email, so your inbox fills up with real problems instead of tracking-number requests you could have answered from data.

What if the order has not shipped yet?

The workflow reads the fulfillment status from Shopify. When there is no fulfillment yet, the email says the order is confirmed and being prepared, with no tracking link. The customer still gets a clear answer, which is usually all they wanted before they emailed you a second time.

Do I need to write code to build this?

No. Every step is a standard n8n node configured through the visual editor. The only expressions you type are short references like the order number and email fields. If you can fill in a form, you can build and run this order status lookup automation.

How does the workflow find the right order?

It queries the Shopify Admin API by order name, the number the customer sees on their confirmation, then confirms the email on the order matches the one submitted. That two-part check stops the workflow from emailing order details to the wrong person.

Can I send the reply from Outlook instead of Gmail?

Yes. Swap the Gmail node for the Microsoft Outlook node or a generic SMTP node and keep the same message body. The rest of the workflow, the webhook intake and the Shopify lookup, does not change at all when you switch the email sender.

Related guides

n8n
Shopify
Gmail
Telegram
WISMO
automation

Is Your Shopify Store Visible in AI Answers? (Free n8n Template)

More and more shoppers skip Google and ask an AI instead: “what is the best organic cotton t-shirt to buy,” “recommend a natural skincare brand for sensitive skin.” The assistant answers with a short list of brands and stores. If your Shopify store is on that list, you get the visit. If it is not, you are invisible, and you will not see it in your normal analytics because no click ever happened. This guide gives you a free n8n template that checks, every week, whether an AI recommends your store for the buying questions your customers actually ask, and logs the answer to Google Sheets so you can track it over time.

AI visibility is the new SEO for Shopify stores

Ranking on Google still matters, but a growing share of product research now happens inside AI answers. When a shopper asks an assistant for a recommendation, the model replies with a handful of specific brands and stores. Being one of them is the new front page. People call this AEO, answer engine optimization, or simply AI visibility: making sure your store shows up when an AI answers a buying question.

The hard part is that this is invisible by default. There is no rank tracker built into ChatGPT, and an AI answer that leaves you out produces no impression and no click, so nothing shows up in Search Console or your store analytics. The only way to know where you stand is to ask the questions yourself, on a schedule, and record the answers. That is exactly what this workflow does.

What this free n8n template does

Once a week, the workflow runs through a list of shopping questions you care about, asks an AI each one as if it were a real shopper, checks whether your store name or domain appears in the answer, and writes a row to a Google Sheet. Over a few weeks you build a simple, honest record of your AI visibility that you can watch trend up as you improve it.

  • Runs on a weekly schedule, hands off, no manual checking.
  • Uses Google Gemini’s free tier, so there is no per-check cost.
  • Logs date, prompt, a clear Yes or No for whether your store was mentioned, and the full AI answer for context.
  • Everything lives in your own n8n and your own Google Sheet. No third-party AEO tool subscription.

Download the template

Free download: Shopify AI Visibility Monitor (JSON). In n8n, choose Import from File and select it. New to connecting n8n to your stack? Start with the 2026 Shopify to n8n connection guide. This particular workflow does not even need a Shopify connection, only a free Gemini key and a Google Sheet.

How the workflow works

  • Weekly check: a schedule trigger fires once a week, on Monday morning by default.
  • Your store and prompts: one node holds your store name, your domain, and the list of buying questions to track. This is the only node you edit.
  • Ask the AI as a shopper: each question is sent to Google Gemini with a shopping-assistant instruction, so the model answers the way it would for a real customer.
  • Is your store mentioned: a small check looks for your store name or domain in the answer and marks it Yes or No.
  • Log to sheet: the date, the question, the Yes or No, and the AI answer are appended as a row in your Google Sheet.

Setup (about 5 minutes)

  1. Import the JSON into n8n.
  2. Open Your store and prompts and set your store name, your domain, and the five example questions. Use the real questions your customers would ask an AI before buying.
  3. On Google Gemini Chat Model, add a free Gemini API key from Google AI Studio. No credit card, no OpenAI subscription needed.
  4. Make a Google Sheet with the headers Date, Prompt, Store mentioned, AI answer. On Log to sheet, add your Google Sheets credential and pick that sheet and tab.
  5. Save, run it once to confirm rows appear, then turn the workflow Active.

What questions should you track?

The quality of your monitor depends on the questions you feed it. Pick the ones a real shopper would type before buying what you sell, not your brand name. A good mix looks like this:

  • Category questions: “best [your product category] to buy online,” the broad query where you want to be one of the names.
  • Problem questions: “what should I use for [the problem your product solves],” how shoppers describe their need before they know the product.
  • Attribute questions: “best [material or feature] [product],” for the specifics your range is known for, like organic, handmade, or budget.
  • Occasion questions: “gift ideas for [your audience],” if your products fit a gifting or seasonal moment.

Five to ten questions is plenty to start. Keep them stable so your week-to-week comparison stays honest, and add new ones as you expand your range.

How to read your results

Open your sheet after the first run. Each row is one buying question and whether the AI mentioned your store. A column full of No is not a failure, it is your baseline. The point is the trend: as you improve the things that feed AI answers, you want to see No turn into Yes for more of your questions over the following weeks. Keep the AI answer column too, because it shows which competitors are being recommended instead of you, which is a useful shortlist of who is winning the AI shelf in your category.

How to actually improve your AI visibility

Monitoring tells you where you stand. Moving the needle comes down to giving AI models clear, trustworthy, well-structured information about your products, in the places they read. A few things that help:

  • Rich, specific product descriptions. Vague copy gives a model nothing to latch onto. Detailed, benefit-led descriptions with materials, use cases, and specifics are far more likely to be surfaced. Our Shopify AI Product Description agent writes this kind of copy automatically on Gemini.
  • Reviews and third-party mentions. Models lean on what independent sources say about you. Being reviewed and listed on relevant sites in your niche builds the signal that you are a real option.
  • Clean structured data. Product schema, clear titles, and consistent naming make it easy for a model to understand what you sell and who it is for.
  • Answer the buying questions on your own site. Content that directly answers the questions in your tracker gives models a source to cite.

Re-run the monitor every week and watch the Yes count climb. For the full toolkit, browse the best n8n AI agent templates for Shopify and the wider n8n Shopify automation guide.

Frequently asked questions

What is AEO, or answer engine optimization?

AEO is the practice of getting your store recommended inside AI answers, the way SEO is about ranking in search results. When a shopper asks an assistant for a product recommendation, AEO is what decides whether your store is in the reply.

How do I check if my Shopify store appears in AI answers?

Import the free template above, add your store name and a list of buying questions, connect a Gemini key and a Google Sheet, and activate it. Each week it asks the AI those questions and logs whether your store was mentioned.

Is it free to run?

Yes. It runs on Google Gemini’s free tier, and n8n is free when self hosted. A weekly run of a handful of prompts stays well within the free limits, so there is no per-check cost.

Which AI models does it check?

By default it checks Google Gemini, because it is free to run. Gemini draws on the same kind of public signals other assistants use, so it is a strong free proxy for your overall AI visibility. If you have OpenAI or Perplexity keys, you can add them as extra query nodes to check those surfaces directly.

How do I improve my store’s visibility in AI answers?

Give models clear, specific, trustworthy information about your products: detailed descriptions, reviews and mentions on independent sites, clean product structured data, and content that answers real buying questions. Then re-run the monitor to confirm your Yes count is rising.

Want the alerts and agents that pair with this? See the best n8n AI agent templates for Shopify, all tested and free to run on Gemini.

Shopify sales tax report automation with n8n

Shopify sales tax report workflow in n8n










This shopify sales tax report automation n8n build runs on the first of every month, pulls the previous month’s orders from the Shopify Admin API, sums the tax you collected by state and jurisdiction, writes a clean summary row to Google Sheets, and emails you the totals. No paid tax app, no CSV exports, no manual pivot tables. You keep a permanent audit trail and hand your accountant a tidy sheet instead of a raw order dump.

Prefer to skip the setup? Grab the ready-made template and be running in under 10 minutes.

What it does

Every Shopify store collects sales tax, and every store owner eventually has to answer a boring but unavoidable question: how much tax did I actually collect last month, and where? Shopify shows you a total in the admin, but the moment you need it broken down by state, county, or tax name for a filing, you are back to exporting orders and building a spreadsheet by hand.

This workflow removes that chore. On a monthly schedule it reads your orders through the n8n Shopify automation pattern, then does three things:

  • Adds up total collected tax, subtotal, and gross sales for the month.
  • Breaks that tax down by jurisdiction using each order’s tax_lines, so “California State Tax” and “Los Angeles County” appear as separate lines.
  • Saves the summary to a Google Sheet and emails it to you, so the number is filed before you even open your inbox.

The output is the sheet a bookkeeper actually wants: one row per month, tax grouped by name, sales grouped by currency, and a running history you can hand over at quarter end.

Why it beats the default

Shopify’s built-in reports give you a single tax figure and a finance summary, but three things push store owners toward automation.

First, the native breakdown is shallow. You can see total tax, but pulling collected tax per jurisdiction across a full month still means an export and a pivot table. If you sell into several states with economic nexus, that manual step happens every single month.

Second, the manual export has no memory. Once you close the spreadsheet, nothing is archived in a consistent place. Six months later, when you reconcile or an accountant asks for March, you rebuild it from scratch.

Third, paid tax-reporting apps solve this but add a recurring bill and yet another dashboard to log into. For a store that just needs a monthly collected-tax summary for filing prep, that is more tool than the job requires.

The n8n version sits in the middle: it reads the same authoritative order data Shopify already stores, runs on your schedule, and drops a permanent record into a sheet you own. No per-order fee, no vendor lock-in.

📌

This is a reporting and reconciliation tool for tax you already collected. It is not a tax engine and does not decide what you owe. Always have a professional review filings.

What you need

  • A running n8n instance, either n8n Cloud or self-hosted.
  • A Shopify custom app access token created through the 2026 Shopify Dev Dashboard, with read_orders scope. If you have not connected Shopify to n8n yet, follow how to connect Shopify to n8n in 2026 first.
  • A Google account for Google Sheets and Gmail (both free tiers are plenty).
  • One blank Google Sheet with a header row to receive the monthly summaries.

Build time is roughly 30 to 40 minutes from scratch, or under 10 minutes if you import the template and paste in your credentials.

Node-by-node list

The workflow is a straight line, no branches. Six nodes, one authoritative data source.

┌────────────────────────────────────────────────────────────────┐
│  SHOPIFY MONTHLY SALES TAX REPORT                              │
│                                                                │
│  [Schedule Trigger]                                            │
│        ↓                                                        │
│  [Build Date Range] (Code)                                     │
│        ↓                                                        │
│  [Get Orders] (HTTP Request, paginated)                        │
│        ↓                                                        │
│  [Aggregate Tax] (Code)                                        │
│        ↓                                                        │
│  [Append to Sheet] (Google Sheets)   →   [Email Report] (Gmail)│
└────────────────────────────────────────────────────────────────┘
# Node Type Job
1 Schedule Trigger scheduleTrigger Fire on the 1st of each month at 06:00
2 Build Date Range code Compute previous month’s start and end in ISO
3 Get Orders httpRequest Pull last month’s orders with tax_lines, paginated
4 Aggregate Tax code Sum tax by jurisdiction, sales by currency
5 Append to Sheet googleSheets Write one summary row per month
6 Email Report gmail Send the totals to you

Step-by-step build

1 Schedule Trigger

Add a Schedule Trigger node. Set the rule to a Months interval, trigger at day of month 1, hour 6. That runs the report early on the first day of each month, covering the full month that just ended.

💡

Set your instance timezone in n8n settings before you rely on scheduled dates. A store on US Central time reporting against a UTC instance can pull the wrong day at month boundaries.

2 Build Date Range (Code)

Add a Code node to compute last month’s window. This keeps the workflow self-adjusting so you never hardcode dates.

const now = new Date();
const firstThisMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), 1));
const firstLastMonth = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth() - 1, 1));

return [{
  json: {
    created_at_min: firstLastMonth.toISOString(),
    created_at_max: firstThisMonth.toISOString(),
    label: firstLastMonth.toISOString().slice(0, 7) // e.g. "2026-06"
  }
}];

3 Get Orders (HTTP Request)

Add an HTTP Request node. Use GET against the Admin API orders endpoint and attach your Shopify token as a Header Auth credential named X-Shopify-Access-Token.

  • URL: https://YOUR_STORE.myshopify.com/admin/api/2026-04/orders.json
  • Query parameters: status=any, created_at_min={{ $json.created_at_min }}, created_at_max={{ $json.created_at_max }}, limit=250, and fields=id,created_at,currency,subtotal_price,total_price,total_tax,tax_lines.
  • Turn Pagination on. Set the mode to follow the response’s Link header so n8n walks every page until Shopify stops returning a next link.
💡

Requesting only the fields you need keeps each order payload small. On a busy store that is the difference between one fast call and a slow, memory-heavy pull of full order objects.

4 Aggregate Tax (Code)

This is the heart of the workflow. Add a Code node in “Run once for all items” mode. It walks every order, sums the money, and groups collected tax by the jurisdiction name Shopify records in tax_lines.

const orders = $input.all().map(i => i.json);
const byJurisdiction = {};
const byCurrency = {};
let totalTax = 0, orderCount = 0;

for (const o of orders) {
  orderCount++;
  const cur = o.currency || 'USD';
  byCurrency[cur] = byCurrency[cur] || { subtotal: 0, gross: 0, tax: 0 };
  byCurrency[cur].subtotal += parseFloat(o.subtotal_price || 0);
  byCurrency[cur].gross    += parseFloat(o.total_price || 0);
  byCurrency[cur].tax      += parseFloat(o.total_tax || 0);
  totalTax += parseFloat(o.total_tax || 0);

  for (const t of (o.tax_lines || [])) {
    const key = t.title || 'Unnamed tax';
    byJurisdiction[key] = (byJurisdiction[key] || 0) + parseFloat(t.price || 0);
  }
}

const label = $('Build Date Range').first().json.label;

return [{
  json: {
    month: label,
    order_count: orderCount,
    total_tax_collected: Number(totalTax.toFixed(2)),
    tax_by_jurisdiction: byJurisdiction,
    sales_by_currency: byCurrency,
    jurisdiction_summary: Object.entries(byJurisdiction)
      .map(([k, v]) => `${k}: ${v.toFixed(2)}`).join(' | ')
  }
}];

After this node one clean item looks like:

{
  "month": "2026-06",
  "order_count": 214,
  "total_tax_collected": 1873.44,
  "jurisdiction_summary": "California State Tax: 902.10 | Los Angeles County: 311.20 | Texas State Tax: 660.14"
}

5 Append to Sheet (Google Sheets)

Add a Google Sheets node set to the Append operation. Pick your spreadsheet and sheet, then map the columns: Month, Orders, Total Tax, and Jurisdiction Breakdown. Each monthly run adds exactly one row, so the sheet becomes a year-at-a-glance tax ledger.

6 Email Report (Gmail)

Add a Gmail node with the Send operation. Address it to yourself or your bookkeeper, subject Sales tax report {{ $json.month }}, and drop the total plus the jurisdiction summary into the body. Now the number reaches you without opening n8n or Shopify.

Common mistakes

  • Summing total_tax and also summing tax_lines into the same total. They describe the same money at different granularity. Use the order-level total_tax for the grand total and tax_lines only for the per-jurisdiction breakdown.
  • Leaving pagination off. Without it you only get the first 250 orders and every busy month silently under-reports. Always confirm the Link-header pagination is active.
  • Using status=open instead of status=any. Archived and cancelled orders still carry collected tax that belongs in a reconciliation, so any is the safe default.
  • Running the schedule against the wrong timezone, which shifts the month boundary and double-counts or drops a day’s orders.
  • Treating the output as owed tax. It is collected tax. Owed tax depends on nexus rules your accountant applies on top of this data.

Cost at realistic volume

The whole build runs on free infrastructure. Here is the monthly math for a store doing a few hundred orders.

Component Usage per run Cost
Shopify Admin API 1 to 4 paginated calls, once a month Free on every plan
n8n 1 execution/month, 6 nodes Free self-hosted, or well inside Cloud starter
Google Sheets 1 row appended Free
Gmail 1 email sent Free

Even a store pushing several thousand orders a month stays free: pagination just adds a handful of API calls, and you are still writing a single summary row. Compare that to a paid tax-reporting app billing monthly, and the automation pays for itself immediately.

🚀 Get the Shopify sales tax report template

The guide above is free to follow. If you would rather skip the build, download the ready-to-import workflow, then just add your credentials. Prefer it installed and tuned for you? See our done-for-you services.

Download the template ($14) →

Instant download · Works on n8n Cloud and self-hosted

Frequently asked questions

Does this workflow calculate the tax I owe?

No. It reports the tax you already collected from customers, grouped by jurisdiction, straight from Shopify order data. It is a reconciliation and filing-prep tool, not a tax engine. Your accountant or filing software decides what is actually remitted and where.

Which Shopify orders are included in the report?

Every order created in the previous calendar month with status=any, so paid, partially refunded, and cancelled orders all appear. You can filter to paid orders only inside the aggregation Code node if your accountant prefers a stricter cash-basis view of collected tax.

Do I need Shopify Plus or a paid tax app?

No. The workflow only reads the standard Admin API orders endpoint, which every Shopify plan exposes. There is no dependency on Shopify Tax, Avalara, or any paid reporting add-on. Your only real cost is running n8n, which can be entirely free when self-hosted.

How far back can the report go?

The default range is last month, but you can set any created_at_min and created_at_max window in the date node to pull a quarter or a full year. Shopify retains order history for the life of the store, so historical backfills work without extra tooling.

What if a customer paid in a different currency?

Shopify stores each order’s currency. The template groups totals by currency code, so multi-currency stores get one subtotal and tax figure per currency. If you only sell in one currency, that grouping collapses to a single line and you can ignore it.

Related guides

n8n
Shopify
Google Sheets
Gmail
automation

Shopify store backup automation with n8n (free and self-hosted)

Shopify store backup workflow in n8n








Shopify store backup automation with n8n saves a nightly JSON snapshot of your products, orders, and customers to Google Drive, then logs each run to a Google Sheet and emails you a summary. It replaces backup apps that charge a monthly fee for the same data your store already exposes through the Admin API. You keep the files, the schedule, and the retention rules. Building it from scratch takes about 40 minutes.

Prefer to skip the setup? Grab the ready-made template and be running in under 10 minutes.

What it does

Shopify keeps your live data, but it does not hand you a full historical backup you control. If a bulk edit goes wrong, an app corrupts your catalog, or you ever need last month’s customer list, the platform will not give you a point-in-time copy. This workflow fills that gap.

Every night, n8n reads three datasets from your store through the Admin API: the full product catalog, all orders, and all customers. It bundles them into one dated JSON file, uploads that file to a Google Drive folder, appends a row to a running log sheet, and sends you a short confirmation email. The next morning you have a fresh, timestamped copy of everything, sitting in storage you own.

This is part of the broader pattern of connecting your store to a workflow engine. If you are new to it, start with our guide to n8n Shopify automation and come back here.

Why it beats the default

Most merchants handle backups in one of two ways, and both leave gaps.

The first is a paid backup app from the Shopify App Store. These work, but they charge a recurring fee that scales with your order volume, and your snapshots live inside a vendor’s account rather than yours. If that vendor changes pricing or shuts down, your safety net goes with it.

The second is manual CSV exports from the admin. This is free but partial. You export products one day, forget orders the next, and end up with a scattered pile of files that nobody dated properly. It relies on a human remembering to do a boring task on time, which is exactly the kind of task automation exists to remove.

The n8n version gives you the full three-dataset snapshot, on a fixed schedule, written to your own Drive, with a log you can audit. There is no per-record pricing, and you decide how long to keep each copy.

Approach Coverage Who holds the files Ongoing cost
Paid backup app Full The vendor Monthly fee
Manual CSV export Partial, easy to forget You Free, but manual
n8n workflow Full, automatic You Near zero

What you need

  • An n8n instance, either n8n Cloud or a self-hosted install.
  • A Shopify custom app access token with read scopes for products, orders, and customers. Follow our walkthrough on how to connect Shopify to n8n in 2026, which uses the current Dev Dashboard method and a modern Admin API version (2026-04).
  • A Google account for Drive, Sheets, and Gmail, connected to n8n with OAuth.
  • One Google Drive folder to hold the snapshots and one Google Sheet for the run log.

Estimated build time is 30 to 45 minutes from scratch, or under 10 minutes with the template.

How it works

The workflow fans out to three Shopify reads in parallel, waits for all of them to finish, then assembles and stores the snapshot.

┌──────────────────────────────────────────────────────────────┐
│  SHOPIFY NIGHTLY BACKUP                                       │
│                                                              │
│  [Schedule 02:00] ─┬─→ [Get all products] ─┐                 │
│                    ├─→ [Get all orders]   ─┤                 │
│                    └─→ [Get all customers]─┘                 │
│                                    ↓                         │
│                               [Merge (3)]                    │
│                                    ↓                         │
│                          [Build snapshot file]               │
│                                    ↓                         │
│              [Upload to Drive] → [Log to Sheet] → [Email]     │
└──────────────────────────────────────────────────────────────┘
  

Node-by-node list

# Node Type Role
1 Schedule Trigger scheduleTrigger v1.3 Fires the backup once a day at 02:00
2 Get all products shopify v1 Reads the full product catalog
3 Get all orders shopify v1 Reads all orders, any status
4 Get all customers shopify v1 Reads the customer list
5 Merge merge v3.2 Waits for all three reads to finish
6 Build snapshot file code v2 Assembles one JSON file and its counts
7 Upload to Google Drive googleDrive v3 Stores the dated snapshot file
8 Log to Google Sheets googleSheets v4.7 Appends one row per run
9 Email confirmation gmail v2.1 Sends a short summary

Step-by-step build

1 Schedule the run

Add a Schedule Trigger node. Set the rule to a daily interval and pick a quiet hour such as 02:00. Overnight runs keep the backup away from busy checkout traffic and the daily API limits that come with it.

💡

Set your n8n instance timezone under Settings so 02:00 means your store’s local night, not UTC.

2 Read products, orders, and customers

Add three Shopify nodes, each connected directly to the Schedule Trigger so they run in parallel from a single trigger item. Configure them like this:

  1. Node one: resource Product, operation Get Many, and turn on Return All so it pages through the entire catalog.
  2. Node two: resource Order, operation Get Many, Return All on, and under options set Status to Any so cancelled and archived orders are included.
  3. Node three: resource Customer, operation Get Many, Return All on.

Attach your Shopify Access Token credential to all three. Each node runs once and returns its full dataset.

📌

Return All is the single most important setting here. With it off, each node stops at the first page of 50 records and your backup silently misses everything after that.

3 Merge the three branches

Add a Merge node and set the number of inputs to 3. Connect each Shopify node to a separate input. Its only job is to pause the flow until all three reads have finished, so the next node does not run before the data is ready.

4 Build the snapshot file

Add a Code node running once for all items. It pulls each dataset by node name, wraps them in one object with a timestamp and record counts, and returns the result as an attached JSON file.

const products  = $('Get all products').all().map(i => i.json);
const orders    = $('Get all orders').all().map(i => i.json);
const customers = $('Get all customers').all().map(i => i.json);

const stamp = $now.format('yyyy-LL-dd');
const fileName = `shopify-backup-${stamp}.json`;

const snapshot = {
  generated_at: $now.toISO(),
  store: 'your-store.myshopify.com',
  counts: {
    products:  products.length,
    orders:    orders.length,
    customers: customers.length
  },
  products, orders, customers
};

return [{
  json: {
    file_name: fileName,
    date: stamp,
    products: products.length,
    orders: orders.length,
    customers: customers.length
  },
  binary: {
    data: {
      data: Buffer.from(JSON.stringify(snapshot, null, 2)).toString('base64'),
      mimeType: 'application/json',
      fileName
    }
  }
}];

After this node, one item carries both the plain counts and the ready-to-upload file:

{
  "file_name": "shopify-backup-2026-07-11.json",
  "date": "2026-07-11",
  "products": 214,
  "orders": 1863,
  "customers": 1592
}

5 Upload to Google Drive

Add a Google Drive node with operation Upload. Set the input binary field to data, and choose the destination folder you created for backups. Use the file name from the previous node so every upload keeps its date.

  1. Operation: Upload.
  2. File Name: ={{ $json.file_name }}
  3. Input Binary Field: data
  4. Parent Folder: select your Shopify Backups folder.

6 Log the run to Google Sheets

Add a Google Sheets node with operation Append. Point it at a log sheet and map the columns to the fields from the Code node. This gives you an audit trail you can scan at a glance.

7 Email the summary

Add a Gmail node with operation Send. Send yourself a short message so you know the backup ran without opening Drive.

Subject: Shopify backup complete — {{ $json.date }}

Products: {{ $json.products }}
Orders:   {{ $json.orders }}
Customers:{{ $json.customers }}
File:     {{ $json.file_name }}
💡

If you prefer a chat ping over an inbox, swap the Gmail node for a Telegram node and send the same summary to your admin channel.

The data structure

The Google Sheet log is deliberately simple. One row per night is enough to prove the backup ran and to spot a sudden drop in record counts that might signal a problem upstream.

Column Type Example Description
date Date 2026-07-11 The day the backup ran
products Number 214 Product records captured
orders Number 1863 Order records captured
customers Number 1592 Customer records captured
file_name Text shopify-backup-2026-07-11.json The Drive file for that day
📌

Make the sheet header row match these column names exactly. The Append node maps by header, so a typo means an empty column.

Common mistakes

  • Leaving Return All off on the Shopify nodes, which caps each dataset at 50 records and produces a backup that looks fine but is missing most of the store.
  • Chaining the three Shopify reads one after another instead of running them in parallel into a Merge. Chaining makes the second read run once per record from the first, which multiplies API calls and can trip rate limits.
  • Forgetting to set the Input Binary Field on the Drive node to data, which uploads an empty or wrong file.
  • A mismatch between the Sheet header row and the mapped fields, which writes blank cells.
  • Running on UTC when you meant local time, so the 02:00 backup lands in the middle of your business day.
  • Granting the access token read scope for only one resource. It needs read access to products, orders, and customers, or the missing branch returns an error.

Cost at realistic volume

Take a store with 200 products, 1,800 orders, and 1,600 customers running one backup a night. Every service in the workflow sits inside a free tier at that scale.

Service Usage per night Cost
Shopify Admin API A few dozen paged read calls Free on your own store
Google Drive One JSON file, a few megabytes Free within 15 GB
Google Sheets One appended row Free
Gmail One email Free
n8n One scheduled execution Free self-hosted, or one run on Cloud

A daily snapshot of a few megabytes adds up to roughly a gigabyte a year, well within the free Drive quota. Compared with a backup app that bills every month whether or not you ever restore, the running cost here rounds to zero.

🚀 Get the Shopify backup workflow

The guide above is free to follow. If you would rather skip the build, download the ready-to-import workflow, then just add your credentials. Prefer it installed and tuned for you? See our done-for-you services.

Download the template ($12) →

Instant download · Works on n8n Cloud and self-hosted

Frequently asked questions

Does this replace a paid Shopify backup app?

For most stores, yes. It captures the same products, orders, and customers a backup app reads through the Admin API, but the files land in your own Google Drive on your own schedule. What it does not do is one-click restore, so keep the snapshots organized and readable.

How much Shopify data can the workflow back up?

As much as the Admin API returns. With the Shopify node set to return all records, it pages through every product, order, and customer regardless of store size. Very large catalogs take longer per run, so schedule the backup overnight when API traffic is quiet.

Where are the backup files stored?

In a Google Drive folder you choose. Each run writes one dated JSON file such as shopify-backup-2026-07-11.json. Google Drive gives 15 GB free, which holds years of daily snapshots for a typical store since each file is only a few megabytes.

Can I restore my store from these backups?

The JSON files hold the raw record data, so you can re-import products or re-create customers through the Admin API or a CSV. This is a data safety net, not a one-click rollback. Test a small restore once so you know the file format before you ever need it.

Is it safe to store customer data in Google Drive?

The files contain personal data, so treat the Drive folder like any customer record. Restrict sharing, keep it inside your business Google account, and delete old snapshots on a retention schedule that matches your privacy policy and local regulations.

Related guides

n8n
Shopify
Google Drive
Google Sheets
backup
automation

Shopify bulk price update from Google Sheets with n8n

New to n8n? Start with our step-by-step setup guide and the 2026 Shopify connection guide, then come back to build this workflow.








A Shopify bulk price update from Google Sheets with n8n turns your sale-prep spreadsheet into the thing that actually changes prices. You keep one sheet of variant IDs and new prices, flip an “apply” column to yes, and run the workflow. n8n groups the rows by product, calls the Shopify GraphQL Admin API once per product, logs every result to a second tab, and emails you a summary. Setup takes about 30 minutes and costs nothing beyond your own n8n instance.

What this workflow does

Every store hits the same week twice a year. Black Friday is coming, or the season is turning, and 200 variants need new prices by Friday. Shopify’s bulk editor handles it, but it is a grid you scroll sideways forever, with no dry run, no record of what changed, and no way to hand the price list to someone else for approval.

This workflow moves the decision into a Google Sheet and leaves execution to n8n. The sheet holds one row per variant: product ID, variant ID, the SKU so a human can read it, the new price, an optional compare-at price for the strike-through, and an apply column that acts as a safety switch. Rows without yes in apply are ignored.

When you run it, n8n reads the sheet, drops the rows you did not approve, groups the remaining variants by parent product, and sends one GraphQL mutation per product. Shopify accepts up to 250 variants in a single productVariantsBulkUpdate call, so a 300-variant repricing across 90 products becomes 90 API calls, not 300. Every result lands in a log tab with a timestamp and rolls into an email telling you which products succeeded and which did not.

Why it beats the default

The Shopify bulk editor is fine for ten variants. Past that it starts costing you in ways that are hard to see until something goes wrong.

A sheet gives you review before execution. Your buyer or your accountant can compare the price column against cost, and put yes in the apply column only for the rows they are confident about. Half the sheet ships, half waits. The bulk editor has no concept of a pending change.

A sheet gives you a rollback. Add an old_price column, fill it once from your product export, and if the sale prices were wrong you swap the two columns and re-run. Undoing a bulk edit in the admin means recovering the old values from a backup you probably did not take.

A sheet gives you an audit trail. The log tab records the product ID, how many variants were sent, whether Shopify accepted it, the error text if it did not, and when. Six months later, when someone asks why this SKU was 19.99 in November, the answer is a row, not a guess. A sheet is also delegable: hand the link to a merchandiser who has never opened the admin, they fill in prices, you press run.

What you need

  • A self-hosted or cloud n8n instance. Most stores running this self-host, because the workflow writes directly to product data.
  • A Shopify custom app created through the Dev Dashboard, with the write_products and read_products Admin API scopes. The 2026 Shopify to n8n connection guide walks through the current method. The old admin-side custom app flow no longer exists.
  • A Google Sheets OAuth2 credential in n8n, plus a Gmail credential for the summary email. Any SMTP node works instead of Gmail.
  • A Google Sheet with two tabs. Tab prices: product_id, variant_id, sku, new_price, compare_at_price, apply. Tab log: product_id, variants_sent, status, message, run_at.

Populate the prices tab from any Shopify product export. The export gives you SKUs and current prices; the numeric variant IDs come from a products query against the Admin API.

Node-by-node list

Ten nodes. The trigger is manual on purpose: nobody wants a cron job quietly changing prices at 3am.

  1. Run bulk update, a Manual Trigger. You press it when the sheet is ready.
  2. Read price sheet, a Google Sheets node in read mode pointed at the prices tab. Returns one item per row.
  3. Keep rows marked apply, a Filter node. Passes only rows where apply equals yes.
  4. Group variants by product, a Code node running once for all items. Converts the numeric IDs into Shopify global IDs, validates each price, and emits one item per product carrying an array of variant inputs.
  5. Loop over products, a Loop Over Items node with batch size 1. One product per iteration.
  6. Update variant prices, an HTTP Request node posting the productVariantsBulkUpdate mutation to the GraphQL Admin API. Continues on error so one bad product cannot abort the run.
  7. Build log row, an Edit Fields node. Flattens the mutation response into the five log columns.
  8. Throttle, a Wait node set to one second, wired back into the loop.
  9. Log results to Sheets, a Google Sheets append on the log tab, fed by the loop’s done output.
  10. Summarize run and Email summary, a Code node that counts successes and failures, then a Gmail node that sends you the result.

Step-by-step build

1. Create the trigger and read the sheet

Add a Manual Trigger and name it Run bulk update. Connect it to a Google Sheets node named Read price sheet. Choose your document, select the prices sheet, and leave the operation on read with no filters. Execute it once and confirm you see one item per spreadsheet row. new_price may arrive as a string or a number depending on cell formatting; either is fine, the Code node normalizes it.

2. Filter to approved rows

Add a Filter node named Keep rows marked apply with one condition: string, {{ $json.apply }}, equals, yes. Turn on the case-insensitive option. This node is your safety switch. Clear the apply column and running the workflow does nothing at all.

3. Group the variants by product

Add a Code node named Group variants by product, mode “Run once for all items”. It rejects rows with a missing product ID or an unparseable price, converts numeric IDs to the gid://shopify/... form the GraphQL API expects, formats prices to two decimals, and only sets a compare-at price when it is higher than the new price, because Shopify rejects a strike-through cheaper than the price it strikes through.

const GQL = `mutation bulkPriceUpdate($productId: ID!, $variants: [ProductVariantsBulkInput!]!) {
  productVariantsBulkUpdate(productId: $productId, variants: $variants) {
    productVariants { id price compareAtPrice }
    userErrors { field message }
  }
}`;

const groups = {};

for (const item of $input.all()) {
  const row = item.json;
  const pid = String(row.product_id ?? '').trim();
  if (!pid) continue;

  const price = Number(String(row.new_price ?? '').replace(',', '.'));
  if (!Number.isFinite(price) || price <= 0) continue;

  const variant = {
    id: `gid://shopify/ProductVariant/${String(row.variant_id).trim()}`,
    price: price.toFixed(2),
  };

  const compareAt = Number(String(row.compare_at_price ?? '').replace(',', '.'));
  if (Number.isFinite(compareAt) && compareAt > price) {
    variant.compareAtPrice = compareAt.toFixed(2);
  }

  (groups[pid] ||= []).push(variant);
}

return Object.entries(groups).map(([pid, variants]) => ({
  json: {
    gql: GQL,
    productGid: `gid://shopify/Product/${pid}`,
    productId: pid,
    variants,
    variantCount: variants.length,
  },
}));

4. Loop one product at a time

Add a Loop Over Items node named Loop over products and set batch size to 1. Its “loop” output feeds the HTTP request. Its “done” output feeds the logging branch you build in step 8.

5. Send the GraphQL mutation

Add an HTTP Request node named Update variant prices on the loop output. Configure it exactly like this:

  • Method: POST
  • URL: https://YOUR-STORE.myshopify.com/admin/api/2026-04/graphql.json
  • Authentication: Predefined Credential Type, then Shopify Access Token API, then pick the credential you created from your Dev Dashboard app.
  • Send Body: on. Body Content Type: JSON. Specify Body: Using JSON.
  • JSON body, as an expression: {{ JSON.stringify({ query: $json.gql, variables: { productId: $json.productGid, variants: $json.variants } }) }}

Then open Settings on the node and set On Error to “Continue (using regular output)”. This matters. Without it, one product with a deleted variant kills the whole run at product 47 of 90, and you have no record of which ones already went through. Note also that Shopify returns HTTP 200 even for a rejected mutation. The failures live in data.productVariantsBulkUpdate.userErrors, which is why the next node reads that array rather than trusting the status code.

6. Flatten the response into a log row

Add an Edit Fields node named Build log row and add five assignments:

  • product_id, string, {{ $('Loop over products').item.json.productId }}
  • variants_sent, number, {{ $('Loop over products').item.json.variantCount }}
  • status, string, {{ $json.data?.productVariantsBulkUpdate && $json.data.productVariantsBulkUpdate.userErrors.length === 0 ? 'ok' : 'error' }}
  • message, string, {{ ($json.data?.productVariantsBulkUpdate?.userErrors ?? []).map(e => e.message).join('; ') || ($json.errors ?? []).map(e => e.message).join('; ') }}
  • run_at, string, {{ $now.toISO() }}

Reaching back to $('Loop over products') instead of reading the HTTP response is deliberate: when a call fails outright, the response carries no product ID at all.

7. Throttle and close the loop

Add a Wait node named Throttle, set to 1 second, and connect it back into the input of Loop over products. Shopify’s GraphQL Admin API uses a leaky-bucket cost model rather than a simple request cap, and a variants mutation is cheap, so one second per product leaves a lot of headroom. A 90-product run finishes in about a minute and a half.

8. Log everything and email yourself

From the loop’s done output, add a Google Sheets node named Log results to Sheets. Operation: append row. Sheet: log. Mapping: map automatically from input, since the Edit Fields node already produced matching column names. After it, add a Code node named Summarize run, mode “Run once for all items”:

const rows = $input.all().map(i => i.json);
const ok = rows.filter(r => r.status === 'ok');
const failed = rows.filter(r => r.status !== 'ok');
const variants = ok.reduce((n, r) => n + Number(r.variants_sent || 0), 0);

return [{
  json: {
    subject: `Shopify repricing: ${ok.length} products updated, ${failed.length} failed`,
    html: `<p>Products updated: ${ok.length}</p>`
      + `<p>Variants repriced: ${variants}</p>`
      + `<p>Failed: ${failed.length}</p>`
      + (failed.length
          ? `<ul>${failed.map(r => `<li>${r.product_id}: ${r.message}</li>`).join('')}</ul>`
          : ''),
  },
}];

Finish with a Gmail node named Email summary, operation send, To set to your address, Subject {{ $json.subject }}, Message {{ $json.html }}, email type HTML.

9. Do a dry run

Before pointing this at 300 rows, put yes in the apply column for exactly one cheap product. Run it, check the product in the Shopify admin, check the log tab, check the email. Then widen.

Common mistakes

Using SKUs instead of variant IDs

The mutation takes variant global IDs, not SKUs. A SKU is a label you chose; the variant ID is Shopify’s primary key. Keep the SKU column so humans can read the sheet, but never send it.

Sending more than 250 variants for one product

productVariantsBulkUpdate caps at 250 variants per call. Almost no product has that many, but if one of yours is that configurable, chunk the variants array inside the Code node and emit two items for that product.

Setting a compare-at price below the new price

Shopify rejects it, and the product’s whole mutation fails rather than half-applying. The Code node guards against this by only including compareAtPrice when it exceeds the price. To clear an old sale price deliberately, send compareAtPrice: null, not an empty string.

Trusting the HTTP status code

A GraphQL mutation that fails validation still returns 200. If your log tab is full of ok but nothing changed in the store, you are reading the status code instead of userErrors.

Reaching for the old REST variants endpoint

Older tutorials show a PUT to /admin/api/xxxx-xx/variants/123.json. Shopify moved product and variant writes to GraphQL, and that REST path is gone on current API versions. Copy an old guide and you get a 404, then blame your credential.

Comma decimal separators

In a European locale, 19,99 arrives as a string that Number() turns into NaN. The Code node replaces the comma before parsing and skips the row if it still cannot read a positive number. Watch for a log tab shorter than expected.

Cost at realistic volume

Nothing in this workflow has a per-use price attached. Self-hosted n8n on a small VPS runs about 5 to 10 USD a month, and that instance is already paying for every other workflow you run on it. If you are on n8n Cloud, one repricing run across 90 products consumes a single execution, because everything after the trigger happens inside one execution.

The Shopify Admin API is free and included with your plan. Google Sheets and Gmail are free at any volume a store will produce. There is no AI model here, so there is no token cost either.

The honest comparison is against the bulk-editing apps in the Shopify App Store, at 10 to 30 USD a month for scheduled price changes. This workflow does the scheduled part too: swap the Manual Trigger for a Schedule Trigger and add a run_on date column the Filter node checks against today. Most stores keep it manual, because the whole point of the sheet is that a person looked at it.

Get the ready-to-import template

If you would rather not wire ten nodes by hand, we build and test these workflows for stores that want them running the same day. See what we do, or browse the wider n8n Shopify automation collection.

FAQ

Where do I get the variant IDs for my sheet?

Run a one-off n8n workflow that queries the GraphQL products connection, pulls the product ID, variant ID, SKU and current price, and writes them straight into the prices tab. That becomes your starting spreadsheet, and you overwrite the price column each season. It takes three nodes and about ten minutes.

Can I schedule this instead of clicking run?

Yes. Replace the Manual Trigger with a Schedule Trigger, and add a date column to the sheet that your Filter node compares against today’s date. Be careful, though. A scheduled price change with nobody watching is how a misplaced decimal point becomes a very good day for your customers.

What happens if the workflow fails halfway through?

The products already processed keep their new prices and the rest stay untouched. Your log tab shows exactly which product IDs completed. Set the apply column back to no for those rows and re-run the remainder. The workflow is safe to run twice anyway, because setting a price to a value it already holds is a no-op.

Will this work for WooCommerce?

Yes, with a smaller build. Replace the Code node’s grouping logic with a plain pass-through, since WooCommerce updates one variation at a time, and point the HTTP Request at the wc/v3 product variations endpoint with a regular_price field. The sheet, the filter, the loop, the log and the email stay identical.

Does this update inventory or only price?

Only price and compare-at price. Inventory lives on a different object in Shopify’s data model and moves through inventoryLevels, so folding the two into one mutation is not possible. Keep repricing and restocking as separate workflows, which is what you want operationally in any case.

Related guides

Repricing pairs naturally with the other catalog jobs. See the bulk product upload from CSV build for getting products into the store in the first place, the auto-hide out-of-stock products workflow for keeping the storefront honest, and the weekly best sellers report for deciding which prices to change next. More builds live in Shopify and e-commerce.