AutomationFlowsEmail & Gmail › Score Shopify Customer Churn Risk and Sync Results to Klaviyo, Slack, and Gmail

Score Shopify Customer Churn Risk and Sync Results to Klaviyo, Slack, and Gmail

ByTricore Infotech Pvt Ltd @jinitp on n8n.io

This workflow runs weekly to fetch customers from the Shopify Admin GraphQL API, calculate churn risk based on each customer’s median reorder interval, and then sync at-risk customers to Klaviyo while sending a CSV report and summary to Slack and/or Gmail. Runs weekly on a…

Cron / scheduled trigger★★★★★ complexity31 nodesSlackHTTP RequestGmailGraphQLError Trigger
Email & Gmail Trigger: Cron / scheduled Nodes: 31 Complexity: ★★★★★ Added:

This workflow corresponds to n8n.io template #16013 — we link there as the canonical source.

This workflow follows the Error Trigger → Gmail recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Calculate Shopify customer churn risk and sync to Klaviyo, Slack, and Gmail",
  "tags": [],
  "nodes": [
    {
      "id": "32393b4f-3c71-4038-b87c-6973915fc6af",
      "name": "Sticky \u2014 Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -7520,
        -448
      ],
      "parameters": {
        "color": 1,
        "width": 480,
        "height": 992,
        "content": "## Calculate Shopify Customer Churn Risk \u2192 Klaviyo + Slack + Gmail\n\nBuilt for e-commerce teams who want to catch at-risk customers before they churn \u2014 by analyzing each customer's own buying pattern, not a fixed date.\n\n### How it works\n\n1. Triggers weekly and loads your config (domain, thresholds, alert toggles).\n2. Pulls paginated customer data from Shopify Admin GraphQL API, 100 per page.\n3. Calculates median days between orders per customer and scores churn risk. Above 1.5x their normal interval = MEDIUM, above 2.0x = HIGH.\n4. Risky customers are stored in memory across all pages until the full scan is done.\n5. Results split into three outputs: Klaviyo profile update, CSV export, and summary alert.\n6. Slack and Gmail receive the risk summary with attached CSV. Klaviyo gets `churn_risk` and `days_since_last_order` written to each profile.\n\n### Setup\n\n1. In **Set Workflow Config**, enter your Shopify domain and toggle `emailAlert`, `slackAlert`, `klaviyoEvent` to `true` as needed.\n2. Add your Shopify Admin API token to the GraphQL node (Header Auth).\n3. Add your Klaviyo Private API key to the HTTP Request node (Header Auth).\n4. Connect Gmail (OAuth2) and Slack (API token).\n5. Test with pinned data on **Fetch Customer Data** before activating.\n\n### Customization\n\n- `customerActiveDays` \u2014 how far back to look for order history (default 180).\n- `minimumOrderRequired` \u2014 customers with fewer orders are skipped entirely (default 3).\n- `safeDays` \u2014 excludes very recent buyers from scoring (default 30)."
      },
      "typeVersion": 1
    },
    {
      "id": "e0be3f7f-da4b-4e35-a6c9-aa4812d0d8e4",
      "name": "Sticky \u2014 Trigger and Config",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -6960,
        -96
      ],
      "parameters": {
        "color": 7,
        "width": 592,
        "height": 320,
        "content": "## Trigger and Config\n\nRuns weekly. Set your Shopify domain, alert toggles, and tune the churn thresholds (Active Days, Min Orders, Safe Days) here. If no alerts are enabled, the workflow stops cleanly."
      },
      "typeVersion": 1
    },
    {
      "id": "0847b46c-c006-408f-9d9d-3879f9e0fa80",
      "name": "Sticky \u2014 Paginated Fetch",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -6336,
        -256
      ],
      "parameters": {
        "color": 7,
        "width": 1520,
        "height": 592,
        "content": "## Paginated Fetch and Churn Scoring\n\nPulls customers from Shopify 100 at a time. Each batch is scored for churn risk based on median purchase interval. Risky customers get saved to memory. Loops until all pages are done."
      },
      "typeVersion": 1
    },
    {
      "id": "73c9b47a-8fc1-4792-90c8-b7dcecee374c",
      "name": "Sticky \u2014 Retrieve Results",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4784,
        -272
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 400,
        "content": "## Retrieve Results\n\nReads all stored risky customers from memory and clears it for the next run. If nobody is at risk, the workflow ends cleanly here."
      },
      "typeVersion": 1
    },
    {
      "id": "87b19953-c3a2-46ae-8b20-5997ac2bd321",
      "name": "Sticky \u2014 Klaviyo Updates",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4352,
        -448
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 352,
        "content": "## Klaviyo Profile Updates\n\nIf Klaviyo is toggled on, writes `churn_risk` and `days_since_last_order` to each at-risk customer's profile via the Klaviyo API."
      },
      "typeVersion": 1
    },
    {
      "id": "85412c66-4896-4acb-ad2f-4c86a169a3ef",
      "name": "Sticky \u2014 Build Report",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -4352,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 400,
        "height": 528,
        "content": "## Build Churn Report\n\nAggregates risk totals and revenue at risk, converts the customer list to a dated CSV, then merges both ready for sending."
      },
      "typeVersion": 1
    },
    {
      "id": "46397ca7-4774-4292-83af-a34d6f499b6f",
      "name": "Sticky \u2014 Email Alert",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3920,
        -64
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 352,
        "content": "## Email Alert\n\nSends an HTML summary email with the CSV attached. Only fires if `emailAlert` is set to `true` in the config node."
      },
      "typeVersion": 1
    },
    {
      "id": "2f6de8bc-60e1-466c-b899-1e53a234d46d",
      "name": "Sticky \u2014 Slack Alert",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -3920,
        320
      ],
      "parameters": {
        "color": 7,
        "width": 416,
        "height": 352,
        "content": "## Slack Alert\n\nPosts a risk summary message to your chosen Slack channel with the CSV attached. Only fires if `slackAlert` is `true`."
      },
      "typeVersion": 1
    },
    {
      "id": "32d942cc-206d-4b22-ba74-5faa384d47c5",
      "name": "Calculate Churn Risk",
      "type": "n8n-nodes-base.code",
      "notes": "Adjust scoring thresholds (1.5 / 2.0) here to tune sensitivity.",
      "position": [
        -6000,
        32
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nconst results = [];\n\nconst graphqlData = items[0]?.json?.data?.customers;\nconst edges = graphqlData?.edges || [];\n\nconst cursor = graphqlData?.pageInfo?.hasNextPage && edges.length > 0 \n  ? edges[edges.length - 1]?.cursor \n  : null;\nconst hasNextPage = graphqlData?.pageInfo?.hasNextPage || false;\n\nfor (const edge of edges) {\n  const node = edge.node;\n  const ordersData = node.orders?.edges || [];\n  \n  if (ordersData.length === 0) continue; \n\n  const totalSpentStr = node.amountSpent?.amount || \"0\";\n  const totalSpent = parseFloat(totalSpentStr);\n  \n  const orderCount = Number(node.numberOfOrders) || 1;\n  const customerAOV = totalSpent / orderCount;\n\n  const dates = ordersData\n    .map(o => new Date(o.node.createdAt))\n    .sort((a, b) => b.getTime() - a.getTime());\n\n  let gaps = [];\n  for (let i = 1; i < dates.length; i++) {\n    gaps.push((dates[i - 1].getTime() - dates[i].getTime()) / (1000 * 60 * 60 * 24));\n  }\n  gaps.sort((a, b) => a - b);\n  \n  const median = gaps.length > 0 ? gaps[Math.floor(gaps.length / 2)] : 45;\n  const lastOrder = dates[0];\n  \n  const daysSinceLast = (Date.now() - lastOrder.getTime()) / (1000 * 60 * 60 * 24);\n  const score = median > 0 ? daysSinceLast / median : 0;\n  \n  let risk = 'LOW';\n  if (score >= 2.0) {\n    risk = 'HIGH';\n  } else if (score >= 1.5) {\n    risk = 'MEDIUM';\n  } else {\n    continue;\n  }\n\n  results.push({\n    \"Email\": node.email,\n    \"Display Name\": node.displayName,\n    \"Number Of Orders\": node.numberOfOrders,\n    \"Expected Order Value\": Math.round(customerAOV * 100) / 100,\n    \"Avg Order Days\": Math.round(median),\n    \"Days Since Last Order\": Math.round(daysSinceLast),\n    \"Risk Status\": risk,\n    \"Last Order Date\": lastOrder.toISOString().split('T')[0],\n    cursor: cursor,\n    hasNextPage: hasNextPage\n  });\n}\n\nif (results.length === 0) {\n  return [{ json: { cursor: cursor, hasNextPage: hasNextPage, isSignal: true } }];\n}\n\nreturn results;"
      },
      "typeVersion": 2,
      "alwaysOutputData": false
    },
    {
      "id": "c7a69855-3ed8-4d4e-8b35-fc10d9dd42d0",
      "name": "If Alerts Enabled",
      "type": "n8n-nodes-base.if",
      "position": [
        -6512,
        64
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "or",
          "conditions": [
            {
              "id": "cd779df6-eae0-4743-9873-4a6734bcb6b0",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.emailAlert }}",
              "rightValue": "={{ $json.emailAlert }}"
            },
            {
              "id": "4e24fc3e-0eae-4d41-bcd7-a894fb425486",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.slackAlert }}",
              "rightValue": ""
            },
            {
              "id": "b4ecbd66-79da-44d2-bee7-84aa8cef9c4f",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.klaviyoEvent }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "ffd4eab0-6e93-46aa-87a0-29175882db91",
      "name": "If Risky Customers Found",
      "type": "n8n-nodes-base.if",
      "position": [
        -4528,
        -32
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "1ffac022-5eb7-44c5-b446-376d0c8ab3e4",
              "operator": {
                "type": "object",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "69da49e0-e907-4130-963d-1f4bcc38ecd7",
      "name": "If Batch Has Risky Customers",
      "type": "n8n-nodes-base.if",
      "position": [
        -5760,
        32
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "0a58e4ed-2cab-42f8-a48e-2198361c3cbf",
              "operator": {
                "type": "object",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json }}",
              "rightValue": ""
            },
            {
              "id": "7e1260c3-1abc-4a78-a282-e3ad99dfabfb",
              "operator": {
                "type": "boolean",
                "operation": "notEquals"
              },
              "leftValue": "={{ $json.isSignal }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "4f722ae5-7dd1-4d9a-94bd-5592c98f7d4b",
      "name": "If Email Notifications On",
      "type": "n8n-nodes-base.if",
      "position": [
        -3872,
        128
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "507c908b-c0be-4381-be2d-9d1d2864e415",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $('Set Workflow Config').item.json.emailAlert }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "a50e4e08-0ae7-490b-adc7-60992e351dc8",
      "name": "If Slack Notifications On",
      "type": "n8n-nodes-base.if",
      "position": [
        -3872,
        512
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "eb471d81-4efd-4a4c-9397-e8d7e70274f6",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $('Set Workflow Config').item.json.slackAlert }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "4933c310-9c71-4499-bd77-ed2470fa1fca",
      "name": "Send CSV to Slack Channel",
      "type": "n8n-nodes-base.slack",
      "position": [
        -3648,
        496
      ],
      "parameters": {
        "options": {
          "channelId": "={{ $('Set Workflow Config').item.json.slackChannelId }}",
          "initialComment": "=\ud83d\udea8 *Weekly Churn Risk Report* \ud83d\udea8\n\n*{{$json.totalAtRisk}} total customers* are showing signs of churn. *{{$json.highRiskCount}}* of them are currently at HIGH risk.\n\n\ud83d\udcca *Summary Report*\n> *\ud83d\udd34 High Risk:* {{$json.highRiskCount}} (${{$json.highRiskRevenue}})\n> *\ud83d\udfe1 Medium Risk:* {{$json.mediumRiskCount}} (${{$json.mediumRiskRevenue}})\n> *\ud83d\udcb0 Total Revenue At Risk:* ${{$json.totalRevenueAtRisk}}\n\n_The full CSV breakdown of all at-risk customers is attached to this message._"
        },
        "resource": "file"
      },
      "typeVersion": 2.4
    },
    {
      "id": "2c3027d0-ea6a-425b-8ec3-4f4d045510c3",
      "name": "If Klaviyo Enabled",
      "type": "n8n-nodes-base.if",
      "position": [
        -4304,
        -256
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "be296d98-337f-4004-aac8-1a001e9285cc",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $('Set Workflow Config').item.json.klaviyoEvent }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "1b18b8b1-14df-4cc4-9fe6-a3b4f92c342e",
      "name": "Send to Klaviyo API",
      "type": "n8n-nodes-base.httpRequest",
      "notes": "Uses Header Auth. Add your Klaviyo Private API key as the value.",
      "position": [
        -4048,
        -272
      ],
      "parameters": {
        "url": "https://a.klaviyo.com/api/profile-import/",
        "method": "POST",
        "options": {},
        "jsonBody": "={\n  \"data\": {\n    \"type\": \"profile\",\n    \"attributes\": {\n      \"email\": \"{{$json.Email}}\",\n      \"properties\": {\n        \"churn_risk\": \"{{$json[\"Risk Status\"]}}\",\n        \"days_since_last_order\": {{ $json['Days Since Last Order'] }}\n      }\n    }\n  }\n}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "headerParameters": {
          "parameters": [
            {
              "name": "content-type",
              "value": "application/vnd.api+json"
            },
            {
              "name": "accept",
              "value": "application/vnd.api+json"
            },
            {
              "name": "revision",
              "value": "2026-04-15"
            }
          ]
        }
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.4
    },
    {
      "id": "6e3ae898-99ab-46a4-802b-ff4c29231a54",
      "name": "Send Email Alert",
      "type": "n8n-nodes-base.gmail",
      "position": [
        -3648,
        112
      ],
      "parameters": {
        "sendTo": "={{ $('Set Workflow Config').item.json.recipientMail }}",
        "message": "=<div style=\"font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; max-width: 600px; margin: 0 auto; border: 1px solid #e0e0e0; border-radius: 8px; overflow: hidden; background-color: #ffffff;\">\n  \n  <div style=\"background-color: #d9534f; color: #ffffff; padding: 20px 24px; text-align: left;\">\n    <h2 style=\"margin: 0; font-size: 20px; font-weight: 600; letter-spacing: 0.5px;\">\ud83d\udea8 Weekly Churn Risk Alert</h2>\n  </div>\n  \n  <div style=\"padding: 24px; color: #333333;\">\n    \n    <p style=\"font-size: 16px; line-height: 1.5; margin-top: 0; margin-bottom: 24px;\">\n      <strong>{{$json.totalAtRisk}} total customers</strong> are showing signs of churn. <strong>{{$json.highRiskCount}}</strong> of them are currently at <span style=\"color: #d9534f; font-weight: bold;\">HIGH</span> risk.\n    </p>\n\n    <div style=\"background-color: #f9f9f9; border: 1px solid #eeeeee; border-radius: 6px; padding: 16px 20px; margin-bottom: 24px;\">\n      <h3 style=\"margin-top: 0; margin-bottom: 16px; color: #666666; font-size: 13px; text-transform: uppercase; letter-spacing: 1px;\">Summary Report</h3>\n      \n      <table style=\"width: 100%; border-collapse: collapse; font-size: 15px;\">\n        <tr>\n          <td style=\"padding: 10px 0; border-bottom: 1px solid #eeeeee;\"><strong>\ud83d\udd34 High Risk Customers</strong></td>\n          <td style=\"padding: 10px 0; border-bottom: 1px solid #eeeeee; text-align: right;\"><strong>{{$json.highRiskCount}}</strong> <span style=\"color: #888; font-size: 13px;\">(${{$json.highRiskRevenue}})</span></td>\n        </tr>\n        <tr>\n          <td style=\"padding: 10px 0; border-bottom: 1px solid #eeeeee;\"><strong>\ud83d\udfe1 Medium Risk Customers</strong></td>\n          <td style=\"padding: 10px 0; border-bottom: 1px solid #eeeeee; text-align: right;\">{{$json.mediumRiskCount}} <span style=\"color: #888; font-size: 13px;\">(${{$json.mediumRiskRevenue}})</span></td>\n        </tr>\n        <tr>\n          <td style=\"padding: 10px 0;\"><strong>\ud83d\udcb0 Total Revenue At Risk</strong></td>\n          <td style=\"padding: 10px 0; text-align: right; color: #d9534f; font-weight: bold;\">${{$json.totalRevenueAtRisk}}</td>\n        </tr>\n      </table>\n    </div>\n\n    <p style=\"font-size: 14px; color: #555555; line-height: 1.5; margin-bottom: 12px;\">\n      The full CSV breakdown of all at-risk customers is attached to this email. \n    </p>\n    \n  </div>\n</div>",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {
                "property": "data"
              }
            ]
          }
        },
        "subject": "Weekly Churn Risk Report",
        "emailType": "HTML"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "122c5573-0fb4-4374-8c2f-f197018166c8",
      "name": "Combine Export Data",
      "type": "n8n-nodes-base.merge",
      "position": [
        -4096,
        224
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition"
      },
      "typeVersion": 3
    },
    {
      "id": "a5818388-8f50-43ba-b9e9-adf9a6bd29ce",
      "name": "Aggregate Churn Results",
      "type": "n8n-nodes-base.code",
      "position": [
        -4304,
        304
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\nlet highRiskCount = 0;\nlet mediumRiskCount = 0;\nlet highRiskRevenue = 0;\nlet mediumRiskRevenue = 0;\n\nfor (const item of items) {\n  const risk = item.json['Risk Status'];\n  const expectedValue = parseFloat(item.json['Expected Order Value'] || 0);\n\n  if (risk === 'HIGH') {\n    highRiskCount++;\n    highRiskRevenue += expectedValue;\n  }\n  if (risk === 'MEDIUM') {\n    mediumRiskCount++;\n    mediumRiskRevenue += expectedValue;\n  }\n}\n\nreturn [{\n  json: {\n    totalAtRisk: highRiskCount + mediumRiskCount,\n    highRiskCount,\n    mediumRiskCount,\n    highRiskRevenue: Math.round(highRiskRevenue * 100) / 100,\n    mediumRiskRevenue: Math.round(mediumRiskRevenue * 100) / 100,\n    totalRevenueAtRisk: Math.round((highRiskRevenue + mediumRiskRevenue) * 100) / 100\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "b085fe4e-97b1-4d23-9028-862ffc0cb0a2",
      "name": "Create CSV Export",
      "type": "n8n-nodes-base.convertToFile",
      "position": [
        -4304,
        144
      ],
      "parameters": {
        "options": {
          "fileName": "=Churn-Data-{{ $now.format('yyyy-MM-dd') }}.csv",
          "headerRow": true
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "626ef524-bd8c-4a09-8480-3e65fbee6ecf",
      "name": "Check for Next Page",
      "type": "n8n-nodes-base.if",
      "position": [
        -5264,
        48
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 3,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "f7d88041-c590-4c91-a11c-5883f1e1f7e4",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              },
              "leftValue": "={{ $json.hasNextPage }}",
              "rightValue": true
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "c2a22dac-6d7e-4017-9fd0-5bd044e8307c",
      "name": "Save Batch to Memory",
      "type": "n8n-nodes-base.code",
      "position": [
        -5520,
        -128
      ],
      "parameters": {
        "jsCode": "const memory = $getWorkflowStaticData('global');\n\nmemory.allCustomers = memory.allCustomers || [];\n\nconst currentBatch = $input.all();\nmemory.allCustomers.push(...currentBatch);\n\nreturn currentBatch;"
      },
      "typeVersion": 2
    },
    {
      "id": "b17520ae-a2cb-49d3-baf6-fa86076ce49f",
      "name": "Set Workflow Config",
      "type": "n8n-nodes-base.set",
      "notes": "Start here. Fill in your Shopify domain, toggle alerts on/off, and set thresholds.",
      "position": [
        -6720,
        64
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "0a645f74-6d27-4252-bd3e-36ba58c0e704",
              "name": "shopifyDomain",
              "type": "string",
              "value": "PASTE_YOUR_SHOPIFY_DOMAIN_HERE"
            },
            {
              "id": "f5e921cb-0fa8-4c3d-afb4-291c8e223a08",
              "name": "emailAlert",
              "type": "boolean",
              "value": false
            },
            {
              "id": "f894f915-2724-4bbc-97dd-edd93270c7ef",
              "name": "recipientMail",
              "type": "string",
              "value": "PASTE_YOUR_RECIPIENT_MAIL_HERE"
            },
            {
              "id": "783d3f65-e4e7-4bcd-9087-a8abae9fd68e",
              "name": "slackAlert",
              "type": "boolean",
              "value": false
            },
            {
              "id": "ac230f6e-4f6f-43a5-a0a8-64b541b401d9",
              "name": "slackChannelId",
              "type": "string",
              "value": "PASTE_YOUR_SLACK_CHANNEL_ID_HERE"
            },
            {
              "id": "45306585-3031-4d1a-bc71-32a5d4feb774",
              "name": "klaviyoEvent",
              "type": "boolean",
              "value": false
            },
            {
              "id": "47484bae-8072-4541-b460-3b6d53b6f479",
              "name": "minimumOrderRequired",
              "type": "number",
              "value": "3"
            },
            {
              "id": "02d619e2-0eea-403d-b9ed-4b1bd5af082e",
              "name": "customerActiveDays",
              "type": "number",
              "value": "180"
            },
            {
              "id": "bd0530ff-c94a-4db3-982e-9dfd96207f6e",
              "name": "safeDays",
              "type": "number",
              "value": "30"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "cca09180-e06d-45ae-8eb2-55636680402f",
      "name": "Weekly Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -6912,
        64
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtHour": 7
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "82dd3f28-6c6c-4d18-9ece-ffaf5ffc2bdf",
      "name": "Fetch Customer Data",
      "type": "n8n-nodes-base.graphql",
      "position": [
        -6288,
        48
      ],
      "parameters": {
        "query": "query CustomersByLastOrderDate($cursor: String, $filter: String!) {\n  customers(first: 100, after: $cursor, query: $filter) {\n    edges {\n      cursor\n      node {\n        id\n        email\n        displayName\n        numberOfOrders\n        amountSpent {\n          amount\n        }\n        orders(first: 5, reverse: true,  query: \"financial_status:paid AND fulfillment_status:fulfilled\") {\n          edges {\n            node {\n              createdAt\n            }\n          }\n        }\n      }\n    }\n    pageInfo {\n      hasNextPage\n    }\n  }\n}",
        "endpoint": "=https://{{ $('Set Workflow Config').first().json.shopifyDomain }}/admin/api/2026-04/graphql.json",
        "variables": "={{\n{\n  \"cursor\": $json.cursor || null,\n  \"filter\": `orders_count:>=${$('Set Workflow Config').first().json.minimumOrderRequired} AND order_date:>=${$now.minus({days: Number($('Set Workflow Config').first().json.customerActiveDays)}).toFormat('yyyy-MM-dd')} AND order_date:<=${$now.minus({days: Number($('Set Workflow Config').first().json.safeDays)}).toFormat('yyyy-MM-dd')}`\n}\n}}",
        "operationName": "CustomersByLastOrderDate",
        "authentication": "headerAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1.1,
      "alwaysOutputData": false
    },
    {
      "id": "31b91178-2d84-4f31-8410-437b8c8dd360",
      "name": "Retrieve Customer Data",
      "type": "n8n-nodes-base.code",
      "notes": "Reads all stored customers from memory and resets it for the next run.",
      "position": [
        -4736,
        -32
      ],
      "parameters": {
        "jsCode": "const memory = $getWorkflowStaticData('global');\n\nconst finalData = memory.allCustomers || [];\n\nmemory.allCustomers = [];\n\nreturn finalData.map(item => {\n  const cleanedJson = { ...item.json };\n  delete cleanedJson.cursor;\n  delete cleanedJson.hasNextPage;\n  return { json: cleanedJson };\n});"
      },
      "typeVersion": 2,
      "alwaysOutputData": true
    },
    {
      "id": "72f4beee-d02a-490a-95dd-134139db2c50",
      "name": "On Global Error",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        -6784,
        432
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "967b3046-6c60-486b-90dc-75b55d0d27d3",
      "name": "Post Error to Slack",
      "type": "n8n-nodes-base.slack",
      "position": [
        -6528,
        432
      ],
      "parameters": {
        "text": "=\ud83d\udea8 *Workflow Error \u2014 {{ $workflow.name }}*\\n\\n*Failed Node:* {{ $json.execution.error.node.name }}\\n*Error:* {{ $json.execution.error.message }}",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "mode": "id",
          "value": "PASTE_YOUR_SLACK_CHANNEL_ID_HERE"
        },
        "otherOptions": {
          "mrkdwn": true
        }
      },
      "credentials": {
        "slackApi": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2
    },
    {
      "id": "f2a1f9b7-10b7-4231-9779-3b54f1234f89",
      "name": "Sticky \u2014 Error Handler",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -6832,
        272
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 320,
        "content": "## Global Error Handler\n\nCatches any execution failure and posts the failed node name and error message directly to your Slack channel."
      },
      "typeVersion": 1
    },
    {
      "id": "94a9df28-4833-4ec0-be78-98f5fee5baa1",
      "name": "Limit to 1 Item",
      "type": "n8n-nodes-base.limit",
      "position": [
        -4960,
        160
      ],
      "parameters": {
        "maxItems": 1
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "connections": {
    "Limit to 1 Item": {
      "main": [
        [
          {
            "node": "Fetch Customer Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "On Global Error": {
      "main": [
        [
          {
            "node": "Post Error to Slack",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create CSV Export": {
      "main": [
        [
          {
            "node": "Combine Export Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Alerts Enabled": {
      "main": [
        [
          {
            "node": "Fetch Customer Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Klaviyo Enabled": {
      "main": [
        [
          {
            "node": "Send to Klaviyo API",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Check for Next Page": {
      "main": [
        [
          {
            "node": "Limit to 1 Item",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Retrieve Customer Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Export Data": {
      "main": [
        [
          {
            "node": "If Email Notifications On",
            "type": "main",
            "index": 0
          },
          {
            "node": "If Slack Notifications On",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Customer Data": {
      "main": [
        [
          {
            "node": "Calculate Churn Risk",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Workflow Config": {
      "main": [
        [
          {
            "node": "If Alerts Enabled",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Churn Risk": {
      "main": [
        [
          {
            "node": "If Batch Has Risky Customers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Batch to Memory": {
      "main": [
        [
          {
            "node": "Check for Next Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retrieve Customer Data": {
      "main": [
        [
          {
            "node": "If Risky Customers Found",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Aggregate Churn Results": {
      "main": [
        [
          {
            "node": "Combine Export Data",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Weekly Schedule Trigger": {
      "main": [
        [
          {
            "node": "Set Workflow Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Risky Customers Found": {
      "main": [
        [
          {
            "node": "If Klaviyo Enabled",
            "type": "main",
            "index": 0
          },
          {
            "node": "Aggregate Churn Results",
            "type": "main",
            "index": 0
          },
          {
            "node": "Create CSV Export",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Email Notifications On": {
      "main": [
        [
          {
            "node": "Send Email Alert",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Slack Notifications On": {
      "main": [
        [
          {
            "node": "Send CSV to Slack Channel",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "If Batch Has Risky Customers": {
      "main": [
        [
          {
            "node": "Save Batch to Memory",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Check for Next Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}

Credentials you'll need

Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.

Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

This workflow runs weekly to fetch customers from the Shopify Admin GraphQL API, calculate churn risk based on each customer’s median reorder interval, and then sync at-risk customers to Klaviyo while sending a CSV report and summary to Slack and/or Gmail. Runs weekly on a…

Source: https://n8n.io/workflows/16013/ — original creator credit. Request a take-down →

More Email & Gmail workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Email & Gmail

14310 Send Overdue Invoice Payment Reminders With Ifirma Gmail Postgrid And Slack. Uses httpRequest, stopAndError, slack, gmail. Scheduled trigger; 53 nodes.

HTTP Request, Stop And Error, Slack +1
Email & Gmail

Instead of providing a routine check, it focuses on significant movements by: Sending a Slack alert only if a query crosses a defined movement threshold. Emailing a structured report with the Top 25 i

HTTP Request, Slack, Gmail
Email & Gmail

Receive inventory movements via webhook, validate data, update stock levels, and trigger automatic alerts when products need reordering.

HTTP Request, Slack, Gmail +1
Email & Gmail

This workflow automates the complete end-to-end processing of daily revenue transactions for finance and accounting teams. It systematically retrieves, validates, and standardizes transaction data fro

HTTP Request, Gmail, Google Drive +2
Email & Gmail

Muzik Platformu Uretim Otomasyonu v4. Uses httpRequest, gmail, errorTrigger. Scheduled trigger; 30 nodes.

HTTP Request, Gmail, Error Trigger