HomeShopify & E-commerceHow to build a Shopify sales…

How to build a Shopify sales channel report with n8n

How to build a Shopify sales channel report with n8n









A Shopify sales channel report built in n8n tells you exactly where last week’s revenue came from, whether that is your online store, Shopify POS, draft-order invoices, Instagram, or Google, without opening a single analytics dashboard. This guide builds a workflow that pulls the past seven days of orders every Monday morning, groups them by order source, writes one row per channel to Google Sheets, and emails you a ranked summary. The whole build takes about 30 minutes.

What it does

Every Shopify order carries a source_name field that records the sales channel it came from: web for the online store, pos for in-person sales, shopify_draft_order for invoices you send by hand, and channel handles like instagram or google for connected surfaces. Individually those values are buried inside each order. Rolled up, they answer the question every store owner actually asks on Monday: which channel made me money last week?

This workflow runs on a weekly schedule and does four things in one pass:

  1. Fetches all Shopify orders from the last seven days, including POS and draft orders.
  2. Groups them by source_name, counting orders and summing revenue per channel.
  3. Appends one row per channel to a Google Sheet so you build a week-over-week history.
  4. Emails you a single ranked table, sorted by revenue, with each channel’s share of the total.

You get a clean read on channel mix, a permanent Sheet you can chart later, and zero manual exporting. It connects Shopify, n8n, Google Sheets, and Gmail. If you have not linked your store to n8n yet, start with our guide on connecting Shopify to n8n in 2026, then come back here.

Why it beats the default

Shopify’s built-in Analytics does have a “Sales by channel” report, but it lives behind a login, resets to a default date range, and cannot be pushed anywhere. You cannot email it to a co-founder on a schedule, you cannot append it to a spreadsheet you already use for forecasting, and on the Basic plan several breakdowns are locked entirely.

Doing this in n8n flips all of that:

  • The report comes to you. It lands in your inbox every Monday at 7am, ranked and ready, before you have opened a browser tab.
  • You keep the history. Each run appends rows to Google Sheets, so after a month you can chart whether Instagram is trending up or your draft-order revenue is drying out. Shopify’s dashboard only shows the window you pick.
  • It is yours to extend. Want to split by channel and month, or flag any week where POS drops below a threshold? It is a Code node and an IF node away, not a plan upgrade.

What you need

  • An n8n instance (Cloud or self-hosted). Both work; nothing here is version-specific beyond n8n 1.0+.
  • A Shopify custom app access token with read_orders scope, created through the Shopify Dev Dashboard method covered in the connection guide. This workflow reads orders through the Admin API (version 2026-04) and never writes to your store.
  • A Google account for Google Sheets and Gmail, connected to n8n by OAuth2.
  • One blank Google Sheet with a header row (columns listed in the build below).

Estimated build time: about 30 minutes from scratch, or under 10 minutes if you import the ready-made template at the end of this guide.

Node-by-node list

Six nodes, wired in a single straight line. No branches, which keeps it easy to debug.

[Every Monday 07:00]  (Schedule Trigger)
        |
        v
[Get last 7 days of orders]  (Shopify - Get Many)
        |
        v
[Group orders by channel]  (Code)
        |
        v
[Append rows to Sheet]  (Google Sheets - Append)
        |
        v
[Keep one for email]  (Limit - maxItems 1)
        |
        v
[Email the report]  (Gmail - Send)
  
# Node Type Job
1 Every Monday 07:00 Schedule Trigger Fires once a week
2 Get last 7 days of orders Shopify Pulls all recent orders
3 Group orders by channel Code Aggregates by source_name
4 Append rows to Sheet Google Sheets Logs one row per channel
5 Keep one for email Limit Reduces to a single item
6 Email the report Gmail Sends the ranked table

Step-by-step build

1. Add the Schedule Trigger

