{
  "name": "REVOPS-LI-007 | 02 | Follow-up planner and manual routing",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "minutes",
              "minutesInterval": 5
            }
          ]
        }
      },
      "id": "495e1245-f322-4f67-82c3-8ae1af965a81",
      "name": "Plan Every Five Minutes",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        0,
        0
      ]
    },
    {
      "parameters": {},
      "id": "7ccc7ff2-1432-4d4b-abbc-0d34371fde0c",
      "name": "Run Planner Manually",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [
        270,
        0
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [{json:{config:{use_ai:false,openai_model:'REPLACE_OPENAI_MODEL',portal_id:'REPLACE_HUBSPOT_PORTAL_ID',approval_url:'REPLACE_N8N_PUBLIC_URL/webhook/revops-li-007-approval'}}}];"
      },
      "id": "7eb9e7d4-88ee-434e-ac9d-c74093139ee3",
      "name": "Planner Configuration",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        540,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT ghosted_proposal.claim_case($1::text) AS case;",
        "options": {
          "queryReplacement": "={{ [String($execution.id)] }}"
        }
      },
      "id": "3903f34e-457e-404b-a9bd-90c2e55e41f7",
      "name": "Claim One Detected Case",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        810,
        0
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "74b68871-cc74-4559-bac9-a0ac16fdec61",
              "leftValue": "={{ $json.case !== null }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "473af25d-286a-4b5c-8718-6fe8e47bc502",
      "name": "Case Found?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1080,
        0
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const G = (function ghostedLibrary(){\n  const text=v=>String(v??'').trim();\n  const bool=v=>['true','1','yes','on'].includes(text(v).toLowerCase());\n  const ms=v=>{if(v==null||v==='')return null;const n=Number(v);if(Number.isFinite(n)&&n>100000000000)return n;const d=Date.parse(v);return Number.isFinite(d)?d:null};\n  const iso=v=>{const n=ms(v);return n?new Date(n).toISOString():null};\n  const latest=(...values)=>values.map(ms).filter(Boolean).sort((a,b)=>b-a)[0]||null;\n  const assoc=(record,type)=>(record.associations?.[type]?.results||[]).map(x=>String(x.id));\n  function classify(deal,config,now=Date.now()){\n    const p=deal.properties||{},proposal=ms(p.revops_proposal_sent_at),reasons=[];\n    if(!proposal)return {action:'MANUAL',status:'MISSING_PROPOSAL_DATE',reasons:['MISSING_PROPOSAL_DATE']};\n    const age=(now-proposal)/86400000;if(age<config.min_age_days||age>config.max_age_days)return {action:'SKIP',status:'OUTSIDE_FOLLOWUP_WINDOW',age_days:age,reasons:[]};\n    if(!config.proposal_stage_ids.includes(text(p.dealstage))||bool(p.hs_is_closed))return {action:'SKIP',status:'DEAL_NO_LONGER_ELIGIBLE',age_days:age,reasons:[]};\n    const activity=latest(p.hs_last_activity_date,p.hs_last_sales_activity_timestamp,p.notes_last_updated,p.hs_last_contacted,p.revops_last_reply_at);\n    const meaningful=activity;\n    const grace=Number(config.activity_grace_minutes||15)*60000;if(meaningful&&meaningful>proposal+grace)return {action:'SKIP',status:'RECENT_ACTIVITY_FOUND',age_days:age,last_activity_at:new Date(meaningful).toISOString(),reasons:[]};\n    if(['FOLLOWED_UP','RE_ENGAGED','MANUAL_REVIEW','APPROVAL_PENDING','SENDING'].includes(text(p.revops_ghosting_status)))return {action:'SKIP',status:'ALREADY_HANDLED',age_days:age,reasons:[]};\n    const contacts=assoc(deal,'contacts'),companies=assoc(deal,'companies');\n    if(contacts.length!==1)reasons.push(contacts.length?'MULTIPLE_CONTACTS':'MISSING_CONTACT');if(companies.length>1)reasons.push('MULTIPLE_COMPANIES');\n    return {action:reasons.length?'MANUAL':'CONTEXT',status:reasons.length?'ASSOCIATION_REVIEW_REQUIRED':'CONTEXT_REQUIRED',age_days:age,proposal_sent_at:new Date(proposal).toISOString(),contacts,companies,last_activity_at:activity?new Date(activity).toISOString():null,reasons};\n  }\n  function finalize(deal,contact,company,openCompanyDeals,config,pre){\n    const reasons=[...(pre.reasons||[])],p=deal.properties||{},cp=contact?.properties||{},channel=text(p.revops_proposal_channel||'email').toLowerCase();\n    if(company&&openCompanyDeals>1)reasons.push('MULTIPLE_ACTIVE_COMPANY_DEALS');\n    if(channel==='email'&&!text(cp.email))reasons.push('MISSING_CONTACT_EMAIL');\n    if(channel==='email'&&(bool(cp.hs_email_optout)||bool(cp.revops_do_not_contact)))reasons.push('CONTACT_SUPPRESSED');\n    if(!['email','crm email'].includes(channel))reasons.push('CHANNEL_REQUIRES_MANUAL_SEND');\n    const amount=Number(p.amount||p.revops_proposal_value||0);if(!Number.isFinite(amount))reasons.push('INVALID_DEAL_VALUE');\n    const highValue=Number.isFinite(amount)&&amount>=config.high_value_threshold;\n    return {deal_id:String(deal.id),deal,contact:contact||null,company:company||null,proposal_sent_at:pre.proposal_sent_at,channel,amount:Number.isFinite(amount)?amount:0,high_value:highValue,requires_approval:highValue&&!reasons.length,action:reasons.length?'MANUAL':'READY',status:reasons.length?'MANUAL_REVIEW_REQUIRED':highValue?'APPROVAL_REQUIRED':'READY_TO_DRAFT',reasons};\n  }\n  function draft(c){const d=c.deal.properties||{},p=c.contact?.properties||{};const first=text(p.firstname)||'there';const proposal=text(d.revops_service_proposed||d.dealname||'proposal');const pain=text(d.revops_main_pain_point);const next=text(d.revops_expected_next_step)||'review any questions together';const objection=text(d.revops_previous_objections);let body=`Hi ${first},\\n\\nI wanted to follow up on the ${proposal} I sent over last week.`;if(pain)body+=` Based on our conversation about ${pain}, I wanted to make sure the proposal covers the priorities we discussed.`;if(objection)body+=` I’m also happy to clarify the points around ${objection}.`;body+=`\\n\\nWould it be helpful to ${next}? If priorities have changed, just let me know and I can adjust the direction.\\n\\nBest,\\n${text(d.revops_salesperson_name)||'Your sales representative'}`;return {subject:`Quick follow-up on ${text(d.dealname)||'the proposal'}`,body};}\n  function revalidate(c,deal,contact,config){const current=classify(deal,config,Date.now()),reasons=[];if(!['CONTEXT','READY'].includes(current.action)&&current.status!=='CONTEXT_REQUIRED')reasons.push('DEAL_CHANGED_OR_ACTIVITY_FOUND');if(iso(deal.properties?.revops_proposal_sent_at)!==iso(c.proposal_sent_at))reasons.push('PROPOSAL_DATE_CHANGED');if(text(deal.properties?.revops_followup_sent_at))reasons.push('FOLLOWUP_ALREADY_SENT');if(!contact||text(contact.properties?.email).toLowerCase()!==text(c.contact?.properties?.email).toLowerCase())reasons.push('CONTACT_EMAIL_CHANGED_OR_MISSING');if(bool(contact?.properties?.hs_email_optout)||bool(contact?.properties?.revops_do_not_contact))reasons.push('CONTACT_SUPPRESSED');return {safe:!reasons.length,reasons};}\n  return {text,bool,ms,iso,latest,assoc,classify,finalize,draft,revalidate};\n})();\nconst c=$('Claim One Detected Case').first().json.case,fallback=G.draft(c);const manual=c.action==='MANUAL',approval=c.requires_approval===true;const prompt=['Write a concise, helpful proposal follow-up email. Do not invent facts. Return JSON with subject and body only.',JSON.stringify({first_name:c.contact?.properties?.firstname,deal_name:c.deal?.properties?.dealname,proposal_summary:c.deal?.properties?.revops_proposal_summary,service:c.deal?.properties?.revops_service_proposed,pain_point:c.deal?.properties?.revops_main_pain_point,expected_next_step:c.deal?.properties?.revops_expected_next_step,objections:c.deal?.properties?.revops_previous_objections,past_communication:c.deal?.properties?.revops_past_communication_summary})].join('\\n');return [{json:{case:c,fallback,prompt,manual,approval}}];"
      },
      "id": "0f3bf8b4-4a9d-486b-8951-520737ae8eec",
      "name": "Build Draft and Route",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1350,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "e6d01316-89e3-4729-bc68-dd5d890369ed",
              "leftValue": "={{ $json.manual === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b9f5a993-53a2-4f53-a102-01d36153f592",
      "name": "Manual Route?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1620,
        0
      ]
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const x=$input.first().json,c=x.case,p=c.deal.properties||{},cfg=$('Planner Configuration').first().json.config;const approval=x.approval;const body=[approval?'High-value proposal follow-up approval required':'Manual proposal follow-up required','Case: '+c.case_id,'Deal: '+(p.dealname||c.deal_id),'Value: '+(p.amount||''),'Reasons: '+(c.reasons||[]).join(', '),approval?'Review pending case in protected approval endpoint: '+cfg.approval_url:'Use the original channel and update the deal after contact.','Deal: https://app.hubspot.com/contacts/'+cfg.portal_id+'/deal/'+c.deal_id].join('\\n');const associations=[{to:{id:c.deal_id},types:[{associationCategory:'HUBSPOT_DEFINED',associationTypeId:216}]}];if(c.contact?.id)associations.push({to:{id:String(c.contact.id)},types:[{associationCategory:'HUBSPOT_DEFINED',associationTypeId:204}]});return [{json:{case:c,approval,task:{properties:{hs_task_subject:approval?'Approve high-value proposal follow-up':'Manual proposal follow-up',hs_task_body:body,hs_timestamp:new Date(Date.now()+86400000).toISOString(),hs_task_status:'NOT_STARTED',hs_task_priority:approval?'HIGH':'MEDIUM',hs_task_type:'TODO',...(p.hubspot_owner_id?{hubspot_owner_id:p.hubspot_owner_id}:{})},associations}}}];"
      },
      "id": "72c105ef-84d5-4417-8928-85d0a1ed6533",
      "name": "Build Manual or Approval Task",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1890,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.hubapi.com/crm/v3/objects/tasks",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ $json.task }}",
        "options": {
          "timeout": 30000
        }
      },
      "id": "fc4a4ca7-777d-4fcd-b62d-4ba47c52a169",
      "name": "Create Owner Task",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        0,
        300
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_HUBSPOT_CREDENTIAL",
          "name": "ATTRIBUTION HubSpot"
        }
      },
      "retryOnFail": true,
      "maxTries": 3,
      "waitBetweenTries": 2000
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT ghosted_proposal.complete_routing($1::uuid,$2::text,$3::text,$4::jsonb);",
        "options": {
          "queryReplacement": "={{ [$('Build Manual or Approval Task').first().json.case.case_id,$('Build Manual or Approval Task').first().json.approval?'APPROVAL_PENDING':'MANUAL_TASK_CREATED',$json.id,JSON.stringify($('Build Manual or Approval Task').first().json.case)] }}"
        }
      },
      "id": "c9af84fc-aa78-4311-be38-78cd869521f5",
      "name": "Complete Manual Routing",
      "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": "0da17685-a4c6-4374-9f2a-2557e4902b0f",
              "leftValue": "={{ $('Build Draft and Route').first().json.approval === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "b73e8b5d-3143-4730-8daa-ffd45d091ada",
      "name": "High Value Approval?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        540,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT ghosted_proposal.save_draft($1::uuid,$2::jsonb,$3::text);",
        "options": {
          "queryReplacement": "={{ [$('Build Draft and Route').first().json.case.case_id,JSON.stringify($('Build Draft and Route').first().json.fallback),'APPROVAL_PENDING'] }}"
        }
      },
      "id": "649a7376-80a5-40d5-b28c-3c6749514e4e",
      "name": "Save Approval Draft",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        810,
        300
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "return [$('Build Draft and Route').first()];"
      },
      "id": "9b473029-8cc6-42e0-a049-7b5973391822",
      "name": "Build Approval Task from Draft",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1080,
        300
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "06af3674-1bc1-4dfa-8ad8-4c4d14e9ffd2",
              "leftValue": "={{ $('Planner Configuration').first().json.config.use_ai === true }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "2e9f91c4-2677-47eb-9b99-c509165ac97a",
      "name": "Use AI Enhancement?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1350,
        300
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.openai.com/v1/responses",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendBody": true,
        "contentType": "json",
        "specifyBody": "json",
        "jsonBody": "={{ {model:$('Planner Configuration').first().json.config.openai_model,input:$('Build Draft and Route').first().json.prompt,text:{format:{type:'json_schema',name:'proposal_followup',strict:true,schema:{type:'object',properties:{subject:{type:'string'},body:{type:'string'}},required:['subject','body'],additionalProperties:false}}}} }}",
        "options": {
          "timeout": 30000,
          "response": {
            "response": {
              "fullResponse": true,
              "neverError": true,
              "responseFormat": "json"
            }
          }
        }
      },
      "id": "b49beb0d-0933-4172-a41a-b490a217fa38",
      "name": "Generate AI Follow-Up",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1620,
        300
      ],
      "credentials": {
        "httpHeaderAuth": {
          "id": "REPLACE_OPENAI_CREDENTIAL",
          "name": "ATTRIBUTION OpenAI"
        }
      }
    },
    {
      "parameters": {
        "mode": "runOnceForAllItems",
        "jsCode": "const base=$('Build Draft and Route').first().json;let draft=base.fallback,source='FALLBACK_TEMPLATE';const r=$input.first().json;if(r?.statusCode===200){try{const txt=(r.body.output||[]).flatMap(o=>o.content||[]).find(c=>c.type==='output_text')?.text;const parsed=JSON.parse(txt);if(typeof parsed.subject==='string'&&typeof parsed.body==='string'&&parsed.subject.length<=200&&parsed.body.length<=5000){draft=parsed;source='AI'}}catch{}}return [{json:{case:base.case,draft,source}}];"
      },
      "id": "11da6698-47ea-41c8-91ea-193fd4e6d905",
      "name": "Validate AI or Use Fallback",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1890,
        300
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT ghosted_proposal.save_draft($1::uuid,$2::jsonb,$3::text);",
        "options": {
          "queryReplacement": "={{ [$json.case.case_id,JSON.stringify({...$json.draft,source:$json.source}),'READY_TO_SEND'] }}"
        }
      },
      "id": "0dbb2bc6-55f8-4c10-95bb-9dcb0eee9f90",
      "name": "Save Send-Ready Draft",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.6,
      "position": [
        0,
        600
      ],
      "credentials": {
        "postgres": {
          "id": "REPLACE_POSTGRES_CREDENTIAL",
          "name": "ATTRIBUTION Postgres"
        }
      }
    },
    {
      "parameters": {
        "content": "## Drafting and routing\nManual/non-email/missing-channel cases become owner tasks. High-value email cases receive a task and protected approval gate.\nAI enhancement is optional and OFF by default. The deterministic fallback is always available and facts are constrained to CRM context.\nTask association IDs are standard HubSpot-defined IDs; verify them with the Associations Labels API in your portal during UAT.",
        "width": 700,
        "height": 220
      },
      "id": "c7a3301f-8e92-495e-9224-a2c188faef8e",
      "name": "Workflow Guide",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        0,
        -270
      ]
    }
  ],
  "connections": {
    "Plan Every Five Minutes": {
      "main": [
        [
          {
            "node": "Planner Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Run Planner Manually": {
      "main": [
        [
          {
            "node": "Planner Configuration",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Planner Configuration": {
      "main": [
        [
          {
            "node": "Claim One Detected Case",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Claim One Detected Case": {
      "main": [
        [
          {
            "node": "Case Found?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Case Found?": {
      "main": [
        [
          {
            "node": "Build Draft and Route",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Draft and Route": {
      "main": [
        [
          {
            "node": "Manual Route?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Route?": {
      "main": [
        [
          {
            "node": "Build Manual or Approval Task",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "High Value Approval?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Manual or Approval Task": {
      "main": [
        [
          {
            "node": "Create Owner Task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Create Owner Task": {
      "main": [
        [
          {
            "node": "Complete Manual Routing",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "High Value Approval?": {
      "main": [
        [
          {
            "node": "Save Approval Draft",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Use AI Enhancement?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Save Approval Draft": {
      "main": [
        [
          {
            "node": "Build Approval Task from Draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Build Approval Task from Draft": {
      "main": [
        [
          {
            "node": "Build Manual or Approval Task",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Use AI Enhancement?": {
      "main": [
        [
          {
            "node": "Generate AI Follow-Up",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Validate AI or Use Fallback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate AI Follow-Up": {
      "main": [
        [
          {
            "node": "Validate AI or Use Fallback",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate AI or Use Fallback": {
      "main": [
        [
          {
            "node": "Save Send-Ready Draft",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "timezone": "Asia/Tbilisi",
    "saveExecutionProgress": true
  },
  "active": false,
  "pinData": {},
  "tags": []
}
