{
  "name": "REVOPS-LI-006 | 02 | HubSpot attribution reconciliation",
  "nodes": [
    {
      "parameters": {
        "inputSource": "passthrough"
      },
      "id": "f3f36519-4a1b-4c15-9f52-bf124c761db6",
      "name": "Captured Event",
      "type": "n8n-nodes-base.executeWorkflowTrigger",
      "typeVersion": 1.1,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 5
            }
          ]
        }
      },
      "id": "8dc96cb5-ff5e-43c8-9b06-0b99ec448b6c",
      "name": "Retry Missing Matches",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        270,
        0
      ]
    },
    {
      "parameters": {},
      "id": "5e12ad10-5722-403f-8859-9cd5cd0713a0",
      "name": "Process One Event",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        540,
        0
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const config={tenant:'REVOPS-LI-006',write_to_hubspot:false,create_review_tasks:true,review_owner_id:'',own_domains:['REPLACE_YOUR_DOMAIN'],normalization_version:'li006-v1'};\nif(config.own_domains.some(d=>!d||d.includes('REPLACE_')||d.includes('://')))throw new Error('Set own_domains to hostnames such as example.com');\nif(config.review_owner_id&&!/^\\d+$/.test(config.review_owner_id))throw new Error('review_owner_id must be a HubSpot owner ID');\nreturn [{json:{config}}];"
      },
      "id": "cf85486d-3354-4c12-89b3-c82980abde3f",
      "name": "Attribution Configuration",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        810,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT * FROM attribution.claim_event($1::text,$2::text);",
        "options": {
          "queryReplacement": "={{ [$('Attribution Configuration').first().json.config.tenant,String($execution.id)] }}"
        }
      },
      "id": "f7447521-373d-497b-ad7d-5feca83ee30b",
      "name": "Claim Event and Related History",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1080,
        0
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const A = (function attributionLibrary() {\n const utmKeys=['utm_source','utm_medium','utm_campaign','utm_content','utm_term','utm_id'];\n const clickKeys=['gclid','fbclid','msclkid','ttclid','li_fat_id'];\n const sourceAliases={li:'linkedin',linkedin_ads:'linkedin','linkedin.com':'linkedin',fb:'facebook',meta:'facebook','facebook.com':'facebook',google_ads:'google','google.com':'google','bing.com':'bing',twitter:'x','twitter.com':'x','x.com':'x'};\n const mediumAliases={ppc:'cpc',paid_social:'paid_social',paidsocial:'paid_social',cpc_social:'paid_social',organic_social:'organic_social',organicsocial:'organic_social',email_newsletter:'email',newsletter:'email',e_mail:'email'};\n const social=new Set(['linkedin','facebook','instagram','tiktok','x','pinterest','reddit','snapchat']);\n const search=new Set(['google','bing','yahoo','duckduckgo','baidu','yandex']);\n const clean=v=>typeof v==='string'?v.normalize('NFKC').trim().toLowerCase().replace(/[\\s-]+/g,'_').replace(/_+/g,'_').replace(/^_|_$/g,''):null;\n function source(v){const s=clean(v);return sourceAliases[s]||s||null}\n function medium(v,s){let m=clean(v);m=mediumAliases[m]||m;if(m==='cpc')m=social.has(s)?'paid_social':search.has(s)?'paid_search':'paid_unknown';return m||null}\n // Portable inside n8n Code sandboxes: no Node require(), URL or external modules.\n function parseUrl(v){\n  if(typeof v!=='string'||/[\\s\\u0000-\\u001f]/.test(v))return null;\n  const m=v.match(/^(https?):\\/\\/([^/?#]+)([^?#]*)(?:\\?([^#]*))?(?:#.*)?$/i);if(!m)return null;\n  const host=m[2].match(/^([a-z0-9.-]+)(?::(\\d{1,5}))?$/i);if(!host)return null;\n  const decode=x=>{try{return decodeURIComponent(x.replace(/\\+/g,' '))}catch{return x}};\n  const pairs=(m[4]||'').split('&').filter(Boolean).map(raw=>{const index=raw.indexOf('=');return {raw,key:decode(index<0?raw:raw.slice(0,index)),value:decode(index<0?'':raw.slice(index+1))}});\n  return {hostname:host[1].toLowerCase(),base:m[1].toLowerCase()+'://'+m[2].toLowerCase()+(m[3]||'/'),pairs,get:key=>pairs.find(p=>p.key===key)?.value||null};\n }\n function url(v){const u=parseUrl(v);if(!u)return null;const kept=u.pairs.filter(p=>!/^utm_/i.test(p.key)&&!clickKeys.includes(p.key)&&!['_ga','_gl','mc_cid','mc_eid'].includes(p.key));return u.base+(kept.length?'?'+kept.map(p=>p.raw).join('&'):'')}\n function touch(raw,receivedAt,config){\n  const issues=[];const warnings=[];const values={};\n  const landing=parseUrl(raw.landing_page_url||raw.page_url||raw.conversion_page_url||'');\n  for(const k of utmKeys){const form=raw[k];const fromUrl=landing?.get(k);const canonical=v=>k==='utm_source'?source(v):k==='utm_medium'?medium(v,source(raw.utm_source||landing?.get('utm_source'))):clean(v);if(form&&fromUrl&&canonical(form)!==canonical(fromUrl))issues.push('FIELD_URL_DISAGREEMENT:'+k);if(new Set((landing?.pairs||[]).filter(p=>p.key===k).map(p=>canonical(p.value))).size>1)issues.push('DUPLICATE_URL_UTM:'+k);values[k]=typeof form==='string'&&form.trim()?form:fromUrl||null}\n  values.utm_source=source(values.utm_source);values.utm_medium=medium(values.utm_medium,values.utm_source);\n  for(const k of utmKeys.filter(k=>!['utm_source','utm_medium'].includes(k)))values[k]=clean(values[k])||null;\n  const clicks={};for(const k of clickKeys){const v=raw[k]||landing?.get(k);if(typeof v==='string'&&v.trim())clicks[k]=v.trim()}\n  let category='UNKNOWN';let evidence='unknown';let src=values.utm_source;let med=values.utm_medium;\n  if(src&&med){evidence='utm';category=({paid_search:'PAID_SEARCH',paid_social:'PAID_SOCIAL',organic:'ORGANIC_SEARCH',organic_search:'ORGANIC_SEARCH',organic_social:'ORGANIC_SOCIAL',email:'EMAIL',referral:'REFERRAL',partner:'PARTNER',event:'EVENT',webinar:'WEBINAR',outbound:'OUTBOUND',display:'DISPLAY',paid_video:'PAID_VIDEO',video:'VIDEO',affiliate:'AFFILIATE',affiliates:'AFFILIATE',cross_network:'CROSS_NETWORK'})[med]||'UNKNOWN';if(src==='(direct)'&&['(none)','none'].includes(med)){category='DIRECT_TRAFFIC';evidence='explicit_direct'}}\n  if(category==='UNKNOWN'&&Object.keys(clicks).length){evidence='click_id';if(!src)src=clicks.gclid?'google':clicks.msclkid?'bing':clicks.fbclid?'facebook':clicks.ttclid?'tiktok':clicks.li_fat_id?'linkedin':null;warnings.push('CLICK_ID_CHANNEL_UNRESOLVED');}\n  if(category==='UNKNOWN'&&src&&med==='social')warnings.push('SOCIAL_PAID_OR_ORGANIC_UNRESOLVED');\n  if(category==='UNKNOWN'&&!Object.keys(clicks).length&&!values.utm_source&&!values.utm_medium){\n   const r=parseUrl(raw.referrer_url);\n   if(r&&!config.own_domains.some(d=>r.hostname===d||r.hostname.endsWith('.'+d))){src=source(r.hostname.replace(/^www\\./,''));med='referral';category='REFERRAL';evidence='external_referrer';warnings.push('REFERRER_CLASSIFICATION_HEURISTIC')}\n   else if(raw.direct_evidence===true&&raw.tracking_complete===true){src='(direct)';med='(none)';category='DIRECT_TRAFFIC';evidence='observed_direct'}\n  }\n  if(!src&&category==='UNKNOWN')src='unknown';\n  values.utm_source=src;values.utm_medium=med;\n  const stamp=raw.conversion_timestamp||raw.timestamp||raw.submission_timestamp;\n  let timestamp=receivedAt;let inferred=!stamp;\n  if(stamp){if(typeof stamp!=='string'||!/(Z|[+-]\\d\\d:\\d\\d)$/.test(stamp)||!Number.isFinite(Date.parse(stamp)))issues.push('INVALID_TIMESTAMP');else timestamp=new Date(stamp).toISOString()}\n  if(Date.parse(timestamp)>Date.parse(receivedAt)+300000)issues.push('FUTURE_TIMESTAMP');\n  const full=!!values.utm_source&&!!values.utm_medium&&!!values.utm_campaign&&category!=='UNKNOWN';\n  const status=issues.length?'ATTRIBUTION_CONFLICT':full?'ATTRIBUTION_COMPLETE':category!=='UNKNOWN'||Object.keys(clicks).length||values.utm_source!=='unknown'?'ATTRIBUTION_PARTIAL':raw.form_type==='embedded'?'ATTRIBUTION_INCOMPLETE':'ATTRIBUTION_UNKNOWN';\n  return {...values,click_ids:clicks,category,evidence,status,timestamp,timestamp_inferred:inferred,issues,warnings,landing_page_url:url(raw.landing_page_url||raw.page_url),conversion_page_url:url(raw.conversion_page_url||raw.form_page_url||raw.page_url),referrer_url:url(raw.referrer_url),form_id:typeof raw.form_id==='string'?raw.form_id.trim():null,form_name:typeof raw.form_name==='string'?raw.form_name.trim():null,event_name:typeof raw.conversion_event_name==='string'?raw.conversion_event_name:'unknown_conversion'};\n }\n function resolve(job,related,config){\n  const raw=job.raw;const matching=related.filter(e=>e.event_id!==job.event_id&&raw.submission_id&&e.raw.submission_id===raw.submission_id);\n  const candidates=[job,...matching];\n  const emailSet=new Set(candidates.map(e=>typeof e.raw.email==='string'?e.raw.email.trim().toLowerCase():'').filter(Boolean));\n  const idSet=new Set(candidates.map(e=>String(e.raw.contact_id||'')).filter(Boolean));\n  const issues=[];\n  if(emailSet.size>1||idSet.size>1)issues.push('IDENTITY_CONFLICT');\n  const email=[...emailSet][0]||null;const contact_id=[...idSet][0]||null;\n  if(email&&!/^[^\\s<>@]+@[^\\s<>@]+\\.[^\\s<>@]+$/.test(email))issues.push('INVALID_EMAIL_IDENTITY');\n  if(contact_id&&!/^\\d+$/.test(contact_id))issues.push('INVALID_CONTACT_ID');\n  const normalized=candidates.map(e=>({event:e,touch:touch(e.raw,e.received_at,config)}));\n  for(const k of ['utm_source','utm_medium','utm_campaign']){\n   const set=new Set(normalized.filter(x=>x.touch.evidence==='utm').map(x=>x.touch[k]).filter(Boolean));\n   if(set.size>1)issues.push('SUBMISSION_SOURCE_CONFLICT:'+k);\n  }\n  const rank=x=>(x.touch.status==='ATTRIBUTION_COMPLETE'?100:0)+({form:40,website:30,crm:20,analytics:10}[x.event.raw.producer]||0)+(x.touch.evidence==='utm'?20:0);\n  normalized.sort((a,b)=>rank(b)-rank(a)||Date.parse(a.event.received_at)-Date.parse(b.event.received_at));\n  const selected=normalized[0];\n  const firsts=normalized.flatMap(x=>{\n   const arr=[x.touch];if(x.event.raw.first_touch&&typeof x.event.raw.first_touch==='object'){const f=touch(x.event.raw.first_touch,x.event.received_at,config);arr.push(f);issues.push(...f.issues.map(i=>'FIRST_TOUCH:'+i))}return arr;\n  }).filter(t=>t.category!=='UNKNOWN'&&!t.issues.length).sort((a,b)=>Date.parse(a.timestamp)-Date.parse(b.timestamp));\n  issues.push(...selected.touch.issues);\n  return {job,email,contact_id,existing_review_task_id:candidates.find(e=>e.review_task_id)?.review_task_id||null,match_method:raw.email||raw.contact_id?'direct_identity':matching.length&&(email||contact_id)?'shared_submission_id':'unmatched',related_event_ids:matching.map(e=>e.event_id),suggested_event_ids:related.filter(e=>!matching.includes(e)).map(e=>e.event_id),current:selected.touch,earliest:firsts[0]||null,conflicts:[...new Set(issues)],config,lookup_allowed:!!(email||contact_id)&&!issues.some(s=>s.includes('IDENTITY')||s==='INVALID_CONTACT_ID')};\n }\n function decide(ctx,response){\n  const conflict=[...ctx.conflicts];const warnings=[...ctx.current.warnings];let contact=null;\n  if(response){if(response.statusCode===200)contact=response.body;else if(response.statusCode!==404)throw new Error('HubSpot lookup failed: HTTP '+response.statusCode)}\n  const p=contact?.properties||{};const fields={};const preserved=[];\n  if(contact&&ctx.email&&String(p.email||'').toLowerCase()!==ctx.email)conflict.push('CONTACT_EMAIL_ID_MISMATCH');\n  const truth=v=>v===true||v==='true';const locked=truth(p.revops_attribution_locked);\n  if(truth(p.revops_source_conflict_flag))conflict.push('UNRESOLVED_CRM_SOURCE_CONFLICT');\n  if(locked)preserved.push('ALL_SOURCE_FIELDS_USER_LOCKED');\n  const current=ctx.current;const earliest=ctx.earliest;\n  if(p.revops_first_touch_at&&(!Number.isFinite(Date.parse(p.revops_first_touch_at))))conflict.push('EXISTING_FIRST_TIMESTAMP_INVALID');\n  if(p.revops_latest_touch_at&&(!Number.isFinite(Date.parse(p.revops_latest_touch_at))))conflict.push('EXISTING_LATEST_TIMESTAMP_INVALID');\n  if(earliest&&p.revops_original_source&&p.revops_first_touch_at&&Date.parse(earliest.timestamp)<Date.parse(p.revops_first_touch_at)-1000)conflict.push('OLDER_FIRST_TOUCH_DISCOVERED');\n  if(p.revops_original_source==='UNKNOWN'&&earliest)conflict.push('UNKNOWN_ORIGINAL_REQUIRES_REVIEW');\n  if(p.revops_latest_touch_at&&Date.parse(current.timestamp)===Date.parse(p.revops_latest_touch_at)&&p.revops_latest_source&&p.revops_latest_source!==current.category)conflict.push('EQUAL_TIMESTAMP_DIFFERENT_SOURCE');\n  if(p.revops_latest_touch_at&&!locked&&!conflict.length&&Date.parse(current.timestamp)<Date.parse(p.revops_latest_touch_at))preserved.push('LATEST_SOURCE_NEWER_THAN_EVENT');\n  const status=conflict.length?'ATTRIBUTION_CONFLICT':current.status;\n  const reliable=current.category!=='UNKNOWN'&&!current.issues.length;\n  const latestNewer=!p.revops_latest_touch_at||Date.parse(current.timestamp)>Date.parse(p.revops_latest_touch_at);\n  if(contact&&!locked&&!conflict.length){\n   if(!p.revops_original_source&&earliest){fields.revops_original_source=earliest.category;fields.revops_original_source_detail=[earliest.utm_source,earliest.utm_medium,earliest.utm_campaign].filter(Boolean).join(' / ');fields.revops_first_touch_at=earliest.timestamp;}\n   else preserved.push('ORIGINAL_SOURCE_AND_TIMESTAMP');\n   if(reliable&&latestNewer){\n    Object.assign(fields,{revops_latest_source:current.category,revops_latest_source_detail:[current.utm_source,current.utm_medium,current.utm_campaign].filter(Boolean).join(' / '),revops_latest_touch_at:current.timestamp});\n    for(const k of utmKeys)fields['revops_'+k]=current[k]||'';\n    for(const k of ['landing_page_url','conversion_page_url','referrer_url','form_id','form_name'])fields['revops_'+k]=current[k]||'';\n    fields.revops_conversion_event_name=current.event_name;\n    fields.revops_last_submission_id=ctx.job.raw.submission_id||ctx.job.event_id;\n    fields.revops_attribution_status=status;fields.revops_source_cleanup_status=current.timestamp_inferred?'TIMESTAMP_INFERRED':'NORMALIZED';\n    fields.revops_source_conflict_flag='false';fields.revops_source_last_updated_at=new Date().toISOString();\n   } else if(!p.revops_latest_touch_at){fields.revops_attribution_status=status;fields.revops_source_cleanup_status=current.timestamp_inferred?'TIMESTAMP_INFERRED':'HISTORY_ONLY';}\n  }\n  if(contact&&!locked&&conflict.length){fields.revops_source_conflict_flag='true';fields.revops_attribution_status='ATTRIBUTION_CONFLICT';fields.revops_source_cleanup_status='REVIEW_REQUIRED';}\n  let action=locked?'history_only':conflict.length?'review':!contact?'awaiting_contact':Object.keys(fields).length?'completed':'history_only';\n  if(!ctx.lookup_allowed&&!conflict.length)action='awaiting_match';\n  const reviewRequired=!!conflict.length||current.category==='UNKNOWN'||['ATTRIBUTION_UNKNOWN','ATTRIBUTION_INCOMPLETE'].includes(status)||(Object.keys(current.click_ids).length>0&&!current.utm_campaign);\n  if(reviewRequired&&contact&&!locked)action='review';\n  const crmPatch=ctx.config.write_to_hubspot===true&&contact&&!locked&&Object.keys(fields).length>0;\n  const createTask=ctx.config.write_to_hubspot===true&&ctx.config.create_review_tasks===true&&reviewRequired&&!!contact&&!locked&&!ctx.existing_review_task_id;\n  const taskProperties={hs_timestamp:new Date(Date.now()+86400000).toISOString(),hs_task_subject:'Review lead attribution: '+ctx.job.event_id,hs_task_body:'Workflow REVOPS-LI-006\\nEvent '+ctx.job.event_id+'\\nReasons: '+[...conflict,...warnings,status].join('; ')+'\\nRaw/normalized payloads and proposed changes are in the attribution audit vault.',hs_task_status:'NOT_STARTED',hs_task_type:'TODO',hs_task_priority:'MEDIUM'};\n  const owner=ctx.config.review_owner_id||p.hubspot_owner_id;if(owner)taskProperties.hubspot_owner_id=String(owner);\n  return {...ctx,contact_id:contact?.id||null,company_ids:(contact?.associations?.companies?.results||[]).map(x=>x.id),previous_properties:p,proposed_properties:fields,fields_preserved:preserved,conflicts:[...new Set(conflict)],warnings,status,action,review_required:reviewRequired,crm_patch:!!crmPatch,create_task:!!createTask,task:{properties:taskProperties,associations:contact?[{to:{id:contact.id},types:[{associationCategory:'HUBSPOT_DEFINED',associationTypeId:204}]}]:[]}};\n }\n return {clean,source,medium,url,touch,resolve,decide};\n})();\nconst row=$input.first().json;return [{json:A.resolve(row.job,row.related||[],$('Attribution Configuration').first().json.config)}];"
      },
      "id": "8e612c92-9c3a-4807-9b0a-f850828cc597",
      "name": "Resolve Identity and Attribution",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1350,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "bb475f08-d9af-4587-902d-3527c9d16ee1",
              "leftValue": "={{ $json.lookup_allowed === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "5147167c-dc96-4989-9f02-b030812d9c10",
      "name": "Reliable Identity?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1620,
        0
      ]
    },
    {
      "parameters": {
        "method": "GET",
        "url": "={{ 'https://api.hubapi.com/crm/v3/objects/contacts/' + ($('Resolve Identity and Attribution').first().json.contact_id || encodeURIComponent($('Resolve Identity and Attribution').first().json.email)) + ($('Resolve Identity and Attribution').first().json.contact_id ? '?' : '?idProperty=email&') + 'properties=revops_original_source,revops_original_source_detail,revops_latest_source,revops_latest_source_detail,revops_first_touch_at,revops_latest_touch_at,revops_source_last_updated_at,revops_utm_source,revops_utm_medium,revops_utm_campaign,revops_utm_content,revops_utm_term,revops_utm_id,revops_landing_page_url,revops_conversion_page_url,revops_referrer_url,revops_form_id,revops_form_name,revops_conversion_event_name,revops_last_submission_id,revops_attribution_status,revops_source_cleanup_status,revops_attribution_locked,revops_source_conflict_flag,email,hubspot_owner_id&associations=companies' }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "3ac7cfef-0462-4ec4-adf3-ef80a64c29b3",
      "name": "Read Existing HubSpot Contact",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1890,
        0
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_HUBSPOT_CREDENTIAL",
          "name": "ATTRIBUTION HubSpot"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const A = (function attributionLibrary() {\n const utmKeys=['utm_source','utm_medium','utm_campaign','utm_content','utm_term','utm_id'];\n const clickKeys=['gclid','fbclid','msclkid','ttclid','li_fat_id'];\n const sourceAliases={li:'linkedin',linkedin_ads:'linkedin','linkedin.com':'linkedin',fb:'facebook',meta:'facebook','facebook.com':'facebook',google_ads:'google','google.com':'google','bing.com':'bing',twitter:'x','twitter.com':'x','x.com':'x'};\n const mediumAliases={ppc:'cpc',paid_social:'paid_social',paidsocial:'paid_social',cpc_social:'paid_social',organic_social:'organic_social',organicsocial:'organic_social',email_newsletter:'email',newsletter:'email',e_mail:'email'};\n const social=new Set(['linkedin','facebook','instagram','tiktok','x','pinterest','reddit','snapchat']);\n const search=new Set(['google','bing','yahoo','duckduckgo','baidu','yandex']);\n const clean=v=>typeof v==='string'?v.normalize('NFKC').trim().toLowerCase().replace(/[\\s-]+/g,'_').replace(/_+/g,'_').replace(/^_|_$/g,''):null;\n function source(v){const s=clean(v);return sourceAliases[s]||s||null}\n function medium(v,s){let m=clean(v);m=mediumAliases[m]||m;if(m==='cpc')m=social.has(s)?'paid_social':search.has(s)?'paid_search':'paid_unknown';return m||null}\n // Portable inside n8n Code sandboxes: no Node require(), URL or external modules.\n function parseUrl(v){\n  if(typeof v!=='string'||/[\\s\\u0000-\\u001f]/.test(v))return null;\n  const m=v.match(/^(https?):\\/\\/([^/?#]+)([^?#]*)(?:\\?([^#]*))?(?:#.*)?$/i);if(!m)return null;\n  const host=m[2].match(/^([a-z0-9.-]+)(?::(\\d{1,5}))?$/i);if(!host)return null;\n  const decode=x=>{try{return decodeURIComponent(x.replace(/\\+/g,' '))}catch{return x}};\n  const pairs=(m[4]||'').split('&').filter(Boolean).map(raw=>{const index=raw.indexOf('=');return {raw,key:decode(index<0?raw:raw.slice(0,index)),value:decode(index<0?'':raw.slice(index+1))}});\n  return {hostname:host[1].toLowerCase(),base:m[1].toLowerCase()+'://'+m[2].toLowerCase()+(m[3]||'/'),pairs,get:key=>pairs.find(p=>p.key===key)?.value||null};\n }\n function url(v){const u=parseUrl(v);if(!u)return null;const kept=u.pairs.filter(p=>!/^utm_/i.test(p.key)&&!clickKeys.includes(p.key)&&!['_ga','_gl','mc_cid','mc_eid'].includes(p.key));return u.base+(kept.length?'?'+kept.map(p=>p.raw).join('&'):'')}\n function touch(raw,receivedAt,config){\n  const issues=[];const warnings=[];const values={};\n  const landing=parseUrl(raw.landing_page_url||raw.page_url||raw.conversion_page_url||'');\n  for(const k of utmKeys){const form=raw[k];const fromUrl=landing?.get(k);const canonical=v=>k==='utm_source'?source(v):k==='utm_medium'?medium(v,source(raw.utm_source||landing?.get('utm_source'))):clean(v);if(form&&fromUrl&&canonical(form)!==canonical(fromUrl))issues.push('FIELD_URL_DISAGREEMENT:'+k);if(new Set((landing?.pairs||[]).filter(p=>p.key===k).map(p=>canonical(p.value))).size>1)issues.push('DUPLICATE_URL_UTM:'+k);values[k]=typeof form==='string'&&form.trim()?form:fromUrl||null}\n  values.utm_source=source(values.utm_source);values.utm_medium=medium(values.utm_medium,values.utm_source);\n  for(const k of utmKeys.filter(k=>!['utm_source','utm_medium'].includes(k)))values[k]=clean(values[k])||null;\n  const clicks={};for(const k of clickKeys){const v=raw[k]||landing?.get(k);if(typeof v==='string'&&v.trim())clicks[k]=v.trim()}\n  let category='UNKNOWN';let evidence='unknown';let src=values.utm_source;let med=values.utm_medium;\n  if(src&&med){evidence='utm';category=({paid_search:'PAID_SEARCH',paid_social:'PAID_SOCIAL',organic:'ORGANIC_SEARCH',organic_search:'ORGANIC_SEARCH',organic_social:'ORGANIC_SOCIAL',email:'EMAIL',referral:'REFERRAL',partner:'PARTNER',event:'EVENT',webinar:'WEBINAR',outbound:'OUTBOUND',display:'DISPLAY',paid_video:'PAID_VIDEO',video:'VIDEO',affiliate:'AFFILIATE',affiliates:'AFFILIATE',cross_network:'CROSS_NETWORK'})[med]||'UNKNOWN';if(src==='(direct)'&&['(none)','none'].includes(med)){category='DIRECT_TRAFFIC';evidence='explicit_direct'}}\n  if(category==='UNKNOWN'&&Object.keys(clicks).length){evidence='click_id';if(!src)src=clicks.gclid?'google':clicks.msclkid?'bing':clicks.fbclid?'facebook':clicks.ttclid?'tiktok':clicks.li_fat_id?'linkedin':null;warnings.push('CLICK_ID_CHANNEL_UNRESOLVED');}\n  if(category==='UNKNOWN'&&src&&med==='social')warnings.push('SOCIAL_PAID_OR_ORGANIC_UNRESOLVED');\n  if(category==='UNKNOWN'&&!Object.keys(clicks).length&&!values.utm_source&&!values.utm_medium){\n   const r=parseUrl(raw.referrer_url);\n   if(r&&!config.own_domains.some(d=>r.hostname===d||r.hostname.endsWith('.'+d))){src=source(r.hostname.replace(/^www\\./,''));med='referral';category='REFERRAL';evidence='external_referrer';warnings.push('REFERRER_CLASSIFICATION_HEURISTIC')}\n   else if(raw.direct_evidence===true&&raw.tracking_complete===true){src='(direct)';med='(none)';category='DIRECT_TRAFFIC';evidence='observed_direct'}\n  }\n  if(!src&&category==='UNKNOWN')src='unknown';\n  values.utm_source=src;values.utm_medium=med;\n  const stamp=raw.conversion_timestamp||raw.timestamp||raw.submission_timestamp;\n  let timestamp=receivedAt;let inferred=!stamp;\n  if(stamp){if(typeof stamp!=='string'||!/(Z|[+-]\\d\\d:\\d\\d)$/.test(stamp)||!Number.isFinite(Date.parse(stamp)))issues.push('INVALID_TIMESTAMP');else timestamp=new Date(stamp).toISOString()}\n  if(Date.parse(timestamp)>Date.parse(receivedAt)+300000)issues.push('FUTURE_TIMESTAMP');\n  const full=!!values.utm_source&&!!values.utm_medium&&!!values.utm_campaign&&category!=='UNKNOWN';\n  const status=issues.length?'ATTRIBUTION_CONFLICT':full?'ATTRIBUTION_COMPLETE':category!=='UNKNOWN'||Object.keys(clicks).length||values.utm_source!=='unknown'?'ATTRIBUTION_PARTIAL':raw.form_type==='embedded'?'ATTRIBUTION_INCOMPLETE':'ATTRIBUTION_UNKNOWN';\n  return {...values,click_ids:clicks,category,evidence,status,timestamp,timestamp_inferred:inferred,issues,warnings,landing_page_url:url(raw.landing_page_url||raw.page_url),conversion_page_url:url(raw.conversion_page_url||raw.form_page_url||raw.page_url),referrer_url:url(raw.referrer_url),form_id:typeof raw.form_id==='string'?raw.form_id.trim():null,form_name:typeof raw.form_name==='string'?raw.form_name.trim():null,event_name:typeof raw.conversion_event_name==='string'?raw.conversion_event_name:'unknown_conversion'};\n }\n function resolve(job,related,config){\n  const raw=job.raw;const matching=related.filter(e=>e.event_id!==job.event_id&&raw.submission_id&&e.raw.submission_id===raw.submission_id);\n  const candidates=[job,...matching];\n  const emailSet=new Set(candidates.map(e=>typeof e.raw.email==='string'?e.raw.email.trim().toLowerCase():'').filter(Boolean));\n  const idSet=new Set(candidates.map(e=>String(e.raw.contact_id||'')).filter(Boolean));\n  const issues=[];\n  if(emailSet.size>1||idSet.size>1)issues.push('IDENTITY_CONFLICT');\n  const email=[...emailSet][0]||null;const contact_id=[...idSet][0]||null;\n  if(email&&!/^[^\\s<>@]+@[^\\s<>@]+\\.[^\\s<>@]+$/.test(email))issues.push('INVALID_EMAIL_IDENTITY');\n  if(contact_id&&!/^\\d+$/.test(contact_id))issues.push('INVALID_CONTACT_ID');\n  const normalized=candidates.map(e=>({event:e,touch:touch(e.raw,e.received_at,config)}));\n  for(const k of ['utm_source','utm_medium','utm_campaign']){\n   const set=new Set(normalized.filter(x=>x.touch.evidence==='utm').map(x=>x.touch[k]).filter(Boolean));\n   if(set.size>1)issues.push('SUBMISSION_SOURCE_CONFLICT:'+k);\n  }\n  const rank=x=>(x.touch.status==='ATTRIBUTION_COMPLETE'?100:0)+({form:40,website:30,crm:20,analytics:10}[x.event.raw.producer]||0)+(x.touch.evidence==='utm'?20:0);\n  normalized.sort((a,b)=>rank(b)-rank(a)||Date.parse(a.event.received_at)-Date.parse(b.event.received_at));\n  const selected=normalized[0];\n  const firsts=normalized.flatMap(x=>{\n   const arr=[x.touch];if(x.event.raw.first_touch&&typeof x.event.raw.first_touch==='object'){const f=touch(x.event.raw.first_touch,x.event.received_at,config);arr.push(f);issues.push(...f.issues.map(i=>'FIRST_TOUCH:'+i))}return arr;\n  }).filter(t=>t.category!=='UNKNOWN'&&!t.issues.length).sort((a,b)=>Date.parse(a.timestamp)-Date.parse(b.timestamp));\n  issues.push(...selected.touch.issues);\n  return {job,email,contact_id,existing_review_task_id:candidates.find(e=>e.review_task_id)?.review_task_id||null,match_method:raw.email||raw.contact_id?'direct_identity':matching.length&&(email||contact_id)?'shared_submission_id':'unmatched',related_event_ids:matching.map(e=>e.event_id),suggested_event_ids:related.filter(e=>!matching.includes(e)).map(e=>e.event_id),current:selected.touch,earliest:firsts[0]||null,conflicts:[...new Set(issues)],config,lookup_allowed:!!(email||contact_id)&&!issues.some(s=>s.includes('IDENTITY')||s==='INVALID_CONTACT_ID')};\n }\n function decide(ctx,response){\n  const conflict=[...ctx.conflicts];const warnings=[...ctx.current.warnings];let contact=null;\n  if(response){if(response.statusCode===200)contact=response.body;else if(response.statusCode!==404)throw new Error('HubSpot lookup failed: HTTP '+response.statusCode)}\n  const p=contact?.properties||{};const fields={};const preserved=[];\n  if(contact&&ctx.email&&String(p.email||'').toLowerCase()!==ctx.email)conflict.push('CONTACT_EMAIL_ID_MISMATCH');\n  const truth=v=>v===true||v==='true';const locked=truth(p.revops_attribution_locked);\n  if(truth(p.revops_source_conflict_flag))conflict.push('UNRESOLVED_CRM_SOURCE_CONFLICT');\n  if(locked)preserved.push('ALL_SOURCE_FIELDS_USER_LOCKED');\n  const current=ctx.current;const earliest=ctx.earliest;\n  if(p.revops_first_touch_at&&(!Number.isFinite(Date.parse(p.revops_first_touch_at))))conflict.push('EXISTING_FIRST_TIMESTAMP_INVALID');\n  if(p.revops_latest_touch_at&&(!Number.isFinite(Date.parse(p.revops_latest_touch_at))))conflict.push('EXISTING_LATEST_TIMESTAMP_INVALID');\n  if(earliest&&p.revops_original_source&&p.revops_first_touch_at&&Date.parse(earliest.timestamp)<Date.parse(p.revops_first_touch_at)-1000)conflict.push('OLDER_FIRST_TOUCH_DISCOVERED');\n  if(p.revops_original_source==='UNKNOWN'&&earliest)conflict.push('UNKNOWN_ORIGINAL_REQUIRES_REVIEW');\n  if(p.revops_latest_touch_at&&Date.parse(current.timestamp)===Date.parse(p.revops_latest_touch_at)&&p.revops_latest_source&&p.revops_latest_source!==current.category)conflict.push('EQUAL_TIMESTAMP_DIFFERENT_SOURCE');\n  if(p.revops_latest_touch_at&&!locked&&!conflict.length&&Date.parse(current.timestamp)<Date.parse(p.revops_latest_touch_at))preserved.push('LATEST_SOURCE_NEWER_THAN_EVENT');\n  const status=conflict.length?'ATTRIBUTION_CONFLICT':current.status;\n  const reliable=current.category!=='UNKNOWN'&&!current.issues.length;\n  const latestNewer=!p.revops_latest_touch_at||Date.parse(current.timestamp)>Date.parse(p.revops_latest_touch_at);\n  if(contact&&!locked&&!conflict.length){\n   if(!p.revops_original_source&&earliest){fields.revops_original_source=earliest.category;fields.revops_original_source_detail=[earliest.utm_source,earliest.utm_medium,earliest.utm_campaign].filter(Boolean).join(' / ');fields.revops_first_touch_at=earliest.timestamp;}\n   else preserved.push('ORIGINAL_SOURCE_AND_TIMESTAMP');\n   if(reliable&&latestNewer){\n    Object.assign(fields,{revops_latest_source:current.category,revops_latest_source_detail:[current.utm_source,current.utm_medium,current.utm_campaign].filter(Boolean).join(' / '),revops_latest_touch_at:current.timestamp});\n    for(const k of utmKeys)fields['revops_'+k]=current[k]||'';\n    for(const k of ['landing_page_url','conversion_page_url','referrer_url','form_id','form_name'])fields['revops_'+k]=current[k]||'';\n    fields.revops_conversion_event_name=current.event_name;\n    fields.revops_last_submission_id=ctx.job.raw.submission_id||ctx.job.event_id;\n    fields.revops_attribution_status=status;fields.revops_source_cleanup_status=current.timestamp_inferred?'TIMESTAMP_INFERRED':'NORMALIZED';\n    fields.revops_source_conflict_flag='false';fields.revops_source_last_updated_at=new Date().toISOString();\n   } else if(!p.revops_latest_touch_at){fields.revops_attribution_status=status;fields.revops_source_cleanup_status=current.timestamp_inferred?'TIMESTAMP_INFERRED':'HISTORY_ONLY';}\n  }\n  if(contact&&!locked&&conflict.length){fields.revops_source_conflict_flag='true';fields.revops_attribution_status='ATTRIBUTION_CONFLICT';fields.revops_source_cleanup_status='REVIEW_REQUIRED';}\n  let action=locked?'history_only':conflict.length?'review':!contact?'awaiting_contact':Object.keys(fields).length?'completed':'history_only';\n  if(!ctx.lookup_allowed&&!conflict.length)action='awaiting_match';\n  const reviewRequired=!!conflict.length||current.category==='UNKNOWN'||['ATTRIBUTION_UNKNOWN','ATTRIBUTION_INCOMPLETE'].includes(status)||(Object.keys(current.click_ids).length>0&&!current.utm_campaign);\n  if(reviewRequired&&contact&&!locked)action='review';\n  const crmPatch=ctx.config.write_to_hubspot===true&&contact&&!locked&&Object.keys(fields).length>0;\n  const createTask=ctx.config.write_to_hubspot===true&&ctx.config.create_review_tasks===true&&reviewRequired&&!!contact&&!locked&&!ctx.existing_review_task_id;\n  const taskProperties={hs_timestamp:new Date(Date.now()+86400000).toISOString(),hs_task_subject:'Review lead attribution: '+ctx.job.event_id,hs_task_body:'Workflow REVOPS-LI-006\\nEvent '+ctx.job.event_id+'\\nReasons: '+[...conflict,...warnings,status].join('; ')+'\\nRaw/normalized payloads and proposed changes are in the attribution audit vault.',hs_task_status:'NOT_STARTED',hs_task_type:'TODO',hs_task_priority:'MEDIUM'};\n  const owner=ctx.config.review_owner_id||p.hubspot_owner_id;if(owner)taskProperties.hubspot_owner_id=String(owner);\n  return {...ctx,contact_id:contact?.id||null,company_ids:(contact?.associations?.companies?.results||[]).map(x=>x.id),previous_properties:p,proposed_properties:fields,fields_preserved:preserved,conflicts:[...new Set(conflict)],warnings,status,action,review_required:reviewRequired,crm_patch:!!crmPatch,create_task:!!createTask,task:{properties:taskProperties,associations:contact?[{to:{id:contact.id},types:[{associationCategory:'HUBSPOT_DEFINED',associationTypeId:204}]}]:[]}};\n }\n return {clean,source,medium,url,touch,resolve,decide};\n})();\nconst ctx=$('Resolve Identity and Attribution').first().json;const input=$input.first().json;const response=typeof input.statusCode==='number'?input:null;return [{json:A.decide(ctx,response)}];"
      },
      "id": "974e0e90-02eb-439b-b004-e7f60c9273e0",
      "name": "Plan Contact Changes",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        0,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT attribution.audit_plan($1::uuid,$2::text,$3::jsonb) AS audit_id;",
        "options": {
          "queryReplacement": "={{ [$('Claim Event and Related History').first().json.job.event_id,String($execution.id),JSON.stringify($('Plan Contact Changes').first().json)] }}"
        }
      },
      "id": "d7148b37-6e0b-4c18-a7ec-e1a139017cc3",
      "name": "Audit Proposed Changes Before Write",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        270,
        300
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "a60c5ef8-c5ad-4152-accc-f262bca7d26c",
              "leftValue": "={{ $('Plan Contact Changes').first().json.crm_patch === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b6b12b02-3cdf-418e-8a2a-eaf5c896d8bd",
      "name": "CRM Patch Enabled?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        540,
        300
      ]
    },
    {
      "parameters": {
        "method": "PATCH",
        "url": "={{ 'https://api.hubapi.com/crm/v3/objects/contacts/'+$('Plan Contact Changes').first().json.contact_id }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ {properties:$('Plan Contact Changes').first().json.proposed_properties} }}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "2a42c6e6-32e5-4366-ac4c-285067d3c612",
      "name": "Patch Attribution Fields",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        810,
        300
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_HUBSPOT_CREDENTIAL",
          "name": "ATTRIBUTION HubSpot"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE attribution.events SET crm_applied=true,updated_at=now() WHERE event_id=$1::uuid RETURNING event_id;",
        "options": {
          "queryReplacement": "={{ [$('Claim Event and Related History').first().json.job.event_id] }}"
        }
      },
      "id": "5d472ced-a1f1-40bf-8c1b-3422b93cb7b6",
      "name": "Record Applied Patch",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1080,
        300
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "1779586d-7889-4037-8675-42f2ffaea53e",
              "leftValue": "={{ $('Plan Contact Changes').first().json.create_task === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "01c39c3f-f225-4645-9d0b-24a487392dbe",
      "name": "Review Task Enabled?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1350,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/tasks",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ $('Plan Contact Changes').first().json.task }}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "4792f25e-e5d0-4ab3-8671-1f1321a17cd3",
      "name": "Create Attribution Review Task",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1620,
        300
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_HUBSPOT_CREDENTIAL",
          "name": "ATTRIBUTION HubSpot"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "UPDATE attribution.events SET review_task_id=$2::text WHERE event_id=$1::uuid RETURNING event_id;",
        "options": {
          "queryReplacement": "={{ [$('Claim Event and Related History').first().json.job.event_id,$json.id] }}"
        }
      },
      "id": "d5883ba4-2a3d-418d-88d3-f24023cb7ea9",
      "name": "Save Review Task ID",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        1890,
        300
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT * FROM attribution.complete_event($1::uuid,$2::jsonb);",
        "options": {
          "queryReplacement": "={{ [$('Claim Event and Related History').first().json.job.event_id,JSON.stringify($('Plan Contact Changes').first().json)] }}"
        }
      },
      "id": "35496e9c-3a46-460a-9313-9754985c30f7",
      "name": "Commit Decision and Release Queue",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        0,
        600
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "6906c131-bcf0-46c3-9738-485189519d51",
              "leftValue": "={{ $json.pending === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "9e3abebe-6c42-414a-a9a2-bbd90c0a53fe",
      "name": "Ready Events Remain?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        270,
        600
      ]
    },
    {
      "parameters": {
        "source": "database",
        "workflowId": {
          "__rl": true,
          "value": "REPLACE_ATTRIBUTION_WORKER_ID",
          "mode": "id"
        },
        "mode": "once",
        "options": {
          "waitForSubWorkflow": false
        }
      },
      "id": "9fee7373-71d1-4f00-bbc2-c9dead661ff3",
      "name": "Drain Next Event",
      "type": "n8n-nodes-base.executeWorkflow",
      "typeVersion": 1.3,
      "position": [
        540,
        600
      ]
    },
    {
      "parameters": {
        "content": "## Attribution reconciliation\nDry-run default: write_to_hubspot=false. Audit and report facts are still stored.\nOriginal/first-touch values are immutable once present. Latest changes only for newer reliable touches.\nNo identity guesses from company domain, phone, visitor or session alone.\nBind Drain Next Event to this workflow. Attach workflow 06 as Error Workflow.",
        "width": 700,
        "height": 220
      },
      "id": "c32c93d9-81ac-4415-b052-337493cbbce9",
      "name": "Workflow Guide",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -270
      ]
    }
  ],
  "connections": {
    "Captured Event": {
      "main": [
        [
          {
            "node": "Attribution Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Retry Missing Matches": {
      "main": [
        [
          {
            "node": "Attribution Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Process One Event": {
      "main": [
        [
          {
            "node": "Attribution Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Attribution Configuration": {
      "main": [
        [
          {
            "node": "Claim Event and Related History",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claim Event and Related History": {
      "main": [
        [
          {
            "node": "Resolve Identity and Attribution",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Resolve Identity and Attribution": {
      "main": [
        [
          {
            "node": "Reliable Identity?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Reliable Identity?": {
      "main": [
        [
          {
            "node": "Read Existing HubSpot Contact",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Plan Contact Changes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Read Existing HubSpot Contact": {
      "main": [
        [
          {
            "node": "Plan Contact Changes",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Plan Contact Changes": {
      "main": [
        [
          {
            "node": "Audit Proposed Changes Before Write",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Audit Proposed Changes Before Write": {
      "main": [
        [
          {
            "node": "CRM Patch Enabled?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "CRM Patch Enabled?": {
      "main": [
        [
          {
            "node": "Patch Attribution Fields",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Review Task Enabled?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Patch Attribution Fields": {
      "main": [
        [
          {
            "node": "Record Applied Patch",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Record Applied Patch": {
      "main": [
        [
          {
            "node": "Review Task Enabled?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Review Task Enabled?": {
      "main": [
        [
          {
            "node": "Create Attribution Review Task",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Commit Decision and Release Queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Attribution Review Task": {
      "main": [
        [
          {
            "node": "Save Review Task ID",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Review Task ID": {
      "main": [
        [
          {
            "node": "Commit Decision and Release Queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Commit Decision and Release Queue": {
      "main": [
        [
          {
            "node": "Ready Events Remain?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Ready Events Remain?": {
      "main": [
        [
          {
            "node": "Drain Next Event",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Asia/Tbilisi",
    "saveExecutionProgress": true
  },
  "active": false,
  "pinData": {},
  "tags": []
}

