{
  "name": "AI Craft Pad — Deduplicate contact records",
  "nodes": [
    {
      "parameters": {},
      "id": "start",
      "name": "Manual start",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "return [{json: {\"records\": [{\"email\": \" ALICE@example.com \", \"name\": \"Alice\"}, {\"email\": \"alice@example.com\", \"name\": \"A. Smith\"}, {\"email\": \"bad\", \"name\": \"Bad\"}]}}];"
      },
      "id": "sample",
      "name": "Sample input",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        260,
        0
      ]
    },
    {
      "parameters": {
        "jsCode": "const module = {exports:{}};\n/*\nMIT License\n\nCopyright (c) 2026 AI Craft Pad\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n\n*/\n/* AI Craft Pad original transformations. MIT. No network, storage or dynamic evaluation. */\n(function(root,factory){const api=factory();if(typeof module==='object'&&module.exports)module.exports=api;else root.AICPTemplates=api;})(typeof window==='undefined'?this:window,function(){\n 'use strict';\n const text=v=>typeof v==='string'?v.trim():'';\n const date=v=>{const s=text(v);if(!/^\\d{4}-\\d{2}-\\d{2}$/.test(s))return null;const d=new Date(s+'T00:00:00Z');return !Number.isNaN(+d)&&d.toISOString().slice(0,10)===s?+d:null;};\n const email=v=>{const s=text(v).toLowerCase();return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(s)?s:null;};\n const num=v=>typeof v==='number'&&Number.isFinite(v)?v:null;\n // Conservative ASCII HTTPS parser, shared with n8n's sandbox (which has no URL global).\n // No resolution or fetching. IPv6, raw Unicode URLs and path-dot normalization are outside scope.\n const safeUrl=v=>{try{\n  const raw=text(v);if(!/^[\\x21-\\x7e]+$/.test(raw)||raw.includes('\\\\')||/%(?![0-9a-f]{2})/i.test(raw))return null;\n  const m=/^https:\\/\\/([a-z0-9.-]+)(?::([0-9]{1,5}))?(\\/[^?#]*)?(\\?[^#]*)?(#.*)?$/i.exec(raw);if(!m)return null;\n  const host=m[1].toLowerCase();if(host.length>253||host.split('.').some((s,i,a)=>!(i===a.length-1&&s==='')&&!/^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/.test(s)))return null;\n  if(/^\\d+\\.\\d+\\.\\d+\\.\\d+$/.test(host)&&host.split('.').some(s=>Number(s)>255))return null;\n  const port=m[2]?Number(m[2]):443;if(port<1||port>65535)return null;\n  let pairs=m[4]?m[4].slice(1).split('&').filter(Boolean).map(s=>{const i=s.indexOf('=');return [decodeURIComponent((i<0?s:s.slice(0,i)).replace(/\\+/g,' ')),decodeURIComponent((i<0?'':s.slice(i+1)).replace(/\\+/g,' '))];}):[];\n  const enc=s=>encodeURIComponent(s).replace(/[!'()~]/g,c=>'%'+c.charCodeAt(0).toString(16).toUpperCase()).replace(/%20/g,'+');\n  const u={hash:m[5]||'',searchParams:{set(k,v){const i=pairs.findIndex(p=>p[0]===k);pairs=pairs.filter((p,n)=>p[0]!==k||n===i);if(i<0)pairs.push([k,v]);else pairs[i]=[k,v];},delete(k){pairs=pairs.filter(p=>p[0]!==k);},keys(){return pairs.map(p=>p[0]);},sort(){pairs.sort((a,b)=>a[0]<b[0]?-1:a[0]>b[0]?1:0);}}};\n  u.href=function(){const query=pairs.map(p=>enc(p[0])+'='+enc(p[1])).join('&');return 'https://'+host+(port===443?'':':'+port)+(m[3]||'/')+(query?'?'+query:'')+u.hash;};return u;\n }catch(_){return null;}};\n const days=(a,b)=>Math.round((a-b)/86400000);\n function run(id,input){\n  if(!input||typeof input!=='object'||Array.isArray(input))throw Error('Use a JSON object with a records array.');\n  const rows=input.records;if(!Array.isArray(rows)||rows.length>1000||rows.some(r=>!r||typeof r!=='object'||Array.isArray(r)))throw Error('records must contain at most 1,000 JSON objects.');\n  const asof=date(input.as_of);\n  if(['renewal-review','invoice-followups','task-due-review'].includes(id)&&asof===null)throw Error('Add a valid as_of date in YYYY-MM-DD format.');\n  const invalid=[],out=[];const reject=(i,reason)=>invalid.push({row:i+1,reason});\n  if(id==='contact-dedup'){\n   const seen=new Map();rows.forEach((r,i)=>{const e=email(r.email);if(!e){reject(i,'Invalid email');return;}if(seen.has(e)){seen.get(e).occurrences++;return;}const v={email:e,name:text(r.name),occurrences:1};seen.set(e,v);out.push(v);});\n  }else if(id==='support-routing'){\n   rows.forEach((r,i)=>{if(!text(r.subject)){reject(i,'Missing subject');return;}const s=(text(r.subject)+' '+text(r.message)).toLowerCase();const matches=[['billing',/invoice|refund|payment/],['access',/login|password|sign.in/],['technical',/error|crash|bug/]].filter(x=>x[1].test(s)).map(x=>x[0]);out.push({id:text(r.id),subject:text(r.subject),queue:matches.length===1?matches[0]:'manual-review',matched:matches,review_required:true});});\n  }else if(id==='weekly-metrics'){\n   rows.forEach((r,i)=>{const c=num(r.current),p=num(r.previous),delta=c-p,percent=p===0?null:Math.round(delta/Math.abs(p)*10000)/100;if(!text(r.metric)||c===null||p===null||!Number.isFinite(delta)||(percent!==null&&!Number.isFinite(percent))){reject(i,'Metric and finite numeric current/previous/change required');return;}out.push({metric:text(r.metric),current:c,previous:p,change:delta,percent_change:percent});});\n  }else if(id==='order-totals'){\n   const totals=new Map();rows.forEach((r,i)=>{if(!/^[A-Z]{3}$/.test(text(r.currency))||!Number.isSafeInteger(r.amount_cents)||r.amount_cents<0){reject(i,'Use currency code and nonnegative integer amount_cents');return;}const c=r.currency,v=(totals.get(c)||0)+r.amount_cents;if(!Number.isSafeInteger(v)){reject(i,'Total exceeds safe integer range');return;}totals.set(c,v);});totals.forEach((amount_cents,currency)=>out.push({currency,amount_cents}));\n  }else if(id==='inventory-reorder'){\n   rows.forEach((r,i)=>{if(!text(r.sku)||![r.stock,r.minimum,r.target].every(n=>Number.isSafeInteger(n)&&n>=0)||r.target<r.minimum){reject(i,'SKU and valid stock/minimum/target required');return;}if(r.stock<=r.minimum)out.push({sku:text(r.sku),stock:r.stock,suggested_units:Math.max(0,r.target-r.stock),approval_required:true});});\n  }else if(id==='renewal-review'||id==='invoice-followups'||id==='task-due-review'){\n   rows.forEach((r,i)=>{const d=date(r.due);if(!text(r.id)||d===null){reject(i,'ID and valid due date required');return;}const status=text(r.status).toLowerCase();if((id==='task-due-review'&&status==='done')||(id==='invoice-followups'&&status==='paid')||(id==='renewal-review'&&status==='cancelled'))return;const n=days(d,asof);if(id==='renewal-review'&&n>30)return;if(id==='invoice-followups'&&n>=0)return;out.push({id:text(r.id),due:text(r.due),days_until_due:n,bucket:n<0?'overdue':n===0?'today':'upcoming',review_required:true});});out.sort((a,b)=>a.days_until_due-b.days_until_due);\n  }else if(id==='meeting-actions'){\n   rows.forEach((r,i)=>{if(!text(r.action)){reject(i,'Missing explicit action');return;}out.push({action:text(r.action),owner:text(r.owner)||'Unassigned',due:date(r.due)===null?null:text(r.due),needs_review:!text(r.owner)||date(r.due)===null});});\n  }else if(id==='content-calendar'){\n   const seen=new Set();rows.forEach((r,i)=>{if(date(r.date)===null||!text(r.channel)||!text(r.title)){reject(i,'Date, channel and title required');return;}const key=r.date+'|'+text(r.channel).toLowerCase();out.push({date:r.date,channel:text(r.channel),title:text(r.title),slot_conflict:seen.has(key),publish_automatically:false});seen.add(key);});out.sort((a,b)=>a.date.localeCompare(b.date));\n  }else if(id==='utm-builder'){\n   rows.forEach((r,i)=>{const u=safeUrl(r.url);if(!u||!text(r.source)||!text(r.medium)||!text(r.campaign)){reject(i,'HTTPS URL, source, medium and campaign required');return;}['source','medium','campaign'].forEach(k=>u.searchParams.set('utm_'+k,text(r[k])));out.push({url:u.href()});});\n  }else if(id==='url-dedup'||id==='feed-digest'){\n   const seen=new Set();rows.forEach((r,i)=>{const u=safeUrl(r.url);if(!u){reject(i,'HTTPS URL without credentials required');return;}u.hash='';for(const k of [...u.searchParams.keys()])if(/^utm_/i.test(k)||['fbclid','gclid'].includes(k.toLowerCase()))u.searchParams.delete(k);u.searchParams.sort();const href=u.href();if(seen.has(href))return;seen.add(href);out.push(id==='feed-digest'?{title:text(r.title)||'Untitled',url:href,published:date(r.published)===null?null:r.published}:{url:href});});\n  }else if(id==='release-notes'){\n   rows.forEach((r,i)=>{const type=text(r.type).toLowerCase();if(!text(r.title)||!['added','fixed','changed','removed'].includes(type)){reject(i,'Title and added/fixed/changed/removed type required');return;}out.push({section:type,title:text(r.title),reference:text(r.reference)});});out.sort((a,b)=>['added','fixed','changed','removed'].indexOf(a.section)-['added','fixed','changed','removed'].indexOf(b.section));\n  }else if(id==='survey-summary'){\n   const counts=[0,0,0,0,0];let total=0;rows.forEach((r,i)=>{if(!Number.isInteger(r.rating)||r.rating<1||r.rating>5){reject(i,'Rating must be an integer from 1 to 5');return;}counts[r.rating-1]++;total+=r.rating;});out.push({responses:counts.reduce((a,b)=>a+b,0),average:counts.reduce((a,b)=>a+b,0)?Math.round(total/counts.reduce((a,b)=>a+b,0)*100)/100:null,counts});\n  }else if(id==='feedback-tags'){\n   rows.forEach((r,i)=>{if(!text(r.feedback)){reject(i,'Missing feedback');return;}const s=r.feedback.toLowerCase(),tags=[['pricing',/price|expensive|cost/],['performance',/slow|latency|speed/],['usability',/confus|difficult|easy/],['reliability',/crash|error|broken/]].filter(x=>x[1].test(s)).map(x=>x[0]);out.push({id:text(r.id),feedback:text(r.feedback),tags:tags.length?tags:['unclassified'],method:'keyword rules; human review required'});});\n  }else if(id==='crm-field-map'){\n   rows.forEach((r,i)=>{const e=email(r.email_address);if(!e||!text(r.full_name)){reject(i,'full_name and valid email_address required');return;}out.push({name:text(r.full_name),email:e,company:text(r.organization),source_id:text(r.record_id),write_to_crm:false});});\n  }else if(id==='webhook-envelope'){\n   rows.forEach((r,i)=>{if(!text(r.event_id)||!text(r.event_type)){reject(i,'Explicit event_id and event_type required');return;}out.push({dedupe_key:text(r.event_type)+':'+text(r.event_id),event_type:text(r.event_type),event_id:text(r.event_id),requires_signature_verification:true});});\n  }else if(id==='email-review-queue'){\n   rows.forEach((r,i)=>{const e=email(r.from);if(!e||!text(r.subject)){reject(i,'Sender email and subject required');return;}out.push({from:e,subject:text(r.subject),queue:/urgent|deadline|overdue/i.test(r.subject)?'priority-review':'standard-review',send_reply:false});});\n  }else if(id==='incident-review'){\n   const levels=['critical','high','medium','low'];rows.forEach((r,i)=>{const s=text(r.severity).toLowerCase();if(!text(r.id)||!levels.includes(s)){reject(i,'ID and critical/high/medium/low severity required');return;}out.push({id:text(r.id),severity:s,owner:text(r.owner)||'Unassigned',page_automatically:false});});out.sort((a,b)=>levels.indexOf(a.severity)-levels.indexOf(b.severity));\n  }else throw Error('Unknown template.');\n  return {template:id,input_count:rows.length,output_count:out.length,records:out,rejected:invalid};\n }\n return {run};\n});\n\nreturn [{json: module.exports.run(\"contact-dedup\", $input.first().json)}];"
      },
      "id": "transform",
      "name": "Reviewable output",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        520,
        0
      ]
    }
  ],
  "connections": {
    "Manual start": {
      "main": [
        [
          {
            "node": "Sample input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Sample input": {
      "main": [
        [
          {
            "node": "Reviewable output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1"
  },
  "pinData": {},
  "tags": []
}
