AutomationFlowsE-commerce › Send Payment Reminders and Cancel Unpaid Shopify Orders via Gmail

Send Payment Reminders and Cancel Unpaid Shopify Orders via Gmail

ByTricore Infotech Pvt Ltd @jinitp on n8n.io

This workflow runs daily to find open, unpaid Shopify orders, send staged payment reminder emails via Gmail, and automatically cancel long-unpaid orders while restocking inventory. It also emails an admin a daily CSV audit report and sends error alerts if the workflow fails.…

Event trigger★★★★★ complexity30 nodesError TriggerGmailGraphQL
E-commerce Trigger: Event Nodes: 30 Complexity: ★★★★★ Added:
Send Payment Reminders and Cancel Unpaid Shopify Orders via Gmail — n8n workflow card showing Error Trigger, Gmail, GraphQL integration

This workflow corresponds to n8n.io template #16084 — 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
{
  "name": "Send payment reminders and cancel unpaid orders in Shopify",
  "tags": [
    "shopify",
    "ecommerce",
    "email",
    "orders"
  ],
  "nodes": [
    {
      "id": "86ab8a60-54a3-4bc6-9ba7-8e4e10a28f5d",
      "name": "Template Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1984,
        144
      ],
      "parameters": {
        "color": 1,
        "width": 560,
        "height": 776,
        "content": "## Send Payment Reminders and Cancel Unpaid Orders in Shopify\n\n### How it works\n1. **Daily Fetch**: Triggers every morning at 8 AM to query Shopify via GraphQL for open, unpaid orders within your lookback window (default: 30 days).\n2. **Age & Tag Audit**: A JavaScript node evaluates the exact number of days since order creation and inspects existing customer timeline tags.\n3. **Escalation Routing**: Routes orders down specific tracks depending on their age to either send tiered reminders at configurable intervals (default: Days 3, 7, and 10) or execute an automated cancellation.\n4. **Inventory Protection**: Orders remaining unpaid after 14 days are automatically cancelled via Shopify mutation to restock held inventory, and a cancellation email is sent to the customer.\n5. **Admin Reporting**: Compiles all processed actions into an audit-ready CSV log and emails a daily performance breakdown to the store manager.\n\n### Setup\n* **Store Domain**: In the `Set Workflow Config` node, replace the placeholder with your store's primary `.myshopify.com` handle.\n* **Shopify Auth**: Configure HTTP Header Authentication named exactly `Shopify Token` (requires read/write access for orders and tags).\n* **Gmail Auth**: Link your sending inbox using OAuth2 credentials named `Gmail OAuth2`.\n* **Admin Contact**: Provide your admin email address in the `adminEmail` field of `Set Workflow Config`. This address receives both the daily report and any workflow error alerts.\n\n### Customization tips\n- **Lookback window**: Adjust `lookbackDays` inside the config node to change how far back in time the daily query checks for unresolved pending orders.\n- **Reminder & cancellation timing**: Update the numeric values for `reminder1Days`, `reminder2Days`, `reminder3Days`, or `cancelDays` to change your store's payment grace periods and follow-up cadence.\n- **Order processing limit**: Modify `fetchOrdersLimit` to alter the maximum total number of pending checkouts pulled and evaluated during a single daily run (default: 250)"
      },
      "typeVersion": 1
    },
    {
      "id": "d19456f4-83e6-438e-9267-a79d8c3746fa",
      "name": "Section: Trigger",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1376,
        512
      ],
      "parameters": {
        "color": 7,
        "width": 464,
        "height": 280,
        "content": "## Trigger & Workspace Configuration\nHandles execution timing schedules and houses global variables for your store domain, target admin emails, and custom aging thresholds."
      },
      "typeVersion": 1
    },
    {
      "id": "bba6fb9a-6c10-43ac-ba5b-cd8f8072341b",
      "name": "Section: GraphQL Ingestion",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -864,
        512
      ],
      "parameters": {
        "color": 7,
        "width": 704,
        "height": 280,
        "content": "## Shopify GraphQL Ingestion\nQueries the Shopify store endpoint to isolate open, pending orders and filters out checkouts missing a valid customer email profile."
      },
      "typeVersion": 1
    },
    {
      "id": "333c0af5-e647-4384-976c-0604fc67e1e3",
      "name": "Section: Matrix Calculation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -128,
        512
      ],
      "parameters": {
        "color": 7,
        "width": 502,
        "height": 312,
        "content": "## Matrix Calculation\nCalculates how many days each order has been unpaid and checks existing reminder tags to determine the next action for each order."
      },
      "typeVersion": 1
    },
    {
      "id": "4d8225c0-3133-445e-8a3d-1089c91a3462",
      "name": "Section: Email Preparation & Routing",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        380,
        512
      ],
      "parameters": {
        "color": 7,
        "width": 432,
        "height": 304,
        "content": "## Email Preparation & Routing\nBuilds the HTML email content for each order based on its assigned action, then routes it to the correct downstream path \u2014 reminder, cancellation, or no action required."
      },
      "typeVersion": 1
    },
    {
      "id": "dad687ba-3925-4caa-89dc-496e9efa275c",
      "name": "Section: Admin Report",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        304,
        -32
      ],
      "parameters": {
        "color": 7,
        "width": 992,
        "height": 456,
        "content": "## Summary Logging & Performance Reports\nAggregates metrics across all evaluated order items, flattens data fields into a formatted CSV sheet, and mails the log file to management."
      },
      "typeVersion": 1
    },
    {
      "id": "1de0c744-7bfc-4021-a649-fac6b84d6da6",
      "name": "Section: Customer Outbound",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        880,
        448
      ],
      "parameters": {
        "color": 7,
        "width": 1112,
        "height": 504,
        "content": "## Customer Outbound & Inventory Release\nSends the appropriate reminder email, tags the order, or cancels it and restocks inventory."
      },
      "typeVersion": 1
    },
    {
      "id": "error-sticky",
      "name": "Section: Error Handling",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -340,
        80
      ],
      "parameters": {
        "color": 7,
        "width": 460,
        "height": 338,
        "content": "## Workflow Failure Alerts\n\nCatches any workflow failure and emails your admin with the failed node name and error details. The recipient is automatically pulled from the `adminEmail` field in `Set Workflow Config`."
      },
      "typeVersion": 1
    },
    {
      "id": "error-trigger",
      "name": "On Global Error",
      "type": "n8n-nodes-base.errorTrigger",
      "position": [
        -270,
        250
      ],
      "parameters": {},
      "typeVersion": 1
    },
    {
      "id": "error-email",
      "name": "Send Error Notification",
      "type": "n8n-nodes-base.gmail",
      "maxTries": 3,
      "position": [
        -80,
        250
      ],
      "parameters": {
        "sendTo": "={{ $('Set Workflow Config').first().json.adminEmail }}",
        "message": "=\ud83d\udea8 *Workflow Error Report*\n\nWorkflow: {{ $workflow.name }}\nFailed Node: {{ $json.execution.error.node.name }}\nError Message: {{ $json.execution.error.message }}\nTimestamp: {{ $now.format('YYYY-MM-DD HH:mm:ss') }}\n\nCheck the n8n execution log for more details.",
        "options": {},
        "subject": "\u26a0\ufe0f n8n Workflow Failed: {{ $workflow.name }}",
        "emailType": "text"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 2.2,
      "waitBetweenTries": 1000
    },
    {
      "id": "d71b1588-51be-421f-8e39-ae8455e2fc47",
      "name": "Set Workflow Config",
      "type": "n8n-nodes-base.set",
      "notes": "\u26a0\ufe0f Edit these values before activating:\n- shopifyDomain: Your actual Shopify store URL (e.g., mycoolstore.myshopify.com)\n- adminEmail: Where the daily report should be sent\n- reminder1Days/2Days/3Days: When to send each reminder (in days after order creation)\n- cancelDays: After how many days an unpaid order gets auto-cancelled",
      "position": [
        -1120,
        624
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "domain",
              "name": "shopifyDomain",
              "type": "string",
              "value": "your-store.myshopify.com"
            },
            {
              "id": "admin-email",
              "name": "adminEmail",
              "type": "string",
              "value": "user@example.com"
            },
            {
              "id": "reminder1",
              "name": "reminder1Days",
              "type": "number",
              "value": 3
            },
            {
              "id": "reminder2",
              "name": "reminder2Days",
              "type": "number",
              "value": 7
            },
            {
              "id": "reminder3",
              "name": "reminder3Days",
              "type": "number",
              "value": 10
            },
            {
              "id": "cancel",
              "name": "cancelDays",
              "type": "number",
              "value": 14
            },
            {
              "id": "lookback",
              "name": "lookbackDays",
              "type": "number",
              "value": 30
            },
            {
              "id": "fetchOrdersLimit",
              "name": "fetchOrdersLimit",
              "type": "number",
              "value": 250
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "1303975c-4b61-42bd-a3f0-8205a83f82cb",
      "name": "Daily Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        -1312,
        624
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "days",
              "daysInterval": 1,
              "triggerAtHour": 8
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "dbfca246-1338-4cf1-8c07-1cf23315e429",
      "name": "Fetch Unpaid Shopify Orders",
      "type": "n8n-nodes-base.graphql",
      "maxTries": 3,
      "position": [
        -784,
        624
      ],
      "parameters": {
        "query": "query UnpaidOrders($limit: Int, $filter: String!) {\n  orders(first: $limit, query: $filter) {\n    edges {\n      node {\n        id\n        name\n        createdAt\n        tags\n        statusPageUrl(audience: CUSTOMERVIEW)\n        customer {\n          email\n          firstName\n        }\n        totalPriceSet {\n          shopMoney {\n            amount\n            currencyCode\n          }\n        }\n      }\n    }\n  }\n}",
        "endpoint": "=https://{{ $('Set Workflow Config').first().json.shopifyDomain }}/admin/api/2026-04/graphql.json",
        "variables": "={{\n{\n  \"limit\": Number($('Set Workflow Config').first().json.fetchOrdersLimit || 50),\n  \"filter\": `financial_status:pending AND status:open AND created_at:>=${$now.minus({days: Number($('Set Workflow Config').first().json.lookbackDays)}).toFormat('yyyy-MM-dd')} `\n}\n}}",
        "operationName": "UnpaidOrders",
        "authentication": "headerAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1.1,
      "waitBetweenTries": 1000
    },
    {
      "id": "a5683b0c-e8d8-4e93-a404-700587e19944",
      "name": "Split Orders from GraphQL Response",
      "type": "n8n-nodes-base.splitOut",
      "position": [
        -592,
        624
      ],
      "parameters": {
        "options": {},
        "fieldToSplitOut": "data.orders.edges"
      },
      "typeVersion": 1
    },
    {
      "id": "e8bd1131-7536-4578-bd47-8fe2d7a6cb7b",
      "name": "Filter Orders with Valid Email",
      "type": "n8n-nodes-base.filter",
      "position": [
        -368,
        624
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "has-email",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              },
              "leftValue": "={{ $json.node.customer.email }}",
              "rightValue": ""
            }
          ]
        }
      },
      "typeVersion": 2.3
    },
    {
      "id": "94559d44-3ebc-43c2-bc20-16cc9eeb6010",
      "name": "Process Orders in Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        -32,
        624
      ],
      "parameters": {
        "options": {},
        "batchSize": 1
      },
      "typeVersion": 3
    },
    {
      "id": "f2b6b27f-e7a2-4859-b5bb-cb5869d17849",
      "name": "Calculate Days & Determine Action",
      "type": "n8n-nodes-base.code",
      "notes": "This node looks at how old the order is and which reminder tags are already present. It then decides what to do next: send a reminder, cancel the order, or skip.",
      "position": [
        224,
        640
      ],
      "parameters": {
        "jsCode": "// Calculate order age and determine which action to take\nconst config = $('Set Workflow Config').first().json;\nconst r1 = config.reminder1Days || 3;\nconst r2 = config.reminder2Days || 7;\nconst r3 = config.reminder3Days || 10;\nconst cancelAt = config.cancelDays || 14;\n\nconst items = $input.all();\nconst now = Date.now();\n\nfor (let item of items) {\n  const order = item.json.node;\n  \n  if (!order || !order.createdAt) {\n    item.json.action = 'skip';\n    continue;\n  }\n\n  const createdAt = new Date(order.createdAt).getTime();\n  const daysSince = Math.floor((now - createdAt) / 86400000);\n  item.json.daysPending = daysSince;\n\n  // Parse existing tags\n  let tags = order.tags || [];\n  if (typeof tags === 'string') {\n    tags = tags.split(',').map(t => t.trim());\n  }\n\n  const hasR1 = tags.includes('reminder-1-sent');\n  const hasR2 = tags.includes('reminder-2-sent');\n  const hasR3 = tags.includes('reminder-3-sent');\n\n  // Assign action based on days and sent reminders\n  if (daysSince >= cancelAt && hasR3) {\n    item.json.action = 'cancel';\n    item.json.tagToAdd = 'auto-cancelled';\n  } else if (daysSince >= r3 && hasR2 && !hasR3) {\n    item.json.action = 'reminder_3';\n    item.json.tagToAdd = 'reminder-3-sent';\n    const cancelDate = new Date(createdAt + (cancelAt * 86400000));\n    item.json.cancelDate = cancelDate.toLocaleDateString('en-US', { \n      weekday: 'long', month: 'long', day: 'numeric', year: 'numeric' \n    });\n  } else if (daysSince >= r2 && hasR1 && !hasR2) {\n    item.json.action = 'reminder_2';\n    item.json.tagToAdd = 'reminder-2-sent';\n  } else if (daysSince >= r1 && !hasR1) {\n    item.json.action = 'reminder_1';\n    item.json.tagToAdd = 'reminder-1-sent';\n  } else {\n    item.json.action = 'skip';\n  }\n}\n\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "7e6844b3-f0ea-46e5-876f-a0ed31f489d8",
      "name": "Build Email Templates",
      "type": "n8n-nodes-base.code",
      "notes": "You can change the colours, wording, or layout of the emails here. Each reminder type has its own block, so it's easy to customise.",
      "position": [
        436,
        640
      ],
      "parameters": {
        "jsCode": "// Build email content based on action type\nconst items = $input.all();\nconst config = $('Set Workflow Config').first().json;\nconst storeName = config.shopifyDomain.replace('.myshopify.com', '');\nconst storeUrl = `https://${config.shopifyDomain}`;\n\n// Shared email styles\n  const styles = `\n    <style>\n      body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; background: #f4f4f5; margin: 0; padding: 40px 20px; }\n      .container { max-width: 560px; margin: 0 auto; background: white; border-radius: 12px; overflow: hidden; box-shadow: 0 2px 12px rgba(0,0,0,0.08); }\n      .header { padding: 40px 32px; text-align: center; }\n      .header h1 { margin: 0; font-size: 24px; font-weight: 700; }\n      .content { padding: 32px; }\n      .order-box { background: #f8f9fa; border-radius: 8px; padding: 20px; margin: 24px 0; }\n      .order-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid #e9ecef; }\n      .order-row:last-child { border-bottom: none; }\n      .btn { display: inline-block; background: #0066cc; color: #fff; text-decoration: none; padding: 12px 28px; border-radius: 6px; font-weight: 600; margin: 16px 0; }\n      .footer { background: #f8f9fa; padding: 24px; text-align: center; font-size: 12px; color: #6c757d; border-top: 1px solid #e9ecef; }\n    </style>`;\n\nfor (let item of items) {\n  const order = item.json.node;\n  const action = item.json.action;\n  \n  if (action === 'skip') continue;\n\n  const firstName = order.customer?.firstName || 'Customer';\n  const orderName = order.name || 'your order';\n  const currency = order.totalPriceSet?.shopMoney?.currencyCode || '';\n  const amount = parseFloat(order.totalPriceSet?.shopMoney?.amount || 0).toLocaleString('en-US');\n  const paymentLink = order.statusPageUrl || '#';\n  const cancelDate = item.json.cancelDate;\n\n  // Reminder 1\n  if (action === 'reminder_1') {\n    item.json.subject = `Complete your ${orderName} payment`;\n    item.json.emailHtml = `<!DOCTYPE html><html><head>${styles}</head><body>\n      <div class=\"container\">\n        <div class=\"header\" style=\"background: #6B6EDD; color: white;\">\n          <h1>You left something behind \ud83d\uded2</h1>\n        </div>\n        <div class=\"content\">\n          <p>Hi ${firstName},</p>\n          <p>We noticed your order is awaiting payment. We've saved everything for you!</p>\n          <div class=\"order-box\">\n            <div class=\"order-row\"><strong>Order:</strong> <span>${orderName}</span></div>\n            <div class=\"order-row\"><strong>Amount:</strong> <span>${currency} ${amount}</span></div>\n          </div>\n          <p style=\"text-align: center;\"><a href=\"${paymentLink}\" class=\"btn\">Complete Payment \u2192</a></p>\n        </div>\n        <div class=\"footer\"><p>\u00a9 ${new Date().getFullYear()} ${storeName}</p></div>\n      </div>\n    </body></html>`;\n  }\n  \n  // Reminder 2\n  else if (action === 'reminder_2') {\n    item.json.subject = `\u26a0\ufe0f Action required: ${orderName} still unpaid`;\n    item.json.emailHtml = `<!DOCTYPE html><html><head>${styles}</head><body>\n      <div class=\"container\">\n        <div class=\"header\" style=\"background: #E04B6A; color: white;\">\n          <h1>Still waiting on your payment \u23f3</h1>\n        </div>\n        <div class=\"content\">\n          <p>Hi ${firstName},</p>\n          <p>Your order <strong>${orderName}</strong> is still unpaid. Items may sell out soon!</p>\n          <div class=\"order-box\">\n            <div class=\"order-row\"><strong>Order:</strong> <span>${orderName}</span></div>\n            <div class=\"order-row\"><strong>Amount:</strong> <span>${currency} ${amount}</span></div>\n          </div>\n          <p style=\"text-align: center;\"><a href=\"${paymentLink}\" class=\"btn\">Complete Payment \u2192</a></p>\n        </div>\n        <div class=\"footer\"><p>\u00a9 ${new Date().getFullYear()} ${storeName}</p></div>\n      </div>\n    </body></html>`;\n  }\n  \n  // Reminder 3 - Final Warning\n  else if (action === 'reminder_3') {\n    item.json.subject = `\u26a0\ufe0f FINAL NOTICE: ${orderName} cancels ${cancelDate}`;\n    item.json.emailHtml = `<!DOCTYPE html><html><head>${styles}</head><body>\n      <div class=\"container\">\n        <div class=\"header\" style=\"background: #E8970A; color: #1a1a1a;\">\n          <h1>\u26a0\ufe0f Final Notice</h1>\n        </div>\n        <div class=\"content\">\n          <p>Hi ${firstName},</p>\n          <div style=\"background: #fff3cd; border: 1px solid #ffc107; border-radius: 6px; padding: 16px; margin: 16px 0;\">\n            <p style=\"margin: 0;\"><strong>Your order will be cancelled on ${cancelDate}</strong> if payment is not completed.</p>\n          </div>\n          <div class=\"order-box\">\n            <div class=\"order-row\"><strong>Order:</strong> <span>${orderName}</span></div>\n            <div class=\"order-row\"><strong>Amount:</strong> <span>${currency} ${amount}</span></div>\n          </div>\n          <p style=\"text-align: center;\"><a href=\"${paymentLink}\" class=\"btn\">Pay Now \u2192</a></p>\n        </div>\n        <div class=\"footer\"><p>\u00a9 ${new Date().getFullYear()} ${storeName}</p></div>\n      </div>\n    </body></html>`;\n  }\n  \n  // Cancellation notice\n  else if (action === 'cancel') {\n    const cancelledOn = new Date().toLocaleDateString('en-US', {\n      weekday: 'long', month: 'long', day: 'numeric', year: 'numeric'\n    });\n    item.json.subject = `Order ${orderName} has been cancelled`;\n    item.json.emailHtml = `<!DOCTYPE html><html><head>${styles}</head><body>\n      <div class=\"container\">\n        <div class=\"header\" style=\"background: #2C2C2C; color: white;\">\n          <h1>Order Cancelled</h1>\n        </div>\n        <div class=\"content\">\n          <p>Hi ${firstName},</p>\n          <p>Your order <strong>${orderName}</strong> was cancelled on <strong>${cancelledOn}</strong> due to non-payment.</p>\n          <div class=\"order-box\">\n            <div class=\"order-row\"><strong>Order:</strong> <span>${orderName}</span></div>\n            <div class=\"order-row\"><strong>Amount:</strong> <span>${currency} ${amount}</span></div>\n          </div>\n          <p style=\"text-align: center;\"><a href=\"${storeUrl}\" class=\"btn\">Shop Again \u2192</a></p>\n        </div>\n        <div class=\"footer\"><p>\u00a9 ${new Date().getFullYear()} ${storeName}</p></div>\n      </div>\n    </body></html>`;\n  }\n}\n\nreturn items;"
      },
      "typeVersion": 2
    },
    {
      "id": "3f0571d6-dbde-4158-8656-5fbf1a13ffff",
      "name": "Route by Action Type",
      "type": "n8n-nodes-base.switch",
      "position": [
        644,
        624
      ],
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "cancel"
                  }
                ]
              }
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "typeValidation": "strict"
                },
                "combinator": "and",
                "conditions": [
                  {
                    "operator": {
                      "type": "string",
                      "operation": "contains"
                    },
                    "leftValue": "={{ $json.action }}",
                    "rightValue": "reminder"
                  }
                ]
              }
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "e879683b-9091-4791-ba8b-0112051935f7",
      "name": "Cancel Order in Shopify",
      "type": "n8n-nodes-base.graphql",
      "notes": "This GraphQL mutation cancels the order and restocks the items. The reason is set to OTHER, which works for most cases.",
      "maxTries": 3,
      "position": [
        992,
        544
      ],
      "parameters": {
        "query": "mutation CancelOrder($id: ID!, $reason: OrderCancelReason, $restock: Boolean) {\n  orderCancel(orderId: $id, reason: $reason, restock: $restock) {\n    order {\n      id\n    }\n    userErrors {\n      field\n      message\n    }\n  }\n}",
        "endpoint": "=https://{{ $('Set Workflow Config').first().json.shopifyDomain }}/admin/api/2026-04/graphql.json",
        "variables": "={{\n{\n  \"id\": $json.node.id,\n  \"reason\": \"OTHER\",\n  \"restock\": true\n}\n}}",
        "operationName": "CancelOrder",
        "authentication": "headerAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1.1,
      "waitBetweenTries": 1000
    },
    {
      "id": "1b73fa38-fc01-4077-ad0d-ffe758dbcf7c",
      "name": "Cancel Succeeded?",
      "type": "n8n-nodes-base.if",
      "notes": "Checks the Shopify userErrors array. If the cancellation failed (e.g. order already fulfilled or insufficient permissions), the true branch sends the email; the false branch routes to the error notification instead of silently proceeding.",
      "position": [
        1250,
        544
      ],
      "parameters": {
        "options": {},
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "combinator": "and",
          "conditions": [
            {
              "id": "no-user-errors",
              "operator": {
                "type": "number",
                "operation": "equals"
              },
              "leftValue": "={{ $json.data.orderCancel.userErrors.length }}",
              "rightValue": 0
            }
          ]
        }
      },
      "typeVersion": 2.2
    },
    {
      "id": "0d56dd02-946d-40e8-ba3f-d3caf243f383",
      "name": "Send Cancellation Email",
      "type": "n8n-nodes-base.gmail",
      "maxTries": 3,
      "position": [
        1496,
        544
      ],
      "parameters": {
        "sendTo": "={{ $('Route by Action Type').item.json.node.customer.email }}",
        "message": "={{ $('Route by Action Type').item.json.emailHtml }}",
        "options": {},
        "subject": "={{ $('Route by Action Type').item.json.subject }}",
        "emailType": "HTML"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 2.2,
      "waitBetweenTries": 1000
    },
    {
      "id": "85d0892d-a727-42ec-885c-3a8d0a3c45cd",
      "name": "Add Reminder Tag to Order",
      "type": "n8n-nodes-base.graphql",
      "notes": "Adds a tag like 'reminder-1-sent' so the workflow knows not to send the same reminder again.",
      "maxTries": 3,
      "position": [
        1296,
        736
      ],
      "parameters": {
        "query": "mutation AddTags($id: ID!, $tags: [String!]!) {\n  tagsAdd(id: $id, tags: $tags) { node { id } }\n}",
        "endpoint": "=https://{{ $('Set Workflow Config').first().json.shopifyDomain }}/admin/api/2026-04/graphql.json",
        "variables": "={{ { \"id\": $('Route by Action Type').item.json.node.id, \"tags\": [$('Route by Action Type').item.json.tagToAdd] } }}",
        "operationName": "AddTags",
        "authentication": "headerAuth"
      },
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 1.1,
      "waitBetweenTries": 1000
    },
    {
      "id": "2c6377ba-0ad2-4ad8-ab77-d993a22f8f99",
      "name": "Send Reminder Email",
      "type": "n8n-nodes-base.gmail",
      "maxTries": 3,
      "position": [
        1008,
        736
      ],
      "parameters": {
        "sendTo": "={{ $json.node.customer.email }}",
        "message": "={{ $json.emailHtml }}",
        "options": {},
        "subject": "={{ $json.subject }}",
        "emailType": "HTML"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 2.2,
      "waitBetweenTries": 1000
    },
    {
      "id": "38063cd5-31fa-4fc6-8fa4-db1a51689367",
      "name": "No Action Required",
      "type": "n8n-nodes-base.set",
      "position": [
        1792,
        625
      ],
      "parameters": {
        "mode": "raw",
        "options": {},
        "jsonOutput": "={{ $('Route by Action Type').item.json }}\n"
      },
      "typeVersion": 3.4
    },
    {
      "id": "475e497f-d78b-4488-9ffe-c339b0678c43",
      "name": "Format Order Data for CSV",
      "type": "n8n-nodes-base.code",
      "notes": "Creates a clean CSV with one row per order. You can add more columns here if needed.",
      "position": [
        416,
        96
      ],
      "parameters": {
        "jsCode": "// Generate CSV rows for admin report\nconst items = $input.all();\nconst actionLabels = {\n  'reminder_1': 'Reminder 1',\n  'reminder_2': 'Reminder 2',\n  'reminder_3': 'Final Reminder',\n  'cancel': 'Auto Cancelled',\n  'skip': 'No Action'\n};\n\nreturn items.map((item, i) => ({\n  json: {\n    'Order ID': item.json.node?.id || 'N/A',\n    'Order #': item.json.node?.name || 'N/A',\n    'Customer Email': item.json.node?.customer?.email || 'N/A',\n    'Amount': item.json.node?.totalPriceSet?.shopMoney?.amount || '0',\n    'Currency': item.json.node?.totalPriceSet?.shopMoney?.currencyCode || '',\n    'Days Pending': item.json.daysPending || 0,\n    'Action': actionLabels[item.json.action] || 'Unknown'\n  }\n}));"
      },
      "typeVersion": 2
    },
    {
      "id": "30865d2d-b7d9-444c-9665-2e0fe479f551",
      "name": "Convert to CSV File",
      "type": "n8n-nodes-base.convertToFile",
      "position": [
        624,
        96
      ],
      "parameters": {
        "options": {
          "fileName": "=order-recovery-log-{{ $now.format('yyyy-MM-dd') }}.csv",
          "delimiter": ",",
          "headerRow": true
        },
        "binaryPropertyName": "data"
      },
      "typeVersion": 1.1
    },
    {
      "id": "bb8e3b1d-71e0-4c5d-8cd0-c890d4a2a0ed",
      "name": "Generate Admin Report Email",
      "type": "n8n-nodes-base.code",
      "notes": "The admin gets a short email with key numbers. The full CSV is attached separately.",
      "position": [
        432,
        272
      ],
      "parameters": {
        "jsCode": "// Build admin report email with statistics\nconst items = $input.all();\nlet reminders = 0, cancelled = 0, skipped = 0;\n\nfor (const item of items) {\n  const action = item.json.action || 'skip';\n  if (action.includes('reminder')) reminders++;\n  else if (action === 'cancel') cancelled++;\n  else skipped++;\n}\n\nconst total = reminders + cancelled + skipped;\nconst date = new Date().toLocaleDateString('en-US', {\n  weekday: 'long', year: 'numeric', month: 'long', day: 'numeric'\n});\n\nconst message = `\n<h2>Daily Order Recovery Report</h2>\n<p><strong>Date:</strong> ${date}</p>\n\n<h3>Summary</h3>\n<ul>\n  <li>\ud83d\udce6 Total orders evaluated: <strong>${total}</strong></li>\n  <li>\ud83d\udce7 Reminders sent: <strong>${reminders}</strong></li>\n  <li>\u274c Orders cancelled: <strong>${cancelled}</strong></li>\n  <li>\u23f8\ufe0f No action taken: <strong>${skipped}</strong></li>\n</ul>\n\n<p>See attached CSV for detailed order information.</p>\n\n<hr>\n<p style=\"font-size: 12px; color: #666;\">This is an automated report from your Shopify Order Recovery workflow.</p>\n`;\n\nreturn [{ json: { subject: `\ud83d\udcca Order Recovery Report - ${date}`, message } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "8ebaf132-1d68-4773-8dff-aad9fe1053a9",
      "name": "Merge CSV with Email",
      "type": "n8n-nodes-base.merge",
      "position": [
        848,
        144
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combineBy": "combineByPosition",
        "numberInputs": 2
      },
      "typeVersion": 3.2
    },
    {
      "id": "da74c6c4-2938-4b8c-8926-09c9b3e19e2d",
      "name": "Send Daily Report to Admin",
      "type": "n8n-nodes-base.gmail",
      "maxTries": 3,
      "position": [
        1088,
        144
      ],
      "parameters": {
        "sendTo": "={{ $('Set Workflow Config').first().json.adminEmail }}",
        "message": "={{ $json.message }}",
        "options": {
          "attachmentsUi": {
            "attachmentsBinary": [
              {
                "property": "data"
              }
            ]
          }
        },
        "subject": "={{ $json.subject }}",
        "emailType": "HTML"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "retryOnFail": true,
      "typeVersion": 2.2,
      "waitBetweenTries": 1000
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "connections": {
    "On Global Error": {
      "main": [
        [
          {
            "node": "Send Error Notification",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cancel Succeeded?": {
      "main": [
        [
          {
            "node": "Send Cancellation Email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Action Required",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "No Action Required": {
      "main": [
        [
          {
            "node": "Process Orders in Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert to CSV File": {
      "main": [
        [
          {
            "node": "Merge CSV with Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Reminder Email": {
      "main": [
        [
          {
            "node": "Add Reminder Tag to Order",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Workflow Config": {
      "main": [
        [
          {
            "node": "Fetch Unpaid Shopify Orders",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge CSV with Email": {
      "main": [
        [
          {
            "node": "Send Daily Report to Admin",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Route by Action Type": {
      "main": [
        [
          {
            "node": "Cancel Order in Shopify",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Send Reminder Email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "No Action Required",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Email Templates": {
      "main": [
        [
          {
            "node": "Route by Action Type",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Daily Schedule Trigger": {
      "main": [
        [
          {
            "node": "Set Workflow Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cancel Order in Shopify": {
      "main": [
        [
          {
            "node": "Cancel Succeeded?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Send Cancellation Email": {
      "main": [
        [
          {
            "node": "No Action Required",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Add Reminder Tag to Order": {
      "main": [
        [
          {
            "node": "No Action Required",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format Order Data for CSV": {
      "main": [
        [
          {
            "node": "Convert to CSV File",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process Orders in Batches": {
      "main": [
        [
          {
            "node": "Format Order Data for CSV",
            "type": "main",
            "index": 0
          },
          {
            "node": "Generate Admin Report Email",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Calculate Days & Determine Action",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Unpaid Shopify Orders": {
      "main": [
        [
          {
            "node": "Split Orders from GraphQL Response",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Admin Report Email": {
      "main": [
        [
          {
            "node": "Merge CSV with Email",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Filter Orders with Valid Email": {
      "main": [
        [
          {
            "node": "Process Orders in Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Days & Determine Action": {
      "main": [
        [
          {
            "node": "Build Email Templates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Orders from GraphQL Response": {
      "main": [
        [
          {
            "node": "Filter Orders with Valid Email",
            "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 daily to find open, unpaid Shopify orders, send staged payment reminder emails via Gmail, and automatically cancel long-unpaid orders while restocking inventory. It also emails an admin a daily CSV audit report and sends error alerts if the workflow fails.…

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

More E-commerce workflows → · Browse all categories →

Related workflows

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

E-commerce

Shopify + Mautic. Uses shopifyTrigger, noOp, mautic, crypto. Event-driven trigger; 26 nodes.

Shopify Trigger, Mautic, Crypto +1
E-commerce

Having a seamless flow of customer data between your online store and your marketing platform is essential.

Shopify Trigger, Mautic, Crypto +1
E-commerce

Shopify + Mautic. Uses shopifyTrigger, mautic, crypto, graphql. Event-driven trigger; 26 nodes.

Shopify Trigger, Mautic, Crypto +1
E-commerce

This workflow automates the synchronization of product prices across Shopify and WooCommerce platforms to ensure retail consistency. It triggers when a price change is detected in either system, appli

Shopify Trigger, Woo Commerce Trigger, WooCommerce +3
E-commerce

This workflow runs daily to calculate Shopify product sales velocity, predict days until stockout, and alert on low inventory. It logs alerted variant IDs to Google Sheets to avoid duplicate notificat

Shopify, Google Sheets, Slack +2