{
  "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
          }
        ]
      ]
    }
  }
}