AutomationFlowsAI & RAG › Wf-ob-2: Post-call Handler

Wf-ob-2: Post-call Handler

WF-OB-2: Post-Call Handler. Uses httpRequest, slack. Webhook trigger; 48 nodes.

Webhook trigger★★★★★ complexity48 nodesHTTP RequestSlack
AI & RAG Trigger: Webhook Nodes: 48 Complexity: ★★★★★ Added:
Wf-ob-2: Post-call Handler — n8n workflow card showing HTTP Request, Slack integration

This workflow follows the HTTP Request → Slack recipe pattern — see all workflows that pair these two integrations.

The workflow JSON

Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →

Download .json
{
  "name": "WF-OB-2: Post-Call Handler",
  "nodes": [
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "retell-post-call",
        "responseMode": "responseNode",
        "options": {}
      },
      "id": "webhook",
      "name": "Retell Post-Call Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        -144,
        0
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "var body = $json.body || $json;\nvar headers = $json.headers || {};\n\nvar signature = headers['x-retell-signature'] || headers['X-Retell-Signature'] || '';\nif (!signature && !body.call_id && !body.event) {\n  return [{ json: { isSkipEvent: true, eventType: 'invalid_webhook', error: 'Missing Retell signature and call data' } }];\n}\n\nvar eventType = body.event || '';\nif (eventType !== 'call_analyzed') {\n  return [{ json: { isSkipEvent: true, eventType: eventType } }];\n}\n\nvar call = body.call || body;\n\nvar callId = call.call_id || '';\nvar metadata = call.metadata || {};\nvar dynamicVars = call.retell_llm_dynamic_variables || {};\nvar transcript = call.transcript || '';\nvar recordingUrl = call.recording_url || '';\nvar callStatus = call.call_status || call.disconnection_reason || 'unknown';\nvar duration = call.duration_ms || call.call_length_ms || 0;\nvar startTime = call.start_timestamp || 0;\nvar endTime = call.end_timestamp || 0;\n\n// Extract call cost (Retell sends in cents)\nvar callCost = call.call_cost || {};\nvar callCostCents = typeof callCost === 'number' ? callCost : (callCost.combined_cost || 0);\n\nvar analysis = call.call_analysis || {};\nvar customData = analysis.custom_analysis_data || {};\nvar outcome = customData.outcome || '';\nvar interestLevel = customData.interest_level || '';\nvar isDecisionMaker = customData.is_decision_maker === true || customData.is_decision_maker === 'true';\nvar meetingBooked = customData.meeting_booked === true || customData.meeting_booked === 'true';\nvar emailCaptured = customData.email_captured || '';\nvar objectionsRaised = customData.objections_raised || '';\nvar competitorMentioned = customData.competitor_mentioned || '';\nvar currentSystem = customData.current_system || '';\nvar callbackTime = customData.callback_time || '';\nvar callSummary = analysis.call_summary || '';\n\nvar spokeWith = customData.spoke_with || 'unknown';\nvar qualityScore = parseInt(customData.quality_score) || 0;\nvar objectionPrimary = customData.objection_primary || 'none';\nvar reconOpenerQuality = customData.recon_opener_quality || 'empty';\nvar toolsFired = customData.tools_fired || '';\nvar voicemailDetected = customData.voicemail_detected === true || customData.voicemail_detected === 'true';\nvar callDurationSeconds = parseInt(customData.call_duration_seconds) || Math.round(duration / 1000);\n\nvar contactId = dynamicVars.contactId || metadata.contact_id || '';\n\nif (outcome === 'voicemail') outcome = 'voicemail_left';\n\nif (!outcome) {\n  if (callStatus === 'no_answer' || callStatus === 'machine_hangup') outcome = 'no_answer';\n  else if (callStatus === 'voicemail' || callStatus === 'machine_end_call') outcome = 'voicemail_left';\n  else if (callStatus === 'busy') outcome = 'busy';\n  else if (duration > 0 && duration < 15000) outcome = 'hung_up';\n  else if (duration >= 15000) outcome = 'connected';\n  else outcome = 'unknown';\n}\n\nreturn [{ json: {\n  isSkipEvent: false,\n  retellCallId: callId,\n  closeLeadId: metadata.close_lead_id || '',\n  contactId: contactId,\n  callSequence: parseInt(metadata.call_sequence || '1'),\n  callAttempt: parseInt(metadata.call_attempt || '1'),\n  callStatus: callStatus,\n  outcome: outcome,\n  interestLevel: interestLevel,\n  isDecisionMaker: isDecisionMaker,\n  meetingBooked: meetingBooked,\n  emailCaptured: emailCaptured,\n  transcript: transcript,\n  recordingUrl: recordingUrl,\n  aiSummary: callSummary || '',\n  objectionsRaised: objectionsRaised,\n  competitorMentioned: competitorMentioned,\n  currentSystem: currentSystem,\n  callbackTime: callbackTime,\n  durationMs: duration,\n  startTime: startTime,\n  endTime: endTime,\n  spokeWith: spokeWith,\n  qualityScore: qualityScore,\n  objectionPrimary: objectionPrimary,\n  reconOpenerQuality: reconOpenerQuality,\n  toolsFired: toolsFired,\n  voicemailDetected: voicemailDetected,\n  callDurationSeconds: callDurationSeconds,\n  callCostCents: callCostCents\n} }];"
      },
      "id": "parse_call",
      "name": "Parse Call Data",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        224,
        0
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "postcall-check",
              "leftValue": "={{ $json.isSkipEvent }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "false",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "is_post_call",
      "name": "Is Post-Call?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        336,
        0
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT/api/outbound-call-log",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ retellCallId: $json.retellCallId, closeLeadId: $json.closeLeadId, callSequence: $json.callSequence, callAttempt: $json.callAttempt, callStatus: $json.callStatus, outcome: $json.outcome, interestLevel: $json.interestLevel, isDecisionMaker: $json.isDecisionMaker, emailCaptured: $json.emailCaptured, currentSystem: $json.currentSystem, callbackTime: $json.callbackTime, transcript: ($json.transcript || '').substring(0, 5000), recordingUrl: $json.recordingUrl, aiSummary: $json.aiSummary, objectionsRaised: $json.objectionsRaised, competitorMentioned: $json.competitorMentioned, meetingBooked: $json.meetingBooked, durationMs: $json.durationMs, endedAt: Date.now(), spokeWith: $json.spokeWith, qualityScore: $json.qualityScore, objectionPrimary: $json.objectionPrimary, reconOpenerQuality: $json.reconOpenerQuality, toolsFired: $json.toolsFired, voicemailDetected: $json.voicemailDetected, callDurationSeconds: $json.callDurationSeconds }) }}",
        "options": {}
      },
      "id": "log_convex",
      "name": "Update Call Log in Convex",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        416,
        -192
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cond_has_email",
              "leftValue": "={{ $('Parse Call Data').first().json.emailCaptured }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            },
            {
              "id": "cond_has_contact",
              "leftValue": "={{ $('Parse Call Data').first().json.contactId }}",
              "rightValue": "",
              "operator": {
                "type": "string",
                "operation": "notEmpty",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "has_captured_email",
      "name": "Has Captured Email?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        584,
        -100
      ]
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.close.com/api/v1/contact/{{ $('Parse Call Data').first().json.contactId }}/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ emails: [{ email: $('Parse Call Data').first().json.emailCaptured, type: 'office' }] }) }}",
        "options": {}
      },
      "id": "store_email_contact",
      "name": "Store Email on Contact",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        672,
        -160
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "rules": {
          "values": [
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "hot1",
                    "leftValue": "={{ $('Parse Call Data').first().json.outcome }}",
                    "rightValue": "hot_lead",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  },
                  {
                    "id": "hot2",
                    "leftValue": "={{ $('Parse Call Data').first().json.outcome }}",
                    "rightValue": "meeting_booked",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  },
                  {
                    "id": "hot3",
                    "leftValue": "={{ $('Parse Call Data').first().json.outcome }}",
                    "rightValue": "interested",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "or"
              },
              "renameOutput": true,
              "outputKey": "hot_lead"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "vm1",
                    "leftValue": "={{ $('Parse Call Data').first().json.outcome }}",
                    "rightValue": "voicemail_left",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "or"
              },
              "renameOutput": true,
              "outputKey": "voicemail"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "na1",
                    "leftValue": "={{ $('Parse Call Data').first().json.outcome }}",
                    "rightValue": "no_answer",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  },
                  {
                    "id": "na2",
                    "leftValue": "={{ $('Parse Call Data').first().json.outcome }}",
                    "rightValue": "hung_up",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  },
                  {
                    "id": "na3",
                    "leftValue": "={{ $('Parse Call Data').first().json.outcome }}",
                    "rightValue": "gatekeeper",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "or"
              },
              "renameOutput": true,
              "outputKey": "no_answer"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "dnc1",
                    "leftValue": "={{ $('Parse Call Data').first().json.outcome }}",
                    "rightValue": "dnc",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  },
                  {
                    "id": "dnc2",
                    "leftValue": "={{ $('Parse Call Data').first().json.outcome }}",
                    "rightValue": "do_not_call",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  },
                  {
                    "id": "dnc3",
                    "leftValue": "={{ $('Parse Call Data').first().json.outcome }}",
                    "rightValue": "wrong_number",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "or"
              },
              "renameOutput": true,
              "outputKey": "dnc"
            },
            {
              "conditions": {
                "options": {
                  "caseSensitive": true,
                  "leftValue": "",
                  "typeValidation": "strict",
                  "version": 2
                },
                "conditions": [
                  {
                    "id": "cb1",
                    "leftValue": "={{ $('Parse Call Data').first().json.outcome }}",
                    "rightValue": "callback",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  },
                  {
                    "id": "cb2",
                    "leftValue": "={{ $('Parse Call Data').first().json.outcome }}",
                    "rightValue": "call_back",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  },
                  {
                    "id": "cb3",
                    "leftValue": "={{ $('Parse Call Data').first().json.outcome }}",
                    "rightValue": "busy",
                    "operator": {
                      "type": "string",
                      "operation": "equals"
                    }
                  }
                ],
                "combinator": "or"
              },
              "renameOutput": true,
              "outputKey": "callback"
            }
          ]
        },
        "options": {
          "fallbackOutput": "extra"
        }
      },
      "id": "outcome_router",
      "name": "Outcome Router",
      "type": "n8n-nodes-base.switch",
      "typeVersion": 3.2,
      "position": [
        800,
        -240
      ]
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.close.com/api/v1/lead/{{ $('Parse Call Data').first().json.closeLeadId }}/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ 'status_id': 'stat_YRcuGl0533SzjVUwsZOjlOpjDoR25szAYhqEgj8V5nE', 'custom.cf_FLDtrthelKZa2CQvNDFbuS2ymA5W2uncrredxpwYFo8': $('Parse Call Data').first().json.outcome, 'custom.cf_XR3VPy96Ie00Twi24v9VtQUTdZnbWY8jXr2qQdALa38': new Date().toISOString(), 'custom.cf_SjZuFwxbnkfDEdcKCnfgCyPd6X7s77SQACmkY4hYG15': $('Parse Call Data').first().json.callSequence, 'custom.cf_U9vqoYVuHP0wx0fu9pdQzRYPWsIZSEFNfmIueOnII9v': 'completed', 'custom.cf_WIIk0f0rAB0qjRl8IFSRV5sL1VhuLQnrybyNEBoqGV1': $('Parse Call Data').first().json.aiSummary ? $('Parse Call Data').first().json.aiSummary.substring(0, 500) : null, 'custom.cf_W1sgGnp5Xqs0IiKN5FToDh149dkToz6pnxMkEDfEMKb': $('Parse Call Data').first().json.callbackTime || null }) }}",
        "options": {}
      },
      "id": "hot_update_close",
      "name": "Hot Lead - Update Close",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1056,
        -304
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.close.com/api/v1/task/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ lead_id: $('Parse Call Data').first().json.closeLeadId, text: 'HOT LEAD from AI outbound call - ' + $('Parse Call Data').first().json.outcome + '. Call back ASAP! Summary: ' + ($('Parse Call Data').first().json.aiSummary || '').substring(0, 200), date: new Date().toISOString().split('T')[0], is_complete: false }) }}",
        "options": {}
      },
      "id": "create_task",
      "name": "Create Urgent Task",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1280,
        -304
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "C0ABWBR67JL",
          "mode": "list",
          "cachedResultName": "test"
        },
        "text": "=:fire: *HOT LEAD from AI Call!*\nLead: {{ $('Parse Call Data').first().json.closeLeadId }}\nOutcome: {{ $('Parse Call Data').first().json.outcome }}\nSummary: {{ $('Parse Call Data').first().json.aiSummary }}",
        "otherOptions": {}
      },
      "id": "slack_hot",
      "name": "Slack Hot Lead Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1488,
        -304
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT/api/outbound-schedule/update",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ closeLeadId: $('Parse Call Data').first().json.closeLeadId, outboundStatus: 'completed', callSequence: parseInt($('Parse Call Data').first().json.callSequence) || 1, lastOutcome: $('Parse Call Data').first().json.outcome || 'hot_lead', lastCallSummary: ($('Parse Call Data').first().json.aiSummary || '').substring(0, 500), lastCallAt: Date.now() }) }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          },
          "timeout": 15000
        }
      },
      "id": "hot_sync_convex",
      "name": "Hot Lead - Sync Convex",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1712,
        -224
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "var data = $('Parse Call Data').first().json;\nvar seq = data.callSequence || 1;\n\nvar dayGaps = {1: 2, 2: 2, 3: 3, 4: 4, 5: 5};\nvar gap = dayGaps[seq];\n\nvar nextSeq = seq + 1;\nvar nextCallAt = null;\nvar nextCallAtIso = '';\nvar newStatus = 'active';\n\nif (!gap || nextSeq > 6) {\n  newStatus = 'completed';\n} else {\n  var next = new Date();\n  next.setDate(next.getDate() + gap);\n  if (next.getDay() === 0) next.setDate(next.getDate() + 1);\n  if (next.getDay() === 6) next.setDate(next.getDate() + 2);\n  next.setHours(9, 0, 0, 0);\n  nextCallAt = next.getTime();\n  nextCallAtIso = next.toISOString();\n}\n\nreturn {json: {\n  closeLeadId: data.closeLeadId,\n  callSequence: nextSeq,\n  nextCallAt: nextCallAt,\n  nextCallAtIso: nextCallAtIso,\n  lastOutcome: data.outcome,\n  outboundStatus: newStatus\n}};",
        "mode": "runOnceForEachItem"
      },
      "id": "vm_advance",
      "name": "Voicemail - Advance Sequence",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1056,
        -96
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.close.com/api/v1/lead/{{ $json.closeLeadId }}/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ 'custom.cf_FLDtrthelKZa2CQvNDFbuS2ymA5W2uncrredxpwYFo8': 'voicemail_left', 'custom.cf_XR3VPy96Ie00Twi24v9VtQUTdZnbWY8jXr2qQdALa38': new Date().toISOString(), 'custom.cf_L6lj9vbbe1pnuRA2tAAeZs6eso00WI5MI8RNf23Bykn': $json.callSequence, 'custom.cf_t7ney3iqzNMheoFtWFeQi5TJFW31vDaIzV1V6UCEaT8': $json.nextCallAtIso || '', 'custom.cf_U9vqoYVuHP0wx0fu9pdQzRYPWsIZSEFNfmIueOnII9v': $json.outboundStatus }) }}",
        "options": {}
      },
      "id": "vm_update_close",
      "name": "Voicemail - Update Close",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1280,
        -96
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "var data = $('Parse Call Data').first().json;\nvar seq = data.callSequence || 1;\nvar outcome = data.outcome || '';\n\n// Gatekeeper: don't advance sequence, retry in 1 day\nvar isGatekeeper = (outcome === 'gatekeeper');\n\nvar dayGaps = {1: 2, 2: 2, 3: 3, 4: 4, 5: 5};\nvar gap = isGatekeeper ? 1 : dayGaps[seq];\n\nvar nextSeq = isGatekeeper ? seq : seq + 1;\nvar nextCallAt = null;\nvar nextCallAtIso = '';\nvar newStatus = 'active';\n\nif (!isGatekeeper && (!gap || nextSeq > 6)) {\n  newStatus = 'completed';\n} else {\n  var next = new Date();\n  next.setDate(next.getDate() + (gap || 1));\n  if (next.getDay() === 0) next.setDate(next.getDate() + 1);\n  if (next.getDay() === 6) next.setDate(next.getDate() + 2);\n  next.setHours(9, 0, 0, 0);\n  nextCallAt = next.getTime();\n  nextCallAtIso = next.toISOString();\n}\n\nreturn {json: {\n  closeLeadId: data.closeLeadId,\n  callSequence: nextSeq,\n  nextCallAt: nextCallAt,\n  nextCallAtIso: nextCallAtIso,\n  lastOutcome: data.outcome,\n  outboundStatus: newStatus\n}};"
      },
      "id": "na_advance",
      "name": "No Answer - Advance Sequence",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1056,
        112
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.close.com/api/v1/lead/{{ $json.closeLeadId }}/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ 'custom.cf_FLDtrthelKZa2CQvNDFbuS2ymA5W2uncrredxpwYFo8': $('Parse Call Data').first().json.outcome, 'custom.cf_XR3VPy96Ie00Twi24v9VtQUTdZnbWY8jXr2qQdALa38': new Date().toISOString(), 'custom.cf_L6lj9vbbe1pnuRA2tAAeZs6eso00WI5MI8RNf23Bykn': $json.callSequence, 'custom.cf_t7ney3iqzNMheoFtWFeQi5TJFW31vDaIzV1V6UCEaT8': $json.nextCallAtIso || '', 'custom.cf_U9vqoYVuHP0wx0fu9pdQzRYPWsIZSEFNfmIueOnII9v': $json.outboundStatus }) }}",
        "options": {}
      },
      "id": "na_update_close",
      "name": "No Answer - Update Close",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1280,
        112
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.close.com/api/v1/lead/{{ $('Parse Call Data').first().json.closeLeadId }}/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ 'status_id': 'stat_OXnpX5vuI6Ujl1AOFHpG4lNsZY5Ek0s9tpzJXIHmw10', 'custom.cf_U9vqoYVuHP0wx0fu9pdQzRYPWsIZSEFNfmIueOnII9v': 'dnc', 'custom.cf_FLDtrthelKZa2CQvNDFbuS2ymA5W2uncrredxpwYFo8': 'dnc', 'custom.cf_XR3VPy96Ie00Twi24v9VtQUTdZnbWY8jXr2qQdALa38': new Date().toISOString(), 'custom.cf_YvT1cF5nYedJGt3kLR87jhCNZ1vsXIVmVIXQqnaTnrp': true }) }}",
        "options": {}
      },
      "id": "dnc_update_close",
      "name": "DNC - Update Close",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1056,
        304
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT/api/outbound-schedule/update",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ closeLeadId: $('Parse Call Data').first().json.closeLeadId, outboundStatus: 'dnc', callSequence: parseInt($('Parse Call Data').first().json.callSequence) || 1, lastOutcome: 'dnc', lastCallAt: Date.now() }) }}",
        "options": {
          "response": {
            "response": {
              "neverError": true
            }
          },
          "timeout": 15000
        }
      },
      "id": "dnc_sync_convex",
      "name": "DNC - Sync Convex",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1168,
        400
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "resource": "message",
        "operation": "post",
        "select": "channel",
        "channelId": {
          "__rl": true,
          "value": "C0ABWBR67JL",
          "mode": "list",
          "cachedResultName": "test"
        },
        "text": "=:no_entry: *DNC Request from AI Call*\nLead: {{ $('Parse Call Data').first().json.closeLeadId }}",
        "otherOptions": {}
      },
      "id": "slack_dnc",
      "name": "DNC Slack Alert",
      "type": "n8n-nodes-base.slack",
      "typeVersion": 2.2,
      "position": [
        1376,
        304
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "PUT",
        "url": "=https://api.close.com/api/v1/lead/{{ $('Parse Call Data').first().json.closeLeadId }}/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ 'custom.cf_FLDtrthelKZa2CQvNDFbuS2ymA5W2uncrredxpwYFo8': $('Parse Call Data').first().json.outcome, 'custom.cf_XR3VPy96Ie00Twi24v9VtQUTdZnbWY8jXr2qQdALa38': new Date().toISOString(), 'custom.cf_WIIk0f0rAB0qjRl8IFSRV5sL1VhuLQnrybyNEBoqGV1': $('Parse Call Data').first().json.aiSummary ? $('Parse Call Data').first().json.aiSummary.substring(0, 500) : null }) }}",
        "options": {}
      },
      "id": "other_update_close",
      "name": "Other - Update Close",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1056,
        512
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cond_ni",
              "leftValue": "={{ $('Parse Call Data').first().json.outcome }}",
              "rightValue": "not_interested",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "if_not_interested",
      "name": "Is Not Interested?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1040,
        688
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.close.com/api/v1/activity/sms/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ lead_id: $('Parse Call Data').first().json.closeLeadId, local_phone: '+15555550100', remote_phone: $('Retell Post-Call Webhook').first().json.body.call.to_number, text: 'Hey ' + ($('Retell Post-Call Webhook').first().json.body.call.retell_llm_dynamic_variables.firstName || 'there') + ', Laura from VoiceSDR. Totally fair - just wanted to leave you with this:\\n\\nMost title offices lose $50K+ a year in missed calls. See your numbers: https://www.voicesdr.ai/roi-calculator\\n\\nWe are here when you are ready.', status: 'outbox', direction: 'outbound' }) }}",
        "options": {}
      },
      "id": "send_roi_sms",
      "name": "Send ROI SMS",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1408,
        752
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "var parsed = $('Parse Call Data').first().json;\nvar webhook = $('Retell Post-Call Webhook').first().json;\nvar dynamicVars = (webhook.body || webhook).call.retell_llm_dynamic_variables || {};\n\nvar emailCaptured = parsed.emailCaptured || '';\nvar contactId = parsed.contactId || '';\nvar leadId = parsed.closeLeadId || '';\nvar firstName = dynamicVars.firstName || 'there';\nvar toNumber = ((webhook.body || webhook).call || {}).to_number || '';\n\nreturn [{ json: {\n  hasEmail: emailCaptured !== '',\n  email: emailCaptured,\n  contactId: contactId,\n  leadId: leadId,\n  firstName: firstName,\n  toNumber: toNumber\n} }];\n"
      },
      "id": "check_email_avail",
      "name": "Check Email Availability",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1152,
        688
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cond_email_avail",
              "leftValue": "={{ $json.hasEmail }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "has_email_roi",
      "name": "Has Email?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        1280,
        624
      ]
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://api.close.com/api/v1/activity/email/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ lead_id: $json.leadId, contact_id: $json.contactId, to: [$json.email], sender: 'user@example.com', subject: 'Your numbers, ' + $json.firstName, body_text: 'Hey ' + $json.firstName + ',\\n\\nTotally fair \u2014 just wanted to leave you with this:\\n\\nMost title offices lose $50K+ a year in missed calls. See your numbers: https://www.voicesdr.ai/roi-calculator\\n\\nWe are here when you are ready.\\n\\nLaura\\nVoiceSDR', status: 'outbox' }) }}",
        "options": {}
      },
      "id": "send_roi_email",
      "name": "Send ROI Email",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1408,
        560
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "var parseData = $('Parse Call Data').first();\nif (!parseData || !parseData.json) {\n  return {json: {closeLeadId: '', callSequence: 1, nextCallAt: null, outboundStatus: 'active', lastOutcome: 'unknown', error: 'Parse Call Data not available'}};\n}\nvar data = parseData.json;\nvar seq = data.callSequence || 1;\nvar outcome = data.outcome || '';\nvar closeApiKey = 'YOUR_CLOSE_API_KEY';\n\n// Check for 2nd consecutive not_interested \u2014 stop calling\nvar previousOutcome = '';\ntry { previousOutcome = $('Mark Task + Check Pipeline').first().json.previousOutcome || ''; } catch(e) {}\n\nif (outcome === 'not_interested' && previousOutcome === 'not_interested') {\n  // 2nd consecutive not_interested \u2014 mark completed, stop outbound\n  try {\n    await this.helpers.httpRequest({\n      method: 'PUT',\n      url: 'https://api.close.com/api/v1/lead/' + data.closeLeadId + '/',\n      headers: { 'Content-Type': 'application/json' },\n      auth: { username: closeApiKey, password: '' },\n      body: {\n        'custom.cf_U9vqoYVuHP0wx0fu9pdQzRYPWsIZSEFNfmIueOnII9v': 'completed',\n        'status_id': 'stat_DpO6mbXdoZIXaDx88uHaGC4lW0DIAcX3rXKVKgaruRw'\n      }\n    });\n  } catch(e) { console.log('Close not_interested stop failed: ' + e.message); }\n\n  return {json: {\n    closeLeadId: data.closeLeadId || '',\n    callSequence: seq,\n    nextCallAt: null,\n    nextCallAtIso: '',\n    lastOutcome: outcome,\n    outboundStatus: 'completed'\n  }};\n}\n\nvar dayGaps = {1: 2, 2: 2, 3: 3, 4: 4, 5: 5};\nvar gap = dayGaps[seq];\n\nvar nextSeq = seq + 1;\nvar nextCallAt = null;\nvar nextCallAtIso = '';\nvar newStatus = 'active';\n\nif (!gap || nextSeq > 6) {\n  newStatus = 'completed';\n} else {\n  var next = new Date();\n  next.setDate(next.getDate() + gap);\n  if (next.getDay() === 0) next.setDate(next.getDate() + 1);\n  if (next.getDay() === 6) next.setDate(next.getDate() + 2);\n  next.setHours(9, 0, 0, 0);\n  nextCallAt = next.getTime();\n  nextCallAtIso = next.toISOString();\n}\n\nreturn {json: {\n  closeLeadId: data.closeLeadId || '',\n  callSequence: nextSeq,\n  nextCallAt: nextCallAt,\n  nextCallAtIso: nextCallAtIso,\n  lastOutcome: outcome,\n  outboundStatus: newStatus\n}};",
        "mode": "runOnceForEachItem"
      },
      "id": "other_advance",
      "name": "Other - Advance Sequence",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1536,
        624
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "jsCode": "var parsed = $('Parse Call Data').first().json;\n\nvar advance = null;\ntry { advance = $('Voicemail - Advance Sequence').first().json; } catch(e) {}\nif (!advance) {\n  try { advance = $('No Answer - Advance Sequence').first().json; } catch(e) {}\n}\nif (!advance) {\n  try { advance = $('Other - Advance Sequence').first().json; } catch(e) {}\n}\n\nif (!advance) {\n  return [{ json: { skip: true } }];\n}\n\nreturn [{ json: {\n  closeLeadId: advance.closeLeadId || parsed.closeLeadId || '',\n  callSequence: advance.callSequence || 1,\n  nextCallAt: advance.nextCallAt || '',\n  outboundStatus: advance.outboundStatus || 'active',\n  lastOutcome: advance.lastOutcome || parsed.outcome || '',\n  lastCallSummary: (parsed.aiSummary || '').substring(0, 500),\n  lastCallAt: Date.now()\n} }];"
      },
      "id": "build_schedule_sync",
      "name": "Build Schedule Sync",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1440,
        0
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT/api/outbound-schedule/update",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ closeLeadId: $json.closeLeadId, callSequence: $json.callSequence, nextCallAt: $json.nextCallAt, outboundStatus: $json.outboundStatus, lastOutcome: $json.lastOutcome, lastCallSummary: $json.lastCallSummary, lastCallAt: $json.lastCallAt }) }}",
        "options": {}
      },
      "id": "sync_convex_schedule",
      "name": "Sync Convex Schedule",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1632,
        0
      ],
      "onError": "continueRegularOutput"
    },
    {
      "parameters": {
        "respondWith": "json",
        "responseBody": "={{ JSON.stringify({ status: $('Parse Call Data').first().json.isSkipEvent ? 'skipped' : 'processed', outcome: $('Parse Call Data').first().json.outcome || 'skipped' }) }}",
        "options": {}
      },
      "id": "webhook_response",
      "name": "Webhook Response",
      "type": "n8n-nodes-base.respondToWebhook",
      "typeVersion": 1.1,
      "position": [
        1872,
        0
      ]
    },
    {
      "parameters": {
        "content": "## WF-OB-2: Post-Call Handler v4\n\n**Webhook**: POST /webhook/retell-post-call\n**Event Filter**: Skips call_started/call_ringing events\n**6-Call Sequence**: Day 1->3->5->8->12->17\n**Convex Sync**: ALL paths (DNC, Hot Lead, VM, NA, Other)\n**Email Capture**: Stores email on contact when captured\n**ROI Email**: Email-first with SMS fallback for not_interested"
      },
      "id": "sticky_overview",
      "name": "WF-OB-2 Overview",
      "type": "n8n-nodes-base.stickyNote",
      "typeVersion": 1,
      "position": [
        -208,
        -400
      ]
    },
    {
      "id": "prepare_sms",
      "name": "Prepare SMS",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1760,
        800
      ],
      "parameters": {
        "jsCode": "var parsed = $('Parse Call Data').first().json;\nvar outcome = parsed.outcome || '';\nvar webhook = $('Retell Post-Call Webhook').first().json;\nvar call = (webhook.body || webhook).call || {};\nvar dynamicVars = call.retell_llm_dynamic_variables || {};\nvar firstName = dynamicVars.firstName || '';\nvar companyName = dynamicVars.companyName || '';\nvar leadId = parsed.closeLeadId || '';\nvar remotePhone = call.to_number || '';\n\nvar smsType = '';\nif (outcome === 'voicemail_left') {\n  smsType = 'voicemail';\n} else if (outcome === 'hot_lead' || outcome === 'meeting_booked' || outcome === 'interested' || outcome === 'connected' || outcome === 'info_requested' || outcome === 'callback') {\n  smsType = 'post_connect';\n} else {\n  return [{ json: { sendSms: false, leadId: leadId } }];\n}\n\nvar hash = 0;\nfor (var i = 0; i < leadId.length; i++) {\n  hash = ((hash << 5) - hash) + leadId.charCodeAt(i);\n  hash = hash & hash;\n}\nvar variant = Math.abs(hash) % 3;\n\nvar smsText = '';\nif (smsType === 'voicemail') {\n  var co = companyName || 'your office';\n  if (variant === 0) smsText = 'Laura, VoiceSDR - just tried ' + co + '. we handle calls for title offices when the team is busy. worth a look or no?';\n  else if (variant === 1) smsText = 'hey - Laura from VoiceSDR. called ' + co + ' a bit ago. we keep calls from hitting voicemail during closings. interested or not your thing?';\n  else smsText = 'tried reaching ' + co + '. VoiceSDR - we answer for title offices when you can\\'t. if that\\'s useful, I\\'m here. if not, all good';\n} else {\n  var fn = firstName || 'there';\n  if (variant === 0) smsText = fn + ' - Laura from VoiceSDR. good talking to you. here\\'s that link to see how it works: voicesdr.ai. let me know if you have questions';\n  else if (variant === 1) smsText = 'hey ' + fn + ', Laura here. sending over what we talked about. take a look when you get a chance and let me know - voicesdr.ai';\n  else smsText = fn + ' - thanks for the time today. here\\'s the walkthrough I mentioned: voicesdr.ai. I\\'ll follow up Friday unless you beat me to it. talk soon';\n}\n\nreturn [{ json: {\n  sendSms: true,\n  smsType: smsType,\n  smsText: smsText,\n  leadId: leadId,\n  remotePhone: remotePhone\n} }];"
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "get_last_sms",
      "name": "Get Last SMS",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1940,
        800
      ],
      "parameters": {
        "method": "GET",
        "url": "=https://api.close.com/api/v1/activity/sms/?lead_id={{ $json.leadId }}&_order_by=-date_created&_limit=5",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "options": {}
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "sms_cooldown_check",
      "name": "SMS Cooldown Check",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2120,
        800
      ],
      "parameters": {
        "jsCode": "var smsData = $('Prepare SMS').first().json;\nif (!smsData.sendSms) {\n  return [{ json: { shouldSend: false } }];\n}\n\nvar activities = $json.data || [];\nvar now = Date.now();\nvar sixHours = 6 * 60 * 60 * 1000;\nvar shouldSend = true;\nvar lastSmsTime = 0;\n\nfor (var i = 0; i < activities.length; i++) {\n  var sms = activities[i];\n  if (sms.direction !== 'inbound') {\n    var smsTime = new Date(sms.date_created).getTime();\n    if (now - smsTime < sixHours) {\n      shouldSend = false;\n      lastSmsTime = smsTime;\n      break;\n    }\n  }\n}\n\nreturn [{ json: {\n  shouldSend: shouldSend,\n  smsText: smsData.smsText,\n  leadId: smsData.leadId,\n  remotePhone: smsData.remotePhone,\n  smsType: smsData.smsType,\n  lastSmsTime: lastSmsTime\n} }];"
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "should_send_sms",
      "name": "Should Send SMS?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        2300,
        800
      ],
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "cooldown-gate",
              "leftValue": "={{ $json.shouldSend }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "send_followup_sms",
      "name": "Send Follow-up SMS",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2480,
        800
      ],
      "parameters": {
        "method": "POST",
        "url": "https://api.close.com/api/v1/activity/sms/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ lead_id: $json.leadId, local_phone: '+15555550100', remote_phone: $json.remotePhone, text: $json.smsText, status: 'outbox' }) }}",
        "options": {}
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "get_lead_tasks",
      "name": "Get Lead Tasks",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        640,
        -40
      ],
      "parameters": {
        "url": "=https://api.close.com/api/v1/task/?lead_id={{ $('Parse Call Data').first().json.closeLeadId }}&is_complete=false&_type=lead&_limit=10&_fields=id,text,date,is_complete",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "options": {
          "timeout": 10000
        }
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "mark_task_check",
      "name": "Mark Task + Check Pipeline",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        700,
        -40
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "var parsed = $('Parse Call Data').first().json;\nvar tasksResp = $('Get Lead Tasks').first().json;\nvar tasks = tasksResp.data || [];\nvar closeApiKey = 'YOUR_CLOSE_API_KEY';\n\n// 1. Mark first incomplete task as complete\ntasks.sort(function(a, b) { return new Date(a.date) - new Date(b.date); });\nvar targetTask = null;\nfor (var i = 0; i < tasks.length; i++) {\n  if (!tasks[i].is_complete) { targetTask = tasks[i]; break; }\n}\nif (targetTask) {\n  try {\n    await this.helpers.httpRequest({\n      method: 'PUT',\n      url: 'https://api.close.com/api/v1/task/' + targetTask.id + '/',\n      headers: { 'Content-Type': 'application/json' },\n      auth: { username: closeApiKey, password: '' },\n      body: { is_complete: true }\n    });\n  } catch(e) { console.log('Task mark failed: ' + e.message); }\n}\n\n// 2. Check lead pipeline status + previous outcome\nvar isConverted = false;\nvar previousOutcome = '';\ntry {\n  var lead = await this.helpers.httpRequest({\n    method: 'GET',\n    url: 'https://api.close.com/api/v1/lead/' + parsed.closeLeadId + '/?_fields=id,status_id,custom.cf_FLDtrthelKZa2CQvNDFbuS2ymA5W2uncrredxpwYFo8',\n    auth: { username: closeApiKey, password: '' }\n  });\n  var terminalStatuses = [\n    'stat_JkhhQYXcbMQXXixqSBlmq35po9YWwgD0cn2XnNXP8z2',\n    'stat_hil86olYhMqD05K6UK0LInMnRnK06u04is5kACpxIOi',\n    'stat_UTHYC5H4tj4TYMi7ySnfB02rSlhEYdOgWWTFHOfXFRU',\n    'stat_YRcuGl0533SzjVUwsZOjlOpjDoR25szAYhqEgj8V5nE',\n    'stat_dVEwvfGbmvqLbYtX6xmjigkU6DnSw74pnueOMMLLqUa',\n    'stat_OXnpX5vuI6Ujl1AOFHpG4lNsZY5Ek0s9tpzJXIHmw10'\n  ];\n  isConverted = terminalStatuses.indexOf(lead.status_id) !== -1;\n  previousOutcome = (lead.custom || {}).cf_FLDtrthelKZa2CQvNDFbuS2ymA5W2uncrredxpwYFo8 || '';\n} catch(e) { console.log('Pipeline check failed: ' + e.message); }\n\nreturn { json: { taskCompleted: !!targetTask, taskId: targetTask ? targetTask.id : null, isConverted: isConverted, previousOutcome: previousOutcome } };"
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "should_skip_seq",
      "name": "Should Skip Sequence?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        740,
        -40
      ],
      "parameters": {
        "conditions": {
          "options": {
            "version": 2,
            "leftValue": "",
            "caseSensitive": true,
            "typeValidation": "strict"
          },
          "conditions": [
            {
              "id": "skip_check",
              "leftValue": "={{ $json.isConverted }}",
              "rightValue": true,
              "operator": {
                "type": "boolean",
                "operation": "equals"
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      }
    },
    {
      "id": "callback_update_close",
      "name": "Callback - Update Close",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1056,
        700
      ],
      "parameters": {
        "jsonBody": "={\n  \"custom.cf_FLDtrthelKZa2CQvNDFbuS2ymA5W2uncrredxpwYFo8\": \"{{ $('Parse Call Data').first().json.outcome }}\",\n  \"custom.cf_XR3VPy96Ie00Twi24v9VtQUTdZnbWY8jXr2qQdALa38\": \"{{ new Date().toISOString() }}\",\n  \"custom.cf_U9vqoYVuHP0wx0fu9pdQzRYPWsIZSEFNfmIueOnII9v\": \"active\",\n  \"custom.cf_WIIk0f0rAB0qjRl8IFSRV5sL1VhuLQnrybyNEBoqGV1\": {{ $('Parse Call Data').first().json.aiSummary ? JSON.stringify($('Parse Call Data').first().json.aiSummary.substring(0, 500)) : 'null' }}\n}",
        "url": "=https://api.close.com/api/v1/lead/{{ $('Parse Call Data').first().json.closeLeadId }}/"
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "create_callback_task",
      "name": "Create Callback Task",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1280,
        700
      ],
      "parameters": {
        "jsCode": "var parsed = $('Parse Call Data').first().json;\nvar webhook = $('Retell Post-Call Webhook').first().json;\nvar dynamicVars = ((webhook.body || webhook).call || {}).retell_llm_dynamic_variables || {};\nvar closeApiKey = 'YOUR_CLOSE_API_KEY';\nvar companyName = dynamicVars.companyName || 'Unknown';\nvar firstName = dynamicVars.firstName || 'Lead';\nvar callbackTime = parsed.callbackTime || '';\nvar seq = parsed.callSequence || 1;\n\nvar dueDate = new Date();\ndueDate.setDate(dueDate.getDate() + 1);\ndueDate.setHours(9, 0, 0, 0);\nvar dueDateStr = dueDate.toISOString().split('T')[0];\n\nvar taskText = 'CALLBACK REQUESTED - ' + companyName + '\\n\\n';\ntaskText += 'Lead said: ' + (callbackTime || 'call back later') + '\\n';\ntaskText += 'Contact: ' + firstName + '\\n';\ntaskText += 'Previous outcome: ' + parsed.outcome + '\\n';\ntaskText += 'Call #' + parsed.callSequence + '\\n';\nif (parsed.aiSummary) taskText += '\\nSummary: ' + parsed.aiSummary.substring(0, 300);\n\ntry {\n  await this.helpers.httpRequest({\n    method: 'POST',\n    url: 'https://api.close.com/api/v1/task/',\n    headers: { 'Content-Type': 'application/json' },\n    auth: { username: closeApiKey, password: '' },\n    body: {\n      lead_id: parsed.closeLeadId,\n      _type: 'lead',\n      text: taskText,\n      date: dueDateStr,\n      is_complete: false\n    }\n  });\n} catch(e) { console.log('Callback task failed: ' + e.message); }\n\nvar dayGaps = {1: 2, 2: 2, 3: 3, 4: 4, 5: 5};\nvar gap = dayGaps[seq];\nvar nextSeq = seq + 1;\nvar nextCallAt = null;\nvar nextCallAtIso = '';\nvar newStatus = 'active';\n\nif (!gap || nextSeq > 6) {\n  newStatus = 'completed';\n} else {\n  var next = new Date();\n  next.setDate(next.getDate() + gap);\n  if (next.getDay() === 0) next.setDate(next.getDate() + 1);\n  if (next.getDay() === 6) next.setDate(next.getDate() + 2);\n  next.setHours(9, 0, 0, 0);\n  nextCallAt = next.getTime();\n  nextCallAtIso = next.toISOString();\n}\n\ntry {\n  await this.helpers.httpRequest({\n    method: 'PUT',\n    url: 'https://api.close.com/api/v1/lead/' + parsed.closeLeadId + '/',\n    headers: { 'Content-Type': 'application/json' },\n    auth: { username: closeApiKey, password: '' },\n    body: {\n      'custom.cf_L6lj9vbbe1pnuRA2tAAeZs6eso00WI5MI8RNf23Bykn': nextSeq,\n      'custom.cf_t7ney3iqzNMheoFtWFeQi5TJFW31vDaIzV1V6UCEaT8': nextCallAtIso || null\n    }\n  });\n} catch(e) { console.log('Close sequence update failed: ' + e.message); }\n\nreturn { json: {\n  callbackTaskCreated: true,\n  dueDate: dueDateStr,\n  callSequence: nextSeq,\n  nextCallAt: nextCallAt,\n  nextCallAtIso: nextCallAtIso,\n  outboundStatus: newStatus,\n  lastOutcome: parsed.outcome\n} };"
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "callback_sync_convex",
      "name": "Callback - Sync Convex",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1440,
        700
      ],
      "parameters": {
        "jsonBody": "={\n  \"closeLeadId\": \"{{ $('Parse Call Data').first().json.closeLeadId }}\",\n  \"outboundStatus\": \"{{ $('Create Callback Task').first().json.outboundStatus }}\",\n  \"callSequence\": {{ $('Create Callback Task').first().json.callSequence }},\n  \"nextCallAt\": {{ $('Create Callback Task').first().json.nextCallAt || 'null' }},\n  \"lastOutcome\": \"{{ $('Parse Call Data').first().json.outcome }}\",\n  \"lastCallAt\": {{ Date.now() }}\n}",
        "url": "https://YOUR_CONVEX_DEPLOYMENT/api/outbound-schedule/update"
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "force_complete_convex",
      "name": "Force Complete - Sync Convex",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        820,
        80
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "var parsed = $('Parse Call Data').first().json;\nvar closeApiKey = 'YOUR_CLOSE_API_KEY';\n\ntry {\n  await this.helpers.httpRequest({\n    method: 'PUT',\n    url: 'https://api.close.com/api/v1/lead/' + parsed.closeLeadId + '/',\n    headers: { 'Content-Type': 'application/json' },\n    auth: { username: closeApiKey, password: '' },\n    body: {\n      'custom.cf_U9vqoYVuHP0wx0fu9pdQzRYPWsIZSEFNfmIueOnII9v': 'completed',\n      'custom.cf_FLDtrthelKZa2CQvNDFbuS2ymA5W2uncrredxpwYFo8': 'converted_pipeline_check'\n    }\n  });\n} catch(e) { console.log('Close update failed: ' + e.message); }\n\ntry {\n  await this.helpers.httpRequest({\n    method: 'POST',\n    url: 'https://YOUR_CONVEX_DEPLOYMENT/outbound-schedule/update',\n    headers: { 'Content-Type': 'application/json' },\n    body: {\n      closeLeadId: parsed.closeLeadId,\n      outboundStatus: 'completed',\n      lastOutcome: 'converted_pipeline_check',\n      lastCallAt: Date.now()\n    }\n  });\n} catch(e) { console.log('Convex sync failed: ' + e.message); }\n\nreturn { json: { forceCompleted: true, closeLeadId: parsed.closeLeadId } };"
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "queue_blocked_sms",
      "name": "Queue Blocked SMS",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        2480,
        960
      ],
      "parameters": {
        "jsCode": "var data = $('SMS Cooldown Check').first().json;\nvar parsed = $('Parse Call Data').first().json;\nvar webhook = $('Retell Post-Call Webhook').first().json;\nvar call = (webhook.body || webhook).call || {};\nvar dynamicVars = call.retell_llm_dynamic_variables || {};\n\n// Guard: if no SMS data (outcome doesn't warrant SMS), skip queuing\nif (!data.smsText || !data.leadId || !data.remotePhone) {\n  return {json: {skipped: true, reason: 'No SMS data to queue \u2014 outcome does not warrant SMS'}};\n}\n\nvar sixHours = 6 * 60 * 60 * 1000;\nvar nextSendAt = (data.lastSmsTime || Date.now()) + sixHours;\n\nreturn {json: {\n  close_lead_id: data.leadId,\n  phone: data.remotePhone,\n  first_name: dynamicVars.firstName || 'there',\n  company_name: dynamicVars.companyName || '',\n  sms_text: data.smsText,\n  sequence_position: Math.floor(parsed.sequencePosition || 1),\n  next_send_at: nextSendAt,\n  product_slug: 'voicesdr'\n}};"
      }
    },
    {
      "id": "write_sms_queue",
      "name": "Write to SMS Queue",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2660,
        960
      ],
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT/api/sms-queue-add",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify($json) }}",
        "options": {}
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "log_cost_cc",
      "name": "Log Cost to CC",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        416,
        -320
      ],
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT/api/service-expenses/create",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({serviceName: 'Retell Voice', productId: 'voicesdr', date: new Date().toISOString().split('T')[0], amount: Math.round($json.callCostCents || 0) / 100, units: 1, unitType: 'calls', notes: 'Call ' + ($json.retellCallId || 'unknown') + ' - ' + ($json.outcome || 'unknown') + ' (' + ($json.callDurationSeconds || 0) + 's)', source: 'n8n'}) }}",
        "options": {}
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "other_update_seq",
      "name": "Other - Update Close Sequence",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1650,
        624
      ],
      "parameters": {
        "method": "PUT",
        "url": "=https://api.close.com/api/v1/lead/{{ $json.closeLeadId }}/",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpBasicAuth",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "={{ JSON.stringify({ 'custom.cf_L6lj9vbbe1pnuRA2tAAeZs6eso00WI5MI8RNf23Bykn': $json.callSequence, 'custom.cf_t7ney3iqzNMheoFtWFeQi5TJFW31vDaIzV1V6UCEaT8': $json.nextCallAtIso || '', 'custom.cf_U9vqoYVuHP0wx0fu9pdQzRYPWsIZSEFNfmIueOnII9v': $json.outboundStatus }) }}",
        "options": {
          "timeout": 10000
        }
      }
    },
    {
      "id": "push_realtime_metrics",
      "name": "Push Realtime Metrics to CC",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        500,
        -400
      ],
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "var CC = 'https://YOUR_CONVEX_DEPLOYMENT';\nvar productId = 'voicesdr';\nvar now = Date.now();\nvar outcome = $json.outcome || 'other';\n\n// Increment calls_total\ntry {\n  await this.helpers.httpRequest({\n    method: 'POST',\n    url: CC + '/api/metrics/increment',\n    body: { productId: productId, metricKey: 'calls_total', increment: 1, timestamp: now },\n    headers: { 'Content-Type': 'application/json' },\n    json: true\n  });\n} catch(e) {}\n\n// Increment calls_today\ntry {\n  await this.helpers.httpRequest({\n    method: 'POST',\n    url: CC + '/api/metrics/increment',\n    body: { productId: productId, metricKey: 'calls_today', increment: 1, timestamp: now },\n    headers: { 'Content-Type': 'application/json' },\n    json: true\n  });\n} catch(e) {}\n\n// Increment outcome-specific counter\nvar outcomeKey = 'outcome_' + outcome.replace(/[^a-z0-9]/gi, '_').toLowerCase();\ntry {\n  await this.helpers.httpRequest({\n    method: 'POST',\n    url: CC + '/api/metrics/increment',\n    body: { productId: productId, metricKey: outcomeKey, increment: 1, timestamp: now },\n    headers: { 'Content-Type': 'application/json' },\n    json: true\n  });\n} catch(e) {}\n\n// If hot_lead, increment hot_leads_today\nif (outcome === 'hot_lead' || outcome === 'meeting_booked') {\n  try {\n    await this.helpers.httpRequest({\n      method: 'POST',\n      url: CC + '/api/metrics/increment',\n      body: { productId: productId, metricKey: 'hot_leads_today', increment: 1, timestamp: now },\n      headers: { 'Content-Type': 'application/json' },\n      json: true\n    });\n  } catch(e) {}\n}\n\nreturn { json: { metricsIncremented: ['calls_total', 'calls_today', outcomeKey], outcome: outcome } };"
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "log_roi_sms_cost",
      "name": "Log ROI SMS Cost",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        1550,
        560
      ],
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT/api/service-expenses/create",
        "sendBody": true,
        "specifyBody": "json",
        "jsonBody": "{\"serviceName\": \"Close SMS\", \"productId\": \"voicesdr\", \"date\": \"{{ $now.format('yyyy-MM-dd') }}\", \"amount\": 0.02, \"units\": 1, \"unitType\": \"sms\", \"notes\": \"ROI SMS\", \"source\": \"n8n\"}",
        "options": {}
      },
      "onError": "continueRegularOutput"
    },
    {
      "id": "log_followup_sms_cost",
      "name": "Log Follow-up SMS Cost",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        2620,
        800
      ],
      "parameters": {
        "method": "POST",
        "url": "https://YOUR_CONVEX_DEPLOYMENT/api/service-expenses/create",
 
Pro

For the full experience including quality scoring and batch install features for each workflow upgrade to Pro

About this workflow

WF-OB-2: Post-Call Handler. Uses httpRequest, slack. Webhook trigger; 48 nodes.

Source: https://github.com/rafiulislam4246/voice-sdr-title-agent/blob/main/workflows/voice-03-post-call-handler.json — original creator credit. Request a take-down →

More AI & RAG workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

AI & RAG

WF-SDR-TOOLS (Agent Email + Notify). Uses httpRequest, slack. Webhook trigger; 15 nodes.

HTTP Request, Slack
AI & RAG

Multi-Agent Pipeline Orchestrator. Uses httpRequest, executeCommand, slack, readWriteFile. Webhook trigger; 11 nodes.

HTTP Request, Execute Command, Slack +1
AI & RAG

AI Lead Qualification & Routing. Uses httpRequest, slack, airtable. Webhook trigger; 9 nodes.

HTTP Request, Slack, Airtable
AI & RAG

[Tool] HITL Gate - ACTION NAME. Uses slack, httpRequest. Webhook trigger; 8 nodes.

Slack, HTTP Request
AI & RAG

This n8n workflow orchestrates a powerful suite of AI Agents and automations to manage and optimize various aspects of an e-commerce operation, particularly for platforms like Shopify. It leverages La

Google Sheets, HTTP Request, Slack +10