{
  "id": "L10NyVYeXhXnkq39",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Zoho CRM - Data Quality Guardian",
  "tags": [],
  "nodes": [
    {
      "id": "08710672-102e-4641-88d6-ba503a40b7c9",
      "name": "Sticky Note2",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3824,
        704
      ],
      "parameters": {
        "color": 7,
        "width": 512,
        "height": 512,
        "content": "## Contact Data Validation\n\nThis section validates and standardizes key customer contact fields. Email addresses are cleaned and checked for format accuracy, while phone numbers are normalized into international structure for consistent storage, reporting and future communication workflows."
      },
      "typeVersion": 1
    },
    {
      "id": "34120122-8478-4a88-a905-11883a008f9a",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4368,
        704
      ],
      "parameters": {
        "color": 7,
        "width": 448,
        "height": 512,
        "content": "## Duplicate Detection & Enrichment\n\nThis section improves CRM accuracy by detecting possible duplicate records and enriching incomplete customer data. It helps prevent repeated leads, reduces database clutter and automatically fills missing company values for cleaner and more complete CRM records."
      },
      "typeVersion": 1
    },
    {
      "id": "e21f01f0-81c1-4cc8-9a63-e011d149bf69",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4848,
        704
      ],
      "parameters": {
        "color": 7,
        "width": 304,
        "height": 512,
        "content": "## Quality Rules & Flagging\n\nThis section evaluates each CRM record against business quality rules and generates warning flags such as invalid email, invalid phone, duplicate entry or missing company information. It enables proactive data governance and cleaner CRM operations."
      },
      "typeVersion": 1
    },
    {
      "id": "d366f741-23f7-47c3-ab0a-bd1430d2811b",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2976,
        304
      ],
      "parameters": {
        "width": 752,
        "height": 368,
        "content": "## Workflow Overview & Setup Guide\n\nThis workflow automatically audits CRM records from Zoho CRM using a scheduled trigger. At the selected interval, it fetches leads or contacts and processes each record individually. Email addresses are validated and standardized, phone numbers are normalized, duplicate records are identified and missing company names are enriched.\n\nAfter processing, business rules assign quality flags such as invalid email, duplicate record, missing company or invalid phone. A reporting node generates a structured summary, which is then converted into a clean HTML report and automatically sent via email for easy review.\n\n**Recommended Setup Steps:**\n**1.** Connect your Zoho CRM credentials in n8n.\n**2.** Select Leads or Contacts module.\n**3.** Configure the Schedule Trigger (hourly, every 4 hours or daily).\n**4.** Configure Email (SMTP/Gmail) credentials.\n**5.** Test with a limited record batch first.\n**6.** Review summary reports.\n**7.** Adjust validation rules as business needs evolve."
      },
      "typeVersion": 1
    },
    {
      "id": "b253810c-8bf4-489d-aad3-b7d3dc7da95e",
      "name": "Sticky Note - Ingestion",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2976,
        704
      ],
      "parameters": {
        "color": 7,
        "width": 468,
        "height": 508,
        "content": "## CRM Data Intake & Trigger\n\nThis section automatically starts the workflow on a defined schedule and retrieves lead or contact records from Zoho CRM. It ensures CRM data is reviewed regularly for validation, duplicate detection, enrichment and quality monitoring without requiring manual execution."
      },
      "typeVersion": 1
    },
    {
      "id": "d47e0e28-098b-444b-a497-7f2f0faf49d4",
      "name": "Sticky Note - Normalization",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3472,
        704
      ],
      "parameters": {
        "color": 7,
        "width": 324,
        "height": 508,
        "content": "## Record Parsing & Preparation\n\nThis section converts CRM records into individual workflow items so each record can be processed independently. It ensures scalable handling of large datasets and prepares records for downstream validation and transformation logic."
      },
      "typeVersion": 1
    },
    {
      "id": "98c25fd5-ca8b-4396-bd51-f1b97c03c1cc",
      "name": "Fetch Zoho CRM Records",
      "type": "n8n-nodes-base.zohoCrm",
      "position": [
        3264,
        976
      ],
      "parameters": {
        "options": {},
        "resource": "lead",
        "operation": "getAll",
        "returnAll": true
      },
      "credentials": {
        "zohoOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 1
    },
    {
      "id": "fd0b451b-f5ef-48d7-9292-033f4ae5359c",
      "name": "Split Records for Processing",
      "type": "n8n-nodes-base.code",
      "position": [
        3584,
        976
      ],
      "parameters": {
        "jsCode": "const records = JSON.parse($json.records || '[]');\nreturn records.map(r => ({ json: r }));"
      },
      "typeVersion": 2
    },
    {
      "id": "2c0a7dad-a579-40f4-b94d-d6d3b348dc32",
      "name": "Validate Email Addresses",
      "type": "n8n-nodes-base.code",
      "position": [
        3920,
        976
      ],
      "parameters": {
        "jsCode": "return items.map(item => {\nconst email = (item.json.email || '').trim().toLowerCase();\n\nconst email_valid =\n/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email) &&\n!email.includes('..');\n\nreturn {\njson:{\n...item.json,\nemail,\nemail_valid\n}\n};\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "970049ae-8c6a-4b21-b96d-b50941f17055",
      "name": "Normalize Phone Numbers",
      "type": "n8n-nodes-base.code",
      "position": [
        4128,
        976
      ],
      "parameters": {
        "jsCode": "return items.map(item => {\n\nlet phone = (item.json.phone || '')\n.toString()\n.replace(/\\D/g,'');\n\nlet country='unknown';\n\nif(phone.length===10){\nphone='91'+phone;\ncountry='IN';\n}\nelse if(phone.startsWith('1')) country='US';\nelse if(phone.startsWith('44')) country='UK';\nelse if(phone.startsWith('971')) country='UAE';\nelse if(phone.startsWith('49')) country='DE';\nelse if(phone.startsWith('61')) country='AU';\n\nif(phone.length < 10 || phone.length > 15){\nphone='';\n}\n\nreturn {\njson:{\n...item.json,\nphone_standardized: phone,\ncountry\n}\n};\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "6fec4e7b-ff3e-42eb-a3e6-d78e9d503795",
      "name": "Detect Duplicate Records",
      "type": "n8n-nodes-base.code",
      "position": [
        4448,
        976
      ],
      "parameters": {
        "jsCode": "const seen = {};\n\nreturn items.map(item => {\n\nconst email = item.json.email;\nconst phone = item.json.phone_standardized;\n\nconst key = email + \"|\" + phone;\n\nlet is_duplicate = false;\n\nif(seen[key]){\nis_duplicate = true;\n}else{\nseen[key] = true;\n}\n\nreturn {\njson:{\n...item.json,\nis_duplicate\n}\n};\n\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "33ccb61b-820f-400b-b155-43ce073b9748",
      "name": "Enrich Missing Company Data",
      "type": "n8n-nodes-base.code",
      "position": [
        4624,
        976
      ],
      "parameters": {
        "jsCode": "return items.map(item => {\n\nlet company = item.json.company;\n\nif(!company || company.trim()===''){\n\nconst email = item.json.email || '';\n\nif(email.includes('@')){\ncompany = email.split('@')[1].split('.')[0];\ncompany =\ncompany.charAt(0).toUpperCase() +\ncompany.slice(1);\n}else{\ncompany='Unknown Company';\n}\n\n}\n\nreturn {\njson:{\n...item.json,\ncompany_enriched: company\n}\n};\n\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "f9113ab3-eaba-4490-92cf-b7a0d493a372",
      "name": "Generate Quality Flags",
      "type": "n8n-nodes-base.code",
      "position": [
        4944,
        976
      ],
      "parameters": {
        "jsCode": "return items.map(item => {\n\nconst flags=[];\n\nif(!item.json.email_valid)\nflags.push('Invalid Email');\n\nif(!item.json.phone_standardized)\nflags.push('Invalid Phone');\n\nif(item.json.is_duplicate)\nflags.push('Duplicate');\n\nif(!item.json.company || item.json.company==='')\nflags.push('Missing Company');\n\nreturn {\njson:{\n...item.json,\ndata_quality_flags: flags,\nquality_score: 100 - (flags.length * 25)\n}\n};\n\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "6a235f60-3a14-465b-a2db-5b886fad0b2f",
      "name": "Generate Quality Summary Report",
      "type": "n8n-nodes-base.code",
      "position": [
        5296,
        976
      ],
      "parameters": {
        "jsCode": "let total = items.length;\n\nlet summary = {\n  total_records: total,\n  valid_records: 0,\n  invalid_email: 0,\n  invalid_phone: 0,\n  duplicates: 0,\n  missing_company: 0\n};\n\nlet report = [];\n\nfor (const item of items) {\n  const d = item.json;\n\n  let isValid = true;\n\n  // Email validation\n  if (!d.email_valid) {\n    summary.invalid_email++;\n    isValid = false;\n  }\n\n  // Phone validation\n  if (!d.phone_standardized) {\n    summary.invalid_phone++;\n    isValid = false;\n  }\n\n  // Duplicate check\n  if (d.is_duplicate) {\n    summary.duplicates++;\n    isValid = false;\n  }\n\n  // Company check\n  if (!d.company || d.company === '' || d.company === null) {\n    summary.missing_company++;\n    isValid = false;\n  }\n\n  if (isValid) {\n    summary.valid_records++;\n  }\n\n  report.push({\n    id: d.id,\n    name: d.name,\n    email: d.email,\n    country: d.country || 'unknown',\n    status: isValid ? 'VALID' : 'INVALID',\n    flags: d.data_quality_flags || []\n  });\n}\n\nreturn [\n  {\n    json: {\n      summary,\n      report\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "70f1c232-5eb9-4a89-97a3-073c884041b4",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5184,
        704
      ],
      "parameters": {
        "color": 7,
        "width": 368,
        "height": 512,
        "content": "## Reporting & Analytics\n\nThis section aggregates processed records into a summary report showing valid entries, duplicates, incomplete records and validation issues. It provides a quick operational view of CRM data health and helps teams track improvement over time."
      },
      "typeVersion": 1
    },
    {
      "id": "140c559c-0d24-4085-8360-212b307ad6d5",
      "name": "Automated Schedule Trigger",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        3040,
        976
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "triggerAtMinute": 5
            }
          ]
        }
      },
      "typeVersion": 1.3
    },
    {
      "id": "9c47b7ef-b298-4344-852c-dd55845c3d51",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5584,
        704
      ],
      "parameters": {
        "color": 7,
        "width": 368,
        "height": 512,
        "content": "## HTML Report Formatting\n\nThis section converts the generated JSON summary and record data into a structured and visually readable HTML report. It formats key metrics and detailed records into tables and sections, making the output suitable for email delivery and quick business review."
      },
      "typeVersion": 1
    },
    {
      "id": "d55a7169-bd77-468e-be3b-ab6ebc021f2f",
      "name": "Sticky Note6",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5984,
        704
      ],
      "parameters": {
        "color": 7,
        "width": 384,
        "height": 512,
        "content": "## Email Notification & Delivery\nThis section sends the formatted HTML report via email to designated recipients. It ensures stakeholders receive automated CRM data quality insights directly in their inbox on a scheduled basis, enabling timely monitoring and action."
      },
      "typeVersion": 1
    },
    {
      "id": "6be3a895-046d-439c-b173-a4a640b727b9",
      "name": "Format CRM Report to HTML",
      "type": "n8n-nodes-base.code",
      "position": [
        5712,
        976
      ],
      "parameters": {
        "jsCode": "const { summary, report } = items[0].json;\n\nlet rows = report.map(r => {\n  const statusColor = r.status === 'VALID' ? '#16a34a' : '#dc2626';\n\n  return `\n  <tr>\n    <td style=\"padding:8px;border:1px solid #ddd;\">${r.id || ''}</td>\n    <td style=\"padding:8px;border:1px solid #ddd;\">${r.name || ''}</td>\n    <td style=\"padding:8px;border:1px solid #ddd;\">${r.email || ''}</td>\n    <td style=\"padding:8px;border:1px solid #ddd;\">${r.country || ''}</td>\n    <td style=\"padding:8px;border:1px solid #ddd;font-weight:bold;color:${statusColor};\">\n      ${r.status}\n    </td>\n    <td style=\"padding:8px;border:1px solid #ddd;\">\n      ${(r.flags || []).join(', ') || '-'}\n    </td>\n  </tr>\n  `;\n}).join('');\n\nconst html = `\n<div style=\"font-family: Arial, sans-serif; background:#f9fafb; padding:20px;\">\n  \n  <h2 style=\"color:#111827;\">\ud83d\udcca CRM Data Quality Report</h2>\n\n  <div style=\"background:#ffffff;padding:15px;border-radius:8px;margin-bottom:20px;\">\n    <h3 style=\"margin-top:0;\">Summary</h3>\n    <p><strong>Total Records:</strong> ${summary.total_records}</p>\n    <p><strong style=\"color:#16a34a;\">Valid Records:</strong> ${summary.valid_records}</p>\n    <p><strong style=\"color:#dc2626;\">Invalid Emails:</strong> ${summary.invalid_email}</p>\n    <p><strong style=\"color:#dc2626;\">Invalid Phones:</strong> ${summary.invalid_phone}</p>\n    <p><strong style=\"color:#dc2626;\">Duplicates:</strong> ${summary.duplicates}</p>\n    <p><strong style=\"color:#dc2626;\">Missing Company:</strong> ${summary.missing_company}</p>\n  </div>\n\n  <div style=\"background:#ffffff;padding:15px;border-radius:8px;\">\n    <h3 style=\"margin-top:0;\">Details</h3>\n\n    <table style=\"width:100%;border-collapse:collapse;font-size:14px;\">\n      <thead>\n        <tr style=\"background:#111827;color:#ffffff;\">\n          <th style=\"padding:10px;border:1px solid #ddd;\">ID</th>\n          <th style=\"padding:10px;border:1px solid #ddd;\">Name</th>\n          <th style=\"padding:10px;border:1px solid #ddd;\">Email</th>\n          <th style=\"padding:10px;border:1px solid #ddd;\">Country</th>\n          <th style=\"padding:10px;border:1px solid #ddd;\">Status</th>\n          <th style=\"padding:10px;border:1px solid #ddd;\">Flags</th>\n        </tr>\n      </thead>\n      <tbody>\n        ${rows}\n      </tbody>\n    </table>\n  </div>\n\n  <p style=\"margin-top:20px;font-size:12px;color:#6b7280;\">\n    Generated automatically by n8n workflow\n  </p>\n\n</div>\n`;\n\nreturn [{ json: { html } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "b17d66ab-47eb-4d2b-a8a9-81c3608ea176",
      "name": "Send Data Quality Report Email",
      "type": "n8n-nodes-base.gmail",
      "position": [
        6128,
        976
      ],
      "parameters": {
        "sendTo": "user@example.com",
        "message": "={{$json.html}}",
        "options": {
          "appendAttribution": false
        },
        "subject": "Zoho CRM Data Quality Report"
      },
      "credentials": {
        "gmailOAuth2": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 2.2
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "48dcd8b4-09fe-47ff-857a-a234fa61a4c2",
  "connections": {
    "Fetch Zoho CRM Records": {
      "main": [
        [
          {
            "node": "Split Records for Processing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Quality Flags": {
      "main": [
        [
          {
            "node": "Generate Quality Summary Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Phone Numbers": {
      "main": [
        [
          {
            "node": "Detect Duplicate Records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Detect Duplicate Records": {
      "main": [
        [
          {
            "node": "Enrich Missing Company Data",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Email Addresses": {
      "main": [
        [
          {
            "node": "Normalize Phone Numbers",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Format CRM Report to HTML": {
      "main": [
        [
          {
            "node": "Send Data Quality Report Email",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Automated Schedule Trigger": {
      "main": [
        [
          {
            "node": "Fetch Zoho CRM Records",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Enrich Missing Company Data": {
      "main": [
        [
          {
            "node": "Generate Quality Flags",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Records for Processing": {
      "main": [
        [
          {
            "node": "Validate Email Addresses",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Quality Summary Report": {
      "main": [
        [
          {
            "node": "Format CRM Report to HTML",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}