{
  "name": "CSR Phone Call Reviewer - Template",
  "nodes": [
    {
      "parameters": {
        "pollTimes": {
          "item": [
            {
              "mode": "everyMinute"
            }
          ]
        },
        "simple": false,
        "filters": {
          "sender": "ucnotify@pbx.scscommunications.com"
        },
        "options": {}
      },
      "type": "n8n-nodes-base.gmailTrigger",
      "typeVersion": 1.3,
      "position": [
        -112,
        -16
      ],
      "id": "2e7634cd-3804-4665-bfdc-ba5663b3b027",
      "name": "Gmail Trigger"
    },
    {
      "parameters": {
        "jsCode": "const html = $json.html || $json.body || $json.text || \"\";\n\n// 1) Extract all <a href=\"...\"> links\nconst linkRegex = /<a[^>]+href=\"([^\"]+)\"[^>]*>(.*?)<\\/a>/gi;\n\nlet matches = [];\nlet match;\n\nwhile ((match = linkRegex.exec(html)) !== null) {\n  matches.push({\n    url: match[1],\n    text: match[2]\n  });\n}\n\n// 2) Only keep SCS recording links\nconst recordings = matches\n  .filter(m => m.url.includes(\"uc.scscommunications.com/get-shared\"))\n  .map(m => {\n    // Extract metadata from link text\n    // Example text:\n    // \"2398774000 -> 1002 at 01:35:08PM Nov 13 2025 (US/Eastern)\"\n    const text = m.text.trim();\n\n    const caller = text.split(\"->\")[0]?.trim() || \"\";\n    const csr = text.split(\"->\")[1]?.split(\"at\")[0]?.trim() || \"\";\n    const timePart = text.split(\"at\")[1]?.trim() || \"\";\n\n    return {\n      download_url: m.url,\n      caller_number: caller,\n      csr_extension: csr,\n      timestamp: timePart\n    };\n  });\n\nreturn [\n  {\n    json: {\n      recordings\n    }\n  }\n];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        208,
        -16
      ],
      "id": "4f144a3b-c02c-42f4-821b-c27455d3c4e2",
      "name": "Extract Recordings URLs"
    },
    {
      "parameters": {
        "url": "={{ $('Extract Audio Ids').item.json.audio_url }}",
        "options": {
          "allowUnauthorizedCerts": true,
          "response": {
            "response": {
              "responseFormat": "file"
            }
          }
        }
      },
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.3,
      "position": [
        896,
        0
      ],
      "id": "51973e8f-8804-4609-9bac-c7b3f7b59ec7",
      "name": "HTTP Request"
    },
    {
      "parameters": {
        "resource": "audio",
        "operation": "analyze",
        "modelId": {
          "__rl": true,
          "value": "models/gemini-2.5-flash",
          "mode": "list"
        },
        "text": "You are a world-class Call Quality Assurance Manager and an expert call-quality auditor for an appliance repair company.  \n\nYour job is to analyze a phone call transcript and score the CSR’s performance using the scoring matrix below.\nYour output must be strict JSON with no explanation outside of the JSON object.\n\n### SCORING RULES (1–100 scale)\n- 1–20 → Very Poor  \n- 21–40 → Needs Improvement  \n- 41–60 → Average  \n- 61–80 → Good  \n- 81–100 → Excellent  \n\n### CATEGORIES TO SCORE\n1. **Greeting & Professionalism (1–100)**  \n   - Was the greeting friendly?  \n   - Did the CSR state their name?  \n   - Was the tone professional and confident?\n    - Did the agent say:\n“Thank you for calling Appliance GrandMasters, this is ___, how may I help you?”\n    - Did the agent verify/update customer profile information?\n     - Did the agent request the correct info to create or update a dispatch?\n\n2. **Active Listening & Understanding (1–100)**  \n   - Did the CSR listen without interrupting?\n   - Used effective call-control techniques  \n   - Did they understand and acknowledge the customer’s issue?  \n   - Did they use clarifying questions appropriately?\n   - \n\n3. **Helpfulness & Problem Resolution (1–100)**  \n   - Did the CSR provide a clear solution or next step?  \n   - Were answers accurate and helpful?  \n   - Did the CSR take ownership of the customer's issue?\n    - Ensured the customer felt taken care of\n\n4. **Confidence & Product/Service Knowledge (1–100)**  \n   - Did the CSR sound confident?  \n   - Did they demonstrate knowledge about services, scheduling, or troubleshooting?  \n   - Any hesitation, uncertainty, or misinformation?\n\n5. **Closing, Friendliness & Customer Care (1–100)**  \n   - Did the CSR close the call politely?  \n   - Did they make the customer feel supported?  \n   - Was the goodbye friendly and professional?\n   - Maintained a positive brand image\n   - Provided final assistance\n\n### ADDITIONAL OUTPUT\n- **overall_sentiment**: “positive”, “neutral”, or “negative”\n- **overall_score**: Average of all category scores (rounded to nearest whole number)\n- **csr_name**: If mentioned (otherwise return null)\n- **caller_number**: If identifiable (otherwise null)\n- **strengths**: 3–5 bullet points\n- **improvements**: 2–4 bullet points\n- **summary**: 3–6 sentence professional summary of the call\n\n### REQUIRED OUTPUT FORMAT\nReturn ONLY valid JSON in this exact structure:\n\n{\n  \"csr_name\": \"\",\n  \"caller_number\": \"\",\n  \"overall_sentiment\": \"\",\n  \"scores\": {\n    \"greeting_professionalism\": 0,\n    \"listening_understanding\": 0,\n    \"helpfulness_resolution\": 0,\n    \"confidence_knowledge\": 0,\n    \"closing_customer_care\": 0\n  },\n  \"overall_score\": 0,\n  \"strengths\": [],\n  \"improvements\": [],\n  \"summary\": \"\"\n}\n\n",
        "inputType": "binary",
        "options": {}
      },
      "type": "@n8n/n8n-nodes-langchain.googleGemini",
      "typeVersion": 1,
      "position": [
        1344,
        0
      ],
      "id": "b9058304-a493-4a4e-92af-bf2dcf2bfcfd",
      "name": "Analyze audio"
    },
    {
      "parameters": {
        "jsCode": "for (const item of items) {\n  if (item.binary?.data) {\n    // Normalize incorrect MIME type\n    item.binary.data.mimeType = \"audio/wav\";\n\n    // Ensure file extension is correct\n    item.binary.data.fileExtension = \"wav\";\n  }\n}\n\nreturn items;\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        0
      ],
      "id": "7c80f7ac-61fb-47fd-bd83-f1ce34fe59fc",
      "name": "Convert to WAV"
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "YOUR_ID",
          "mode": "list"
        },
        "sheetName": {
          "__rl": true,
          "value": "gid=0",
          "mode": "list"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "Date of Call": "={{ $json.DateofCall }}",
            "Caller Number": "=",
            "Extension": "=",
            "CSR": "={{ $('Extract Date').item.json.csr_name }}",
            "Overall Sentiment": "={{ $('Parse Json').item.json.overall_sentiment }}",
            "Greeting": "={{ $('Parse Json').item.json.scores.greeting_professionalism }}",
            "Listening": "={{ $('Parse Json').item.json.scores.listening_understanding }}",
            "Helpfulness": "={{ $('Parse Json').item.json.scores.helpfulness_resolution }}",
            "Confidence": "={{ $('Parse Json').item.json.scores.confidence_knowledge }}",
            "Overall Score": "={{ $('Parse Json').item.json.overall_score }}",
            "Improvements": "={{ $('Parse Json').item.json.improvements }}",
            "Summary": "={{ $('Parse Json').item.json.summary }}",
            "Strengths": "={{ $('Parse Json').item.json.strengths }}",
            "Closing": "={{ $('Parse Json').item.json.scores.closing_customer_care }}"
          },
          "matchingColumns": [],
          "schema": [
            {
              "id": "Date of Call",
              "displayName": "Date of Call",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "CSR",
              "displayName": "CSR",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Extension",
              "displayName": "Extension",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Caller Number",
              "displayName": "Caller Number",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Overall Sentiment",
              "displayName": "Overall Sentiment",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Greeting",
              "displayName": "Greeting",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Listening",
              "displayName": "Listening",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Helpfulness",
              "displayName": "Helpfulness",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Confidence",
              "displayName": "Confidence",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Closing",
              "displayName": "Closing",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Overall Score",
              "displayName": "Overall Score",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Strengths",
              "displayName": "Strengths",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true,
              "removed": false
            },
            {
              "id": "Improvements",
              "displayName": "Improvements",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            },
            {
              "id": "Summary",
              "displayName": "Summary",
              "required": false,
              "defaultMatch": false,
              "display": true,
              "type": "string",
              "canBeUsedToMatch": true
            }
          ],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {
          "useAppend": true
        }
      },
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4.7,
      "position": [
        2304,
        80
      ],
      "id": "c4af5041-9886-49a0-9ec7-066ad61866f5",
      "name": "Append row in sheet"
    },
    {
      "parameters": {
        "jsCode": "const input = $('Extract Recordings URLs').first().json.recordings[0].timestamp || \"\";\nlet dateText = \"\";\n\ntry {\n  // Remove everything before AM/PM and after the parenthesis\n  // Example input: \"12:53:41PM Nov 13 2025 (US/Eastern)\"\n  dateText = input\n    .replace(/^.*(AM|PM)\\s*/i, \"\")   // strip everything up to AM/PM\n    .replace(/\\s*\\(.*\\).*$/, \"\")     // strip everything after the timezone\n    .trim();                         // clean extra spaces\n} catch (err) {\n  dateText = `[ERROR: ${err.message}] ${input}`;\n}\n\nif (!dateText) {\n  dateText = `[ERROR extracting date] ${input}`;\n}\n\nreturn [{\n  json: {\n    ...$json,\n    \"Date of Call\": dateText\n  }\n}];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1792,
        0
      ],
      "id": "e406a00e-d67d-4873-be81-e4527aaf0954",
      "name": "Extract Date"
    },
    {
      "parameters": {
        "jsCode": "// Get the raw Gemini output (usually inside $json.content.parts[0].text)\nconst raw = $json.content?.parts?.[0]?.text || \"\";\n\n// Extract the JSON inside the ```json ... ``` block  \nlet jsonString = raw;\n\n// Remove markdown fences if present\njsonString = jsonString\n  .replace(/```json/i, \"\")\n  .replace(/```/g, \"\")\n  .trim();\n\nlet parsed;\n\ntry {\n  parsed = JSON.parse(jsonString);\n} catch (err) {\n  return [\n    {\n      json: {\n        success: false,\n        error: \"Failed to parse JSON\",\n        raw_text: raw,\n        jsonString\n      }\n    }\n  ];\n}\n\nreturn [\n  {\n    json: {\n      success: true,\n      ...parsed\n    }\n  }\n];\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1568,
        0
      ],
      "id": "8db9a000-69bd-4a69-a285-151846c23b0a",
      "name": "Parse Json"
    },
    {
      "parameters": {
        "options": {}
      },
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        656,
        -16
      ],
      "id": "00e04819-adc8-46ab-b3fd-1d6febd55c48",
      "name": "Loop Over Items"
    },
    {
      "parameters": {
        "assignments": {
          "assignments": [
            {
              "id": "7f0da315-0e3a-4484-a26a-697183d7cf78",
              "name": "DateofCall",
              "value": "={{ $json['Date of Call'] }}",
              "type": "string"
            }
          ]
        },
        "options": {}
      },
      "type": "n8n-nodes-base.set",
      "typeVersion": 3.4,
      "position": [
        2000,
        0
      ],
      "id": "f906c3ef-5610-4cfc-bef8-edf2c684b726",
      "name": "Edit Fields"
    },
    {
      "parameters": {
        "jsCode": "// input is: items[0].json.recordings (array)\nconst output = [];\n\nfor (const rec of items[0].json.recordings) {\n\n  // Extract access_id from query param\n  const accessId = rec.download_url.split(\"access_id=\")[1];\n\n  // Build the REAL audio playback URL\n  const audioUrl = `https://be.scscommunications.com/api/internal/recording/${accessId}/play_shared`;\n\n  // Optional: verify with fetch (this works in n8n)\n  let success = false;\n  try {\n    const res = await fetch(audioUrl, { method: \"GET\" });\n    success = res.ok;\n  } catch (e) {\n    success = false;\n  }\n\n  output.push({\n    access_id: accessId,\n    audio_url: audioUrl,\n    success\n  });\n}\n\nreturn output.map(o => ({ json: o }));\n"
      },
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        384,
        -16
      ],
      "id": "feb2e0eb-2538-424e-8e23-7517d51c231d",
      "name": "Extract Audio Ids"
    },
    {
      "parameters": {
        "content": "## Step 1 \n**Connect Your Email Account (Trigger Node)**\n\nThis automation starts when a call recording email arrives.\nOpen the Email Trigger node\n\n\nChoose your email provider (Gmail, IMAP, etc.)\n\n\nClick Create New Credential\n\n\nSign in and authorize access\n",
        "height": 592,
        "width": 416
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -304,
        -304
      ],
      "typeVersion": 1,
      "id": "00d337d3-69a5-47f1-ad2c-d0e1065215d2",
      "name": "Sticky Note"
    },
    {
      "parameters": {
        "content": "## Step 2\n**Set Up AI Credentials (OpenAI Node)**\n\nThis is the “listener” part of the automation.\n\n- Open the AI Analysis node\n- Click Create New Credential\n- Paste your OpenAI API key\n\nReview the Call Evaluation Prompt (Optional but Recommended)\n\n\n\n",
        "height": 736,
        "width": 272,
        "color": 5
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        1248,
        -320
      ],
      "typeVersion": 1,
      "id": "297e66ec-91de-45a7-b663-49aaf96af50d",
      "name": "Sticky Note1"
    },
    {
      "parameters": {
        "content": "## Step 3\n**Connect Google Sheets (Logging Results)**\n\nThis is where call reviews are stored.\n\n- Open the Google Sheets node\n- Create a new Google Sheet\n- Copy / Paste the Sheet ID or Choose from list\n\n\nConnect your Google account via credentials\n\n\n",
        "height": 720,
        "width": 368,
        "color": 4
      },
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        2144,
        -336
      ],
      "typeVersion": 1,
      "id": "233d25db-9e88-41e2-8a0d-1b0625d224ab",
      "name": "Sticky Note2"
    }
  ],
  "pinData": {},
  "connections": {
    "Gmail Trigger": {
      "main": [
        [
          {
            "node": "Extract Recordings URLs",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Recordings URLs": {
      "main": [
        [
          {
            "node": "Extract Audio Ids",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "HTTP Request": {
      "main": [
        [
          {
            "node": "Convert to WAV",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Convert to WAV": {
      "main": [
        [
          {
            "node": "Analyze audio",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Analyze audio": {
      "main": [
        [
          {
            "node": "Parse Json",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Date": {
      "main": [
        [
          {
            "node": "Edit Fields",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Json": {
      "main": [
        [
          {
            "node": "Extract Date",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Items": {
      "main": [
        [],
        [
          {
            "node": "HTTP Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Append row in sheet": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Edit Fields": {
      "main": [
        [
          {
            "node": "Append row in sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Extract Audio Ids": {
      "main": [
        [
          {
            "node": "Loop Over Items",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "active": false,
  "settings": {
    "executionOrder": "v1",
    "availableInMCP": false
  },
  "meta": {
    "templateCredsSetupCompleted": false
  }
}