Paste a call transcript, score it against the Quality Audit Guide, then submit straight to the Quality Audits sheet.
Basic identifiers for the record — matches the columns in the Quality Audits sheet.
| Staff Name | |
| Date | |
| Call Type | |
| Canvas Details | |
| Call ID | |
| Auditor |
One small relay script, deployed once, lets this page write finished audits into the Quality Audits sheet.
Updating the existing relay (QA Guide V2 — adds the Update compliance and Script compliance columns):
setupNewParameterColumns and click Run (approve access if asked). It inserts the two new columns after "Closing recap".First-time setup: go to script.google.com → New project, paste the script, then Deploy → New deployment → Web app (Execute as Me, Access Anyone), approve the Google consent screen, and put the resulting URL in the field above. The script matches sheet columns by their heading names, so adding or moving columns won't misalign scores.
function doPost(e) {
var body = JSON.parse(e.postData.contents);
if (body.action === 'submit') return handleSubmit(body.payload);
return json_({error: 'Unknown action'});
}
function doGet(e) {
return json_({status: 'Audit relay is running'});
}
var TARGET_SPREADSHEET_ID = '1y_iwRNLomBeeXM9g6nXzIJwfq-X_xMhrsUQnTjHTjCE';
var TARGET_GID = 2132488623;
// Sheet column header (lower-case) -> field sent by the audit tool.
// Values are placed by HEADER NAME, not column position, so adding or moving
// columns in the sheet can never push scores under the wrong heading.
var COLUMN_FIELDS = {
'staff name': 'staffName',
'date': 'date',
'call type': 'callType',
'canvas details': 'canvasDetails',
'call id': 'callId',
'identity verified (name, dob, address) before sharing details': 'identity',
'safety signal handled': 'safety',
'consent integrity': 'consent',
'introduction + referral reason': 'intro',
'program explained (plain language)': 'program',
'insurance confirmed + cost reassurance-first': 'insurance',
'scheduling completed cleanly': 'scheduling',
'update compliance': 'updateCompliance',
'script compliance': 'scriptCompliance',
'objection handled with the right reframe': 'objection',
'accuracy, tone / rapport': 'tone',
'contacted within sla': 'sla',
'canvas documentation accurate': 'canvas',
'consent/portal actually sent': 'consent_sent',
'notes': 'notes',
'final score': 'finalScore',
'auditor': 'auditor'
};
var MONTH_NAMES = ['January', 'February', 'March', 'April', 'May', 'June', 'July',
'August', 'September', 'October', 'November', 'December'];
function getTargetSheet_() {
var ss = SpreadsheetApp.openById(TARGET_SPREADSHEET_ID);
var sheets = ss.getSheets();
for (var i = 0; i < sheets.length; i++) {
if (sheets[i].getSheetId() === TARGET_GID) return sheets[i];
}
throw new Error('Tab with gid ' + TARGET_GID + ' not found in the target spreadsheet.');
}
function norm_(s) {
return String(s).replace(/\s+/g, ' ').trim().toLowerCase();
}
function fieldForHeader_(h) {
var n = norm_(h);
if (COLUMN_FIELDS[n]) return COLUMN_FIELDS[n];
if (n.indexOf('consent + portal + save number') === 0) return 'closing';
return '';
}
function monthName_(dateStr) {
var m = String(dateStr || '').match(/^(\d{4})-(\d{2})/);
return m ? MONTH_NAMES[Number(m[2]) - 1] : '';
}
function findHeaderRow_(sh) {
var top = sh.getRange(1, 1, Math.min(10, sh.getLastRow()), 1).getValues();
for (var r = 0; r < top.length; r++) {
if (norm_(top[r][0]) === 'staff name') return r + 1;
}
return 0;
}
function handleSubmit(payload) {
var sh = getTargetSheet_();
var headerRow = findHeaderRow_(sh);
if (!headerRow) throw new Error('Could not find the header row (a row whose first cell is "Staff Name").');
var headers = sh.getRange(headerRow, 1, 1, sh.getLastColumn()).getValues()[0];
var lastUsed = headers.length;
while (lastUsed > 0 && String(headers[lastUsed - 1]).trim() === '') lastUsed--;
var placed = {};
var row = [];
for (var c = 0; c < lastUsed; c++) {
var h = headers[c];
if (norm_(h) === 'month') { row.push(monthName_(payload.date)); continue; }
var field = fieldForHeader_(h);
if (!field) { row.push(''); continue; }
placed[field] = true;
var v = payload[field];
row.push(v === undefined || v === null ? '' : v);
}
sh.appendRow(row);
// Anything the tool sent that has no matching column in the sheet is reported back.
var unplaced = Object.keys(payload).filter(function (k) { return !placed[k]; });
return json_({status: 'ok', unplacedFields: unplaced});
}
// ONE-TIME: run this once from the Apps Script editor (select it in the function
// dropdown, then press Run). It adds the two QA Guide V2 columns - Update compliance
// and Script compliance - right after the "Consent + portal + save number; Closing
// recap" column. Safe to run twice: it does nothing if the columns already exist.
function setupNewParameterColumns() {
var sh = getTargetSheet_();
var headerRow = findHeaderRow_(sh);
if (!headerRow) throw new Error('Could not find the header row (a row whose first cell is "Staff Name").');
var headers = sh.getRange(headerRow, 1, 1, sh.getLastColumn()).getValues()[0].map(norm_);
if (headers.indexOf('update compliance') !== -1 || headers.indexOf('script compliance') !== -1) {
Logger.log('Already set up - nothing changed.');
return 'Already set up - nothing changed.';
}
var closing = 0;
for (var i = 0; i < headers.length; i++) {
if (headers[i].indexOf('consent + portal + save number') === 0) { closing = i + 1; break; }
}
var objection = headers.indexOf('objection handled with the right reframe') + 1;
if (!closing || !objection) throw new Error('Could not find the "Closing recap" and "Objection" columns - no changes made.');
sh.insertColumnsAfter(closing, 2);
var donor = objection + 2; // the Objection column after the shift
var rowsBelow = sh.getMaxRows() - headerRow + 1;
var width = sh.getColumnWidth(donor);
for (var k = 1; k <= 2; k++) {
var col = closing + k;
// header row + all data rows: copy look & number format from the Objection column
sh.getRange(headerRow, donor, rowsBelow, 1)
.copyTo(sh.getRange(headerRow, col, rowsBelow, 1), SpreadsheetApp.CopyPasteType.PASTE_FORMAT, false);
// group-heading row above (Situational is a single, unmerged cell - a safe style donor)
if (headerRow > 1) {
sh.getRange(headerRow - 1, donor, 1, 1)
.copyTo(sh.getRange(headerRow - 1, col, 1, 1), SpreadsheetApp.CopyPasteType.PASTE_FORMAT, false);
}
sh.setColumnWidth(col, width);
}
sh.getRange(headerRow, closing + 1, 1, 2).setValues([['Update compliance', 'Script compliance']]);
if (headerRow > 1) {
sh.getRange(headerRow - 1, closing + 1, 1, 2).setValues([['Process Update', 'Script Consistency']]);
}
SpreadsheetApp.flush();
Logger.log('Added "Update compliance" and "Script compliance" after column ' + closing + '.');
return 'Done.';
}
function json_(obj) {
return ContentService.createTextOutput(JSON.stringify(obj)).setMimeType(ContentService.MimeType.JSON);
}
Lists your Aircall calls for a date range and sorts them using the tags your team puts on calls in Aircall (Enrolled / Successful / Declined, plus New Referral / Proactive Outreach / Reschedule). Click Audit on a call to load it into the form below.
Put the Aircall link (or call ID) in Call ID above and click Import transcript from Aircall — the transcript, staff name, date and call direction fill in automatically. Then click "Copy audit prompt," paste it into claude.ai, and paste Claude's reply back below. No API key needed. If Aircall has no transcript for a call, paste it into the box by hand. Every result is editable.
| Tiered Scoring Framework | Quality Metrics | Scores |
|---|---|---|
| Green Zone (≥ 90%) → Excellent quality call | Critical Guardrail | — |
| Yellow Zone (85–89%) → Acceptable but improvable | Core Execution | — |
| Red Zone (< 85%) → Needs coaching or corrective action | Process/Compliance | — |
| Total Score | — |