Create a new workflow and add a Schedule Trigger. Set the interval to Weeks, every 1 week, trigger day Monday, at hour 7. That gives you a fresh report waiting each Monday morning. Pick whatever hour suits your timezone; the n8n instance timezone controls when 7 actually fires.

2. Fetch the last 7 days of orders

Add a Shopify node. Set Resource to Order and Operation to Get Many, then turn on Return All so pagination is handled for you. Under Options, add:

  • Created At Min: ={{ $now.minus(7, 'days').toISO() }}
  • Status: any (so cancelled and closed orders are included, matching Shopify’s own revenue view)
  • Fields: id,total_price,source_name,created_at,financial_status (smaller payload, faster run)

Attach your Shopify credential. Each returned item looks roughly like this:

{
  "id": 5218841116,
  "total_price": "129.00",
  "source_name": "web",
  "created_at": "2026-08-25T14:31:07-05:00",
  "financial_status": "paid"
}

3. Group the orders by channel

Add a Code node (Run Once for All Items) and paste the logic below. It reads every incoming order, buckets it by source_name, sums total_price, and returns one item per channel, sorted by revenue. It also builds the HTML email table once and carries it on each item.

const orders = $input.all().map(i => i.json);
const weekOf = $now.minus(7, 'days').toFormat('yyyy-LL-dd');
const generatedAt = $now.toISO();

const map = {};
for (const o of orders) {
  const channel = (o.source_name && String(o.source_name).trim()) || 'unknown';
  const amount = parseFloat(o.total_price || '0') || 0;
  if (!map[channel]) map[channel] = { channel, order_count: 0, revenue: 0 };
  map[channel].order_count += 1;
  map[channel].revenue += amount;
}

const channels = Object.values(map).sort((a, b) => b.revenue - a.revenue);
for (const c of channels) c.revenue = Math.round(c.revenue * 100) / 100;

const totalOrders = channels.reduce((s, c) => s + c.order_count, 0);
const totalRevenue = Math.round(channels.reduce((s, c) => s + c.revenue, 0) * 100) / 100;

const rows = channels.map(c => {
  const share = totalRevenue > 0 ? Math.round((c.revenue / totalRevenue) * 1000) / 10 : 0;
  return `<tr><td>${c.channel}</td><td>${c.order_count}</td><td>${c.revenue.toFixed(2)}</td><td>${share}%</td></tr>`;
}).join('');

const emailHtml = `<h2>Sales channel report — week of ${weekOf}</h2>
<p>${totalOrders} orders and ${totalRevenue.toFixed(2)} in revenue across ${channels.length} channels.</p>
<table border="1" cellpadding="6"><tr><th>Channel</th><th>Orders</th><th>Revenue</th><th>Share</th></tr>${rows}</table>`;

return channels.map(c => ({
  json: {
    week_of: weekOf, channel: c.channel, order_count: c.order_count,
    revenue: c.revenue, total_orders: totalOrders, total_revenue: totalRevenue,
    generated_at: generatedAt, email_html: emailHtml
  }
}));

After this node, the data is one item per channel. For a typical week it might look like:

[
  { "channel": "web", "order_count": 84, "revenue": 9312.50, "share_via_email": "68%" },
  { "channel": "pos", "order_count": 22, "revenue": 2140.00 },
  { "channel": "instagram", "order_count": 9, "revenue": 733.00 },
  { "channel": "shopify_draft_order", "order_count": 3, "revenue": 1180.00 }
]
💡

Tip: Orders with no channel (rare, but it happens with some app-created orders) fall into an unknown bucket instead of vanishing, so your revenue total always reconciles.

4. Append each channel to Google Sheets

Add a Google Sheets node, Operation Append. Point it at your spreadsheet and sheet, then map the columns to the fields the Code node produced: week_of, channel, order_count, revenue, generated_at. Because the Code node emits one item per channel, this node appends one row per channel automatically.

📌

