{
  "name": "CRM List Hygiene & Duplicate Merge Planner",
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "saveDataErrorExecution": "all",
    "saveDataSuccessExecution": "all",
    "saveManualExecutions": true
  },
  "nodes": [
    {
      "name": "Run Audit Now",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        -1180,
        260
      ],
      "parameters": {}
    },
    {
      "name": "Weekly Audit",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -1180,
        440
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": [
                1
              ],
              "triggerAtHour": 6,
              "triggerAtMinute": 0
            }
          ]
        }
      }
    },
    {
      "name": "Init Audit Run",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -960,
        350
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/01-init-audit-run.js"
        }
      }
    },
    {
      "name": "Build Page Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -740,
        350
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/02-build-page-request.js"
        }
      }
    },
    {
      "name": "Use Live CRM?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -520,
        350
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "use-live",
              "leftValue": "={{ $json.useLive }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "name": "CRM: Fetch Contact Page",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        -280,
        220
      ],
      "parameters": {
        "method": "GET",
        "url": "https://services.leadconnectorhq.com/contacts/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendQuery": true,
        "specifyQuery": "keypair",
        "queryParameters": {
          "parameters": [
            {
              "name": "locationId",
              "value": "={{ $('Build Page Request').item.json.request.locationId }}"
            },
            {
              "name": "limit",
              "value": "={{ $('Build Page Request').item.json.request.limit }}"
            },
            {
              "name": "startAfterId",
              "value": "={{ $('Build Page Request').item.json.request.cursor }}"
            }
          ]
        },
        "sendHeaders": true,
        "specifyHeaders": "keypair",
        "headerParameters": {
          "parameters": [
            {
              "name": "Version",
              "value": "2021-07-28"
            },
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "name": "Mock Contact Page",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -280,
        480
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/03-mock-contact-page.js"
        }
      }
    },
    {
      "name": "Classify Fetch Error",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        -40,
        20
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/05-classify-api-error.js"
        }
      }
    },
    {
      "name": "Retry Fetch?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        180,
        20
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "should-retry",
              "leftValue": "={{ $json.shouldRetry }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "name": "Backoff Wait",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1.1,
      "position": [
        180,
        -180
      ],
      "parameters": {
        "amount": "={{ $json.retry.waitSeconds }}",
        "unit": "seconds"
      }
    },
    {
      "name": "Accumulate Page",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        200,
        350
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/04-accumulate-page.js"
        }
      }
    },
    {
      "name": "More Pages?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        400,
        350
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "has-more",
              "leftValue": "={{ $json.audit.hasMore }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "name": "Cluster & Score Contacts",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        640,
        460
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/06-cluster-and-score.js"
        }
      }
    },
    {
      "name": "Build Reports",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        580
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/07-build-reports.js"
        }
      }
    },
    {
      "name": "Apply the Merges?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        880,
        340
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "not-dry-run",
              "leftValue": "={{ $json.config.dryRun !== true && $json.mergePlan.length > 0 }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "name": "Dry Run \u2014 Nothing Written",
      "type": "n8n-nodes-base.noOp",
      "typeVersion": 1,
      "position": [
        1120,
        440
      ],
      "parameters": {}
    },
    {
      "name": "Prepare Merge Batches",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        220
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/08-prepare-merge-batches.js"
        }
      }
    },
    {
      "name": "CRM: Merge Contacts",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1340,
        220
      ],
      "parameters": {
        "method": "POST",
        "url": "https://services.leadconnectorhq.com/contacts/merge",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "specifyHeaders": "keypair",
        "headerParameters": {
          "parameters": [
            {
              "name": "Version",
              "value": "2021-07-28"
            },
            {
              "name": "Accept",
              "value": "application/json"
            }
          ]
        },
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json.payload) }}",
        "options": {
          "timeout": 30000,
          "batching": {
            "batch": {
              "batchSize": 5,
              "batchInterval": 1000
            }
          },
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true
            }
          }
        }
      }
    },
    {
      "name": "Record Merge Results",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1580,
        220
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/09-record-merge-results.js"
        }
      }
    },
    {
      "name": "README",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -1200,
        -80
      ],
      "parameters": {
        "width": 480,
        "height": 300,
        "color": 4,
        "content": "## CRM List Hygiene Audit\n\nWalks the whole contact list, finds real duplicates, scores every record for deliverability, and produces a merge plan as a CSV.\n\n**Dry run by default.** `dryRun: true` in **Init Audit Run** means it writes nothing \u2014 it produces a plan for a human to approve. `mockMode: true` means it needs no credentials at all: hit **Test workflow** and it runs against 24 built-in fixture contacts.\n\nThe interesting node is **Cluster & Score Contacts**. Open it."
      }
    },
    {
      "name": "Pagination note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -760,
        -260
      ],
      "parameters": {
        "width": 420,
        "height": 220,
        "color": 5,
        "content": "## The loop\n\n**Build Page Request** is the loop head, and it exists because an HTTP node's error output does not carry the item that went in. Anything downstream of a retry has to be able to rebuild the loop state \u2014 this does it in one place.\n\nFour stop conditions: no cursor, empty page, page ceiling, contact ceiling. Plus a stalled-cursor check, which is how paginators actually fail in production."
      }
    },
    {
      "name": "Safety note",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        900,
        -80
      ],
      "parameters": {
        "width": 420,
        "height": 240,
        "color": 3,
        "content": "## Before it writes anything\n\nThree gates between a plan and a destructive bulk operation:\n\n1. `dryRun` must be explicitly turned off\n2. confidence floor of 90 \u2014 anything below never gets issued\n3. a hard ceiling of 100 merges per run, throttled to 5 req/s\n\nPairs with conflicting evidence (different phones, different first names on a shared email) go to **needs-review.csv**, never to the merge plan. Merging two real customers is not reversible."
      }
    },
    {
      "name": "Page Fetched OK?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        -40,
        220
      ],
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "loose",
            "version": 2
          },
          "conditions": [
            {
              "id": "page-fetched-ok",
              "leftValue": "={{ $json.statusCode >= 200 && $json.statusCode < 300 }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "looseTypeValidation": true,
        "options": {}
      }
    },
    {
      "name": "Report Partial Audit",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        400,
        20
      ],
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": {
          "$code": "code/10-report-partial-audit.js"
        }
      }
    }
  ],
  "connections": {
    "Run Audit Now": {
      "main": [
        [
          {
            "node": "Init Audit Run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Weekly Audit": {
      "main": [
        [
          {
            "node": "Init Audit Run",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Init Audit Run": {
      "main": [
        [
          {
            "node": "Build Page Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Page Request": {
      "main": [
        [
          {
            "node": "Use Live CRM?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Use Live CRM?": {
      "main": [
        [
          {
            "node": "CRM: Fetch Contact Page",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Mock Contact Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CRM: Fetch Contact Page": {
      "main": [
        [
          {
            "node": "Page Fetched OK?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Mock Contact Page": {
      "main": [
        [
          {
            "node": "Accumulate Page",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Classify Fetch Error": {
      "main": [
        [
          {
            "node": "Retry Fetch?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retry Fetch?": {
      "main": [
        [
          {
            "node": "Backoff Wait",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Report Partial Audit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Backoff Wait": {
      "main": [
        [
          {
            "node": "Build Page Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Accumulate Page": {
      "main": [
        [
          {
            "node": "More Pages?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "More Pages?": {
      "main": [
        [
          {
            "node": "Build Page Request",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Cluster & Score Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Cluster & Score Contacts": {
      "main": [
        [
          {
            "node": "Build Reports",
            "type": "main",
            "index": 0
          },
          {
            "node": "Apply the Merges?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apply the Merges?": {
      "main": [
        [
          {
            "node": "Prepare Merge Batches",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Dry Run \u2014 Nothing Written",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Merge Batches": {
      "main": [
        [
          {
            "node": "CRM: Merge Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CRM: Merge Contacts": {
      "main": [
        [
          {
            "node": "Record Merge Results",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Page Fetched OK?": {
      "main": [
        [
          {
            "node": "Accumulate Page",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Classify Fetch Error",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Report Partial Audit": {
      "main": [
        [
          {
            "node": "Cluster & Score Contacts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}