Your sheet’s header row must match the mapped field names exactly, or n8n will create new columns. Set up the header once: week_of | channel | order_count | revenue | generated_at.

5. Collapse to a single item

Add a Limit node with Max Items set to 1. The Sheet append fanned across every channel; the email should be sent once. Limit keeps only the first item, which still carries the full email_html table built in step 3.

6. Email the report

Add a Gmail node, Operation Send. Set:

  • To: your own address (or a shared inbox)
  • Subject: ={{ 'Sales channel report — week of ' + $json.week_of }}
  • Message: ={{ $json.email_html }}

Save the workflow and toggle it Active. Click Execute Workflow once to confirm the email arrives and the Sheet fills. From then on it runs itself every Monday.

Common mistakes

  • Emailing before collapsing. If you wire Gmail straight after Google Sheets, it fires once per channel and you get four emails. The Limit node is what makes it a single send. Do not skip it.
  • Status left on default. If you leave Status unset, the Shopify node returns only open orders and your totals will not match Shopify Analytics. Set it to any.
  • Mismatched Sheet headers. Field names in the Google Sheets mapping must match your header row character for character, or you end up with duplicate columns and half-empty rows.
  • Building the date window by hand. Use $now.minus(7, 'days') rather than a hardcoded date, or the report silently keeps reporting the same week forever.
  • Summing a string. Shopify sends total_price as a string like "129.00". The Code node runs it through parseFloat; if you aggregate it elsewhere without parsing, you will concatenate text instead of adding numbers.

Cost at realistic volume

This workflow is effectively free to run. It fires once a week, so it uses roughly four executions a month.

Component Cost Notes
n8n (self-hosted) $0 Runs on your own server; 4 executions/month is nothing
n8n Cloud (Starter) ~$20/mo Only if you do not self-host; covers thousands of executions
Shopify Admin API $0 Reading orders is free and within rate limits
Google Sheets + Gmail $0 Standard free Google account

Even a store doing 4,000 orders a week stays cheap: Return All paginates at 250 orders per request, so that is about 16 API calls in a single Monday run, then one in-memory grouping pass. There is no per-order cost and nothing scales with your revenue.

🚀 Download the sales channel report template

The full guide above is free to follow. If you would rather skip the build, the ready-to-import template is the exact workflow from this post: import the JSON, attach your Shopify, Google Sheets, and Gmail credentials, and you are reporting in under 10 minutes. Want it done for you end to end? See our done-for-you setup service.

Download the template ($14) →

Instant download · Works on n8n Cloud and self-hosted

Frequently asked questions

What does source_name mean on a Shopify order?

The source_name field records where an order was placed: web for the online store, pos for in-person Shopify POS, shopify_draft_order for invoices you send manually, and channel handles like instagram or google for connected sales channels. Grouping by it shows which channel actually drove revenue.

Can I run the report daily instead of weekly?

Yes. Change the Schedule Trigger to run every day and adjust the lookback in the Shopify filter and Code node from 7 days to 1 day. A daily report is noisier for small stores, so most owners keep the weekly cadence and read a clean seven-day channel mix each Monday morning.

Does the report include Shopify POS and draft orders?

It includes every order the Shopify node returns with status set to any, so POS sales appear under pos and manually sent invoices appear under shopify_draft_order. Each distinct source_name becomes its own row, so you see the full channel mix rather than only online-store sales.

Will the workflow slow down with thousands of orders?

No. The Shopify node paginates at 250 orders per request, so even 4,000 weekly orders is about 16 API calls in one run. The grouping happens in a single Code node in memory, and the workflow fires only once a week, so it stays well inside n8n and Shopify rate limits.

Can I send the channel report to Slack or Telegram instead of email?

Yes. Replace the Gmail node with a Slack or Telegram node and pass the same email_html field, or a plain-text version, as the message. The Google Sheets log stays the same, so you keep a permanent record while the alert goes wherever your team already reads notifications.

Related guides