Te recomendamos revisar las conversaciones gestionadas por Agente IA después de activarlo, pero la revisión manual se vuelve demasiado lenta a escala. Esta guía muestra cómo automatizar el control de calidad de conversaciones usando n8n + un modelo de IA, para que cada conversación cerrada se revise automáticamente y se registre en Google Sheets para informes.
En resumen, este flujo de trabajo de n8n se dispara cada vez que se cierra una conversación. Una vez disparado, llama al List Messages API para obtener los últimos 50 mensajes (con opción de obtener hasta 100 usando paginación), luego pasa la transcripción a un modelo de IA para el procesamiento de QA — como análisis de sentimiento, puntaje de engagement y comprobaciones de resolución. Los resultados se registran en Google Sheets para que puedas buscar fácilmente cualquier conversación por ID de contacto.
Lo que vas a construir
Una automatización que:
Se dispara cuando se cierra una conversación
Recupera los últimos 50 mensajes usando el List Messages API (opcionalmente hasta 100 con paginación)
Limpia y formatea los mensajes en una transcripción
Envía la transcripción a un modelo de IA para evaluación de QA
Analiza la salida de la IA de forma segura (contrato solo JSON)
Añade los resultados a Google Sheets (una fila por conversación)
Requisitos
Herramientas
Plataforma de automatización: n8n
API para Desarrolladores de Respond.io — List Messages endpoint
API de IA — OpenAI, Anthropic, Gemini o cualquier proveedor soportado por n8n
Informes / almacenamiento — Google Sheets (o tu herramienta de almacenamiento preferida)
Credenciales
Clave de la API para Desarrolladores de Respond.io
Clave API del proveedor de IA (OpenAI, Anthropic, Gemini, etc.)
Credencial de Google Sheets en n8n (OAuth o cuenta de servicio)
Antes de empezar
1) Crea un Google Sheet para los resultados de QA
Crea una hoja de cálculo y añade una pestaña (p. ej. QA_Results) con estos encabezados:
timestampcontact_idopened_atchanneloverall_scoreresolvedcustomer_sentimentreasoningengagement_score
2) Confirma los campos del payload del disparador
Desde la salida del Conversation closed trigger, identifica los nombres exactos de los campos para:
ID de conversación
ID de contacto
Marca de tiempo de apertura de la conversación
Marca de tiempo de cierre de la conversación (opcional)
Los nombres de campo varían según la implementación. En los nodos de código abajo, verás marcadores como
trigger.conversationId. Actualiza esos mapeos para que coincidan con la salida de tu disparador.
Guía paso a paso
1. Disparador: Conversación cerrada
Cuando se cierre una conversación en respond.io, queremos activar automáticamente el flujo de trabajo de n8n. Así te aseguras de que cada conversación completada pase por QA — sin revisión manual necesaria.
Nodo n8n:Conversation closed trigger
En n8n, añade el nodo trigger de respond.io.
Elige Conversation Closed.
Conecta las credenciales de la API para Desarrolladores de Respond.io. Aprende a configurarlo en n8n con nuestra guía de integración.
Este disparador asegura que solo revises conversaciones después de que hayan terminado.
2. Obtener mensajes de la conversación
En este paso, obtendrás los últimos 50 mensajes entrantes y salientes usando el List Messages API. Esto suele ser suficiente para una revisión de QA completa. Si necesitas más contexto, opcionalmente puedes obtener otros 50 mensajes (hasta 100 en total) usando paginación.
Nodos de n8n:
get 1st 50 messages(HTTP Request)Is there a second page?(IF) — opcionalget 2nd 50 messages(HTTP Request) — opcional
2.1 Obtener los primeros 50 mensajes
Node:get 1st 50 messages
Selecciona Core > HTTP Request
Método:
GETURL: tu endpoint de List Messages
URL de ejemplo (reemplázala con la URL base de tu API + formato del endpoint):
https://api.respond.io/v2/contact/{{identifier}}/message/list
Donde identifier debe reemplazarse por uno de los siguientes formatos: id:<contactID>, phone:+<countryCodeAndPhone>, o email:<contactEmail>
Activa Send Query Parameters > Using Fields Below, luego añade:
Nombre:
limitValor:
50
Si tu API lo soporta, establece el orden de más antiguo → más nuevo. Ejemplo: sort=asc.
Activa Send Headers > Using Fields Below, luego añade:
Nombre:
AcceptValor:
application/json
Nombre:
AuthorizationValor:
Bearer <your_respond_api_key>
Si la respuesta de List Messages incluye
pagination.next, puedes usarla directamente para obtener la segunda página.
2.2 Verificar y obtener más de 50 mensajes (Opcional)
Si 50 mensajes no son suficientes para tus necesidades de QA, puedes obtener una segunda página de 50 mensajes. Añade un nodo IF para comprobar si hay más mensajes disponibles.
Node:Is there a second page? (IF)
Valor izquierdo (Expresión):
{{ $json.pagination.next }}Operador: is not empty
Si es verdadero, significa que hay más mensajes para obtener. Si es falso, el flujo de trabajo continúa sin obtener más — esto evita que falle cuando no hay segunda página. El nodo Fusionar aguas abajo esperará a que completen ambas rutas, así que el flujo de trabajo seguirá funcionando sin problemas en cualquier caso.
2.3 Obtener los siguientes 50 mensajes (Opcional)
Si el nodo IF pasa (existe una segunda página), obtén el siguiente lote.
Node:get 2nd 50 messages
Añade otro nodo HTTP Request.
Configura:
Método:
GETURL:
{{ $json.pagination.next }}
Añade los mismos headers:
Accept:
application/jsonAuthorization:
Bearer <your_respond_api_key>
3. Fusionar páginas de mensajes (Opcional)
Este paso solo es necesario si estás obteniendo más de 50 mensajes. Si decidiste no obtener una segunda página, puedes omitir este nodo y conectar
get 1st 50 messagesdirectamente al siguiente paso (Clean + keep messages since last open).
El nodo Fusionar combina los primeros 50 mensajes con los segundos 50 en una sola lista. Sin él, el flujo de trabajo no puede procesar juntas dos respuestas de API separadas.
n8n node:Merge
Añade Flow > Merge.
Configura el Modo a Append.
Establece Número de entradas:
2Conecta:
Entrada 1:
get 1st 50 messagesEntrada 2:
get 2nd 50 messages
4. Limpiar y filtrar mensajes
La respuesta de la API sigue siendo cruda y contiene mucha información extra que la IA no necesita. Este paso la limpia — elimina mensajes antiguos, normaliza las etiquetas de los remitentes y organiza todo en una lista simple, lista para el procesamiento por la IA. Todo lo que necesitas hacer es añadir el nodo Code y copiar/pegar el JavaScript abajo.
n8n node:Clean + keep messages since last convo open
Para esto: Añade nodo → Selecciona Core → Code → Code en JavaScript.
Este código hará:
Combinar mensajes de ambas páginas
Filtrar mensajes enviados antes de que se abriera la conversación
Ordenar mensajes de más antiguo → más nuevo
Normalizar remitentes en etiquetas consistentes:
Contacto,Agente IA,Agente humano,Flujo de trabajoAñadir números de índice de mensaje
Devolver una lista limpia y estructurada para el paso de transcripción
Notas: Este ejemplo filtra usando
messageIdcomparado con la marca de apertura convertida a microsegundos. Si tu API provee marcascreatedAten su lugar, filtra porcreatedAten vez demessageId. Asegúrate de que tuconversation_open_timestampincluya zona horaria. Si no la incluye, ajústala a la zona horaria de tu espacio de trabajo antes de parsearla.
Pega esto en el nodo Code:
// n8n Code node AFTER Merge (Run Once for All Items)
// Incoming items are the API responses from page1 and (optionally) page2.
function unwrapRespondList(json) {
if (Array.isArray(json) && json.length && json[0]?.items) return json[0];
if (json?.items) return json;
if (Array.isArray(json)) return { items: json, pagination: {} };
return { items: [], pagination: {} };
}
function toMicroseconds(ts) {
if (!ts || typeof ts !== "string") return null;
// Prefer timestamps that already contain timezone info.
// If your timestamp lacks timezone, add it upstream (recommended) rather than hardcoding here.
const iso = ts.includes("T") ? ts : ts.replace(" ", "T");
const ms = Date.parse(iso);
if (Number.isNaN(ms)) return null;
return ms * 1000; // microseconds
}
function normalizeSender(source) {
if (!source) return "Unknown";
const s = String(source).toLowerCase();
if (s === "contact") return "Contact";
if (s.includes("ai")) return "AI Agent";
if (s.includes("workflow") || s.includes("automation")) return "Workflow";
if (s.includes("user") || s.includes("agent")) return "Human Agent";
return source;
}
// Read from the Conversation closed trigger
const trigger = $("Conversation closed trigger").first().json;
const body = trigger.body ?? trigger;
const openedTs =
body.conversation_open_timestamp ||
body.conversation_opened_timestamp ||
body.conversationOpenedAt ||
body["conversation_open_timestamp "] ||
null;
const openMicro = toMicroseconds(openedTs);
// Merge items from all incoming API payloads (1 or 2 pages)
let mergedItems = [];
for (const item of $input.all()) {
const unwrapped = unwrapRespondList(item.json);
if (Array.isArray(unwrapped.items)) mergedItems.push(...unwrapped.items);
}
// Filter messages after conversation opened timestamp
if (openMicro !== null) {
mergedItems = mergedItems.filter((m) => {
const idNum = Number(m?.messageId);
return Number.isFinite(idNum) && idNum > openMicro;
});
}
// Sort oldest -> newest
mergedItems.sort((a, b) => Number(a?.messageId ?? 0) - Number(b?.messageId ?? 0));
// Index + shape
const indexed = mergedItems.map((m, i) => {
const traffic = m?.traffic ?? null;
const sender = m?.sender || {};
// Incoming traffic is always from Contact
if (traffic === "incoming") {
return {
Index: i + 1,
traffic,
message: m?.message ?? null,
Sender: { source: "Contact" },
};
}
return {
Index: i + 1,
traffic,
message: m?.message ?? null,
Sender: {
source: normalizeSender(sender?.source),
userId: sender?.userId ?? null,
teamId: sender?.teamId ?? null,
},
};
});
return [
{
json: {
message_count: indexed.length,
items: indexed,
meta: {
conversation_open_timestamp: openedTs,
conversation_open_microseconds: openMicro,
pages_received: $input.all().length,
},
},
},
];Asegúrate de establecer Mode = Run Once for All Items y Language = JavaScript.
El script anterior es una referencia. Tu espacio de trabajo puede devolver campos o estructuras diferentes. Para construir la lógica correcta de parseo JSON para tu configuración, copia la salida del nodo anterior, pégala en una herramienta de IA (p. ej., ChatGPT o Claude) junto con el script de referencia arriba, y describe el formato de salida que necesitas. La IA puede entonces adaptar el script para que coincida con tus datos reales.
5. Generar transcripción
Este paso convierte la lista limpia de mensajes en una transcripción estilo Markdown para el procesamiento por IA. Las transcripciones estructuradas mejoran la comprensión de la IA y reducen las alucinaciones.
n8n node:Build markdown transcript (Core → Code → JavaScript)
Pega esto en el nodo Code:
// Minimal parse of possibly-escaped JSON string
function parseMaybeEscapedJSON(raw) {
if (raw == null) return null;
if (typeof raw === 'object') return raw;
let s = String(raw).trim();
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith('"') && s.endsWith('"'))) s = s.slice(1, -1);
s = s.replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\r/g, '\r').replace(/\\\\/g, '\\');
return JSON.parse(s);
}
// Flatten nested objects/arrays into k1_k2_0_k3 style keys
// Primitive-only arrays are joined as comma-delimited strings
function flatten(obj, prefix = '', out = {}) {
if (obj == null) return out;
const makeKey = (k) => (prefix ? `${prefix}_${k}` : String(k));
if (Array.isArray(obj)) {
// If every element is a primitive, join as comma-delimited string
if (obj.every(v => v == null || typeof v !== 'object')) {
out[prefix || 'value'] = obj.join(', ');
return out;
}
obj.forEach((v, i) => {
const k = makeKey(i);
(v && typeof v === 'object') ? flatten(v, k, out) : out[k] = v;
});
return out;
}
if (typeof obj === 'object') {
for (const [k, v] of Object.entries(obj)) {
const key = makeKey(k);
(v && typeof v === 'object') ? flatten(v, key, out) : out[key] = v;
}
return out;
}
out[prefix || 'value'] = obj;
return out;
}
const parsed = [];
for (const item of items) {
try {
const obj = parseMaybeEscapedJSON(item.json?.Output);
if (!obj || typeof obj !== 'object') {
parsed.push({ json: { error: 'Parsed Output is not an object', raw: item.json?.Output ?? null } });
continue;
}
const out = flatten(obj);
parsed.push({ json: out });
} catch (e) {
parsed.push({ json: { error: 'Failed to parse Output', message: e?.message || String(e), raw: item.json?.Output ?? null } });
}
}
return parsed; `${prefix}_${k}` : String(k));
if (Array.isArray(obj)) {
// If every element is a primitive, join as comma-delimited string
if (obj.every(v => v == null || typeof v !== 'object')) {
out[prefix || 'value'] = obj.join(', ');
return out;
}
obj.forEach((v, i) => {
const k = makeKey(i);
(v && typeof v === 'object') ? flatten(v, k, out) : out[k] = v;
});
return out;
}
if (typeof obj === 'object') {
for (const [k, v] of Object.entries(obj)) {
const key = makeKey(k);
(v && typeof v === 'object') ? flatten(v, key, out) : out[key] = v;
}
return out;
}
out[prefix || 'value'] = obj;
return out;
}
const parsed = [];
for (const item of items) {
try {
const obj = parseMaybeEscapedJSON(item.json?.Output);
if (!obj || typeof obj !== 'object') {
parsed.push({ json: { error: 'Parsed Output is not an object', raw: item.json?.Output ?? null } });
continue;
}
const out = flatten(obj);
parsed.push({ json: out });
} catch (e) {
parsed.push({ json: { error: 'Failed to parse Output', message: e?.message || String(e), raw: item.json?.Output ?? null } });
}
}
return parsed; null } });
continue;
}
const out = flatten(obj);
parsed.push({ json: out });
} catch (e) {
parsed.push({ json: { error: 'Failed to parse Output', message: e?.message || String(e), raw: item.json?.Output ?? null } });
}
}
return parsed; null } });
}
}
return parsed;Asegúrate de establecer Mode = Run Once for All Items y Language = JavaScript.
6. Revisiones de conversaciones con IA
Este nodo envía la transcripción a un modelo de IA (p. ej., OpenAI 5.4) y devuelve una respuesta JSON estructurada.
Cómo configurarlo
Selecciona las credenciales de tu proveedor de IA. En este ejemplo, usaremos OpenAI.
Resource: Enviar mensaje a un modelo
Operation: Enviar mensaje a un asistente
Mensajes:
Tipo: Texto
Rol: Usuario
Prompt:
{{ $json.transcript }}
Simplificar salida: Activar
Añadir opción:
Instrucciones - Esta es solo una plantilla de ejemplo, pero puedes introducirla en otra IA (p. ej. ChatGPT, Claude, etc.) para editarla según tus necesidades:
Eres un analista de calidad de conversaciones para respond.io, una plataforma de mensajería para clientes. Tu trabajo es revisar conversaciones de soporte o ventas y producir puntajes de calidad precisos y consistentes.
Recibirás una transcripción de la conversación como una lista numerada de mensajes. Evalúala usando el proceso de razonamiento estructurado abajo antes de producir tu salida final.
---
## INPUT FORMAT
Each message in the transcript contains:
- A message number (e.g. `1`, `2`, `3`)
- `traffic:incoming` — message FROM the customer
- `traffic:outgoing` — message TO the customer
- `text:` — the message content
- `sender source:` — who sent it:
- `Contact` → customer
- `ai_agent` → AI Agent (automated — evaluate for quality)
- `workflow` → system automation (do NOT score as agent engagement)
- `user` → human agent (note the handover point)
When evaluating, treat `ai_agent` and `user` messages together as "the agent side." Track when a handover from AI to human occurred, as this affects resolution and engagement scoring. Ignora los mensajes `workflow` al puntuar engagement — son respuestas automáticas del sistema, no decisiones de juicio. La conversación puede estar en cualquier idioma. Evalúa sentimiento y contenido con precisión sin importar el idioma.
---
## PASO 1 — CADENA DE PENSAMIENTO (Razonamiento Interno)
Antes de producir cualquier puntuación, razona sobre la conversación paso a paso en este orden exacto: Identifica la intención principal del cliente.
¿Qué quería conseguir el cliente? Exprésalo en una sola oración.
**2. Traza la estructura de la conversación.
¿Quién gestionó la conversación — solo Agente IA, o hubo una transferencia a un humano? ¿En qué punto? ¿Participaron varios agentes humanos? ¿Hubo mensajes de flujo de trabajo que pudieron haber confundido al cliente?
**3. Traza la resolución.
¿El agente realmente atendió la intención principal del cliente? ¿El cliente confirmó que se resolvió? ¿La conversación terminó con el problema sin resolver, con una evasiva o con una transferencia adecuada?
**4. Rastrea el sentimiento.
¿Cómo comenzó emocionalmente el cliente? ¿Cómo terminó? Busca:
- Indicadores de frustración: preguntas repetidas, correcciones, respuestas cortas/abruptas
- Indicadores de satisfacción: "gracias", "entendido", "perfecto", cierres positivos
- Indicadores neutrales: transaccional, cooperativo, sin señales fuertes
Pesa más el tono final del cliente que el inicial.
**5. Evalúa el compromiso del agente.
Califica al agente según:
- ¿Reconoció la situación del cliente antes de proponer una solución?
- ¿Personalizó la respuesta usando el contexto de la conversación anterior?
- ¿Hizo preguntas aclaratorias inteligentes o hizo suposiciones?
- Si un humano tomó el control, ¿mantuvo la continuidad desde la IA o empezó desde cero?
- ¿Fueron las respuestas en el mismo idioma que el cliente?
- NO puntúes los mensajes `workflow` como engagement.
**6. Identifica fallas graves.
Comprueba explícitamente lo siguiente:
- ¿Repetió el cliente su pregunta principal sin que fuera reconocida?
- ¿Proporcionó el agente información factualmente incorrecta?
- ¿Ignoró el agente una parte clave del mensaje del cliente?
- ¿La conversación terminó sin resolución o sin pasos claros a seguir?
- ¿Hubo un tono poco profesional o inapropiado?
- ¿Hubo retrasos excesivos en las respuestas?
- ¿La ruta de escalación fue incorrecta o innecesaria?
- ¿Un agente humano no retomó el contexto de la transferencia desde la IA?
- ¿El agente respondió en un idioma diferente al del cliente?
**7. Redacta tu razonamiento.
En 2–4 oraciones, resume: qué sucedió, qué hizo bien el agente y qué falló. Sé específico — cita momentos reales o números de mensaje.
---
## PASO 2 — PUNTUACIÓN INICIAL
Basado en tu razonamiento del Paso 1, produce un borrador inicial de todas las puntuaciones:
- **`overall_score`** (entero, 1–10): Calidad holística de la conversación. Pesa la resolución, el sentimiento y la participación en conjunto. Las fallas graves limitan automáticamente la puntuación a 5.
- **`resolved`** (enum): Si el problema del cliente fue cerrado. NO marques `resolved` solo porque el agente envió un mensaje final. Requiere confirmación del cliente o una transferencia claramente completada.
- **`customer_sentiment`** (enum): El tono emocional dominante del cliente, ponderado hacia cómo terminó la conversación.
- **`engagement_score`** (entero, 1–10): Qué tan bien se comunicó el agente — personalización, empatía, claridad, concordancia de idioma y continuidad en las transferencias.
- **`reasoning`** (cadena): Tu resumen de 2–4 oraciones del Paso 1.
- **`flags`** (array): Lista de fallas graves identificadas. Array vacío si no hay ninguna.
---
## PASO 3 — CADENA DE VERIFICACIÓN
Antes de finalizar, realiza cada una de las siguientes comprobaciones y respóndelas explícitamente: Si se infiere, ¿`resolved` debería ser `"unresolved"` en su lugar?
**Comprobación 2 — Sentimiento:**
¿Estoy valorando el sentimiento según el tono final del cliente y no solo por su frustración inicial? ¿Mejoró su estado de ánimo, se mantuvo igual o empeoró?
**Comprobación 3 — Engagement:**
¿Resistiría este `engagement_score` la revisión de un gerente de QA? ¿Excluí correctamente los mensajes `workflow` del puntaje de engagement?
**Comprobación 4 — Calidad de la transferencia:**
Si un agente humano tomó el relevo, ¿mantuvo la continuidad — o reinició desde cero, ignoró el contexto o respondió en el idioma equivocado? Ajusta el `engagement_score` en consecuencia.
**Comprobación 5 — Consistencia de puntuaciones:**
- `resolved: unresolved` + `customer_sentiment: negative` → `overall_score` debe ser ≤ 4
- `resolved: resolved` + `customer_sentiment: positive` → `overall_score` debe ser ≥ 6
- Cualquier flag presente → `overall_score` debe ser ≤ 5
**Comprobación 6 — Exhaustividad de flags:**
Revisa nuevamente la lista completa de fallas graves. ¿Me faltó algo?
Si alguna comprobación falla, revisa la puntuación correspondiente antes de continuar.
---
## PASO 4 — RESULTADO FINAL
Entrega tus puntuaciones finales solo en el formato JSON requerido. No incluyas tu razonamiento interno de los Pasos 1–3. Solo el campo `reasoning` debe aparecer en la salida.
---
## REFERENCIA DE PUNTUACIÓN
### overall_score
| Score | Significado |
|-------|---------|
| 9–10 | Excelente — resuelto eficientemente, el cliente terminó positivo, el agente fue empático y personalizado |
| 7–8 | Bueno — resuelto con fricción menor, sentimiento neutral a positivo |
| 5–6 | Regular — parcialmente resuelto, o alto engagement pero resultado pobre |
| 3–4 | Malo — no resuelto, sentimiento negativo o claros fallos de engagement |
| 1–2 | Falla crítica — información errónea dada, cliente se fue enojado o conversación abandonada |
### engagement_score
| Score | Significado |
|-------|---------|
| 9–10 | Muy personalizado, empático, eficiente, proactivo, uso correcto del idioma en todo momento |
| 7–8 | Sólido — abordó el problema bien con cierto toque personal |
| 5–6 | Funcional pero robótico — técnicamente correcto, sin calidez ni personalización |
| 3–4 | Comunicación pobre — se hicieron suposiciones, preguntas ignoradas o handover sin continuidad |
| 1–2 | Dañino — grosero, desdeñoso, idioma incorrecto usado o totalmente fuera de tema |Requerido: salida JSON estricta
Tus instrucciones de asistente deben requerir un JSON Schema.
Formato de salida recomendado:
Tipo: JSON Schema (recomendado)
Nombre: Conversation QA Schema
Estricto: Activado
Esquema: Este es únicamente un esquema de ejemplo para devolver el puntaje de calidad de la conversación, el estado del caso, el sentimiento del cliente, el puntaje de engagement, el razonamiento detrás del resultado y las banderas de fallo de la conversación. Recuerda editarlo para que se ajuste a tus necesidades.
{
"type": "json_schema",
"json_schema": {
"name": "conversation_review",
"strict": true,
"schema": {
"type": "object",
"properties": {
"overall_score": {
"type": "integer",
"description": "Puntuación holística de la calidad de la conversación. 1 = falla crítica, 10 = excelente. Cualquier flag presente limita esto a 5."
},
"resolved": {
"type": "string",
"enum": ["resolved", "unresolved", "escalated"],
"description": "Si se cerró el problema principal del cliente. 'resolved' requiere confirmación explícita del cliente; 'escalated' indica una transferencia intencional y exitosa a un agente humano; 'unresolved' significa que la conversación terminó sin cierre."
},
"customer_sentiment": {
"type": "string",
"enum": ["positive", "neutral", "negative"],
"description": "El tono emocional dominante del cliente, ponderado hacia cómo terminó la conversación. 'positive' = satisfecho/agradecido; 'neutral' = cooperativo/transaccional; 'negative' = frustrado o insatisfecho."
},
"engagement_score": {
"type": "integer",
"description": "Qué tan bien se comunicó el agente — personalización, empatía, claridad, coincidencia de idioma y continuidad en las transferencias. Excluye mensajes 'workflow'."
}
}
}
}
} 1 = falla crítica, 10 = excelente. Cualquier flag presente limita esto a 5.
},
"resolved": {
"type": "string",
"enum": ["resolved", "unresolved", "escalated"],
"description": "Si se cerró el problema principal del cliente. 'resolved' requiere confirmación explícita del cliente; 'escalated' indica una transferencia intencional y exitosa a un agente humano; 'unresolved' significa que la conversación terminó sin cierre."
},
"customer_sentiment": {
"type": "string",
"enum": ["positive", "neutral", "negative"],
"description": "El tono emocional dominante del cliente, ponderado hacia cómo terminó la conversación. 'positive' = satisfecho/agradecido; 'neutral' = cooperativo/transaccional; 'negative' = frustrado o insatisfecho."
},
"engagement_score": {
"type": "integer",
"description": "Qué tan bien se comunicó el agente — personalización, empatía, claridad, coincidencia de idioma y continuidad en las transferencias. Excluye mensajes 'workflow'."
} 'resolved' requiere confirmación explícita del cliente. 'escalated' indica una transferencia intencional y exitosa a un agente humano. 'unresolved' significa que la conversación terminó sin cierre. 'positive' = satisfecho o agradecido. 'neutral' = cooperativo y transaccional. 'negative' = frustrado, repetitivo o insatisfecho. Excluye los mensajes del flujo de trabajo. 1 = dañino o desdeñoso, 10 = muy personalizado y proactivo. Debe referenciar momentos específicos de la conversación. Incluye lo que el agente hizo bien, lo que falló y qué motivó las puntuaciones finales. No hagas generalizaciones. Array vacío si ninguno aplica.Después de este nodo, conéctalo a un nodo Map Output y continúa al paso 7.
7. Analizar y normalizar la salida de la IA
Este paso evita que respuestas de IA malformadas o parciales rompan tu flujo de trabajo.
Nodos de n8n:
Map Output(Data Transformation → Edit Fields / Set)Parse Output(Core → Code → JavaScript)
7.1 Map Output
Node:Map Output
Modo: Mapeo manual
Crea un campo llamado
Outputy mapea la respuesta de la IA allí.
Esto hace que el siguiente nodo sea consistente (siempre puede leer $json.Output).
7.2 Analizar la salida de forma segura
Node:Parse Output
Pega esto en el nodo Code:
// Minimal parse of possibly-escaped JSON string
function parseMaybeEscapedJSON(raw) {
if (raw == null) return null;
if (typeof raw === 'object') return raw;
let s = String(raw).trim();
if ((s.startsWith('"') && s.endsWith('"')) || (s.startsWith('"') && s.endsWith('"'))) s = s.slice(1, -1);
s = s.replace(/\\"/g, '"').replace(/\\n/g, '\n').replace(/\\t/g, '\t').replace(/\\r/g, '\r').replace(/\\\\/g, '\\');
return JSON.parse(s);
}
// Flatten nested objects/arrays into k1_k2_0_k3 style keys
function flatten(obj, prefix = '', out = {}) {
if (obj == null) return out;
const makeKey = (k) => (prefix ? `${prefix}_${k}` : String(k));
if (Array.isArray(obj)) {
obj.forEach((v, i) => {
const k = makeKey(i);
(v && typeof v === 'object') ? flatten(v, k, out) : out[k] = v;
});
return out;
}
if (typeof obj === 'object') {
for (const [k, v] of Object.entries(obj)) {
const key = makeKey(k);
(v && typeof v === 'object') ? flatten(v, key, out) : out[key] = v;
}
return out;
}
out[prefix || 'value'] = obj;
return out;
}
const parsed = [];
for (const item of items) {
try {
const obj = parseMaybeEscapedJSON(item.json?.Output);
if (!obj || typeof obj !== 'object') {
parsed.push({ json: { error: 'Parsed Output is not an object', raw: item.json?.Output ?? null } });
continue;
}
const out = flatten(obj);
parsed.push({ json: out });
} catch (e) {
parsed.push({ json: { error: 'Failed to parse Output', message: e?.message || String(e), raw: item.json?.Output ?? null } });
}
}
return parsed; `${prefix}_${k}` : String(k));
if (Array.isArray(obj)) {
obj.forEach((v, i) => {
const k = makeKey(i);
(v && typeof v === 'object') ? flatten(v, k, out) : out[k] = v;
});
return out;
}
if (typeof obj === 'object') {
for (const [k, v] of Object.entries(obj)) {
const key = makeKey(k);
(v && typeof v === 'object') ? flatten(v, key, out) : out[key] = v;
}
return out;
}
out[prefix || 'value'] = obj;
return out;
}
const parsed = [];
for (const item of items) {
try {
const obj = parseMaybeEscapedJSON(item.json?.Output);
if (!obj || typeof obj !== 'object') {
parsed.push({ json: { error: 'Parsed Output is not an object', raw: item.json?.Output ?? null } });
continue;
}
const out = flatten(obj);
parsed.push({ json: out });
} catch (e) {
parsed.push({ json: { error: 'Failed to parse Output', message: e?.message || String(e), raw: item.json?.Output ?? null } });
}
}
return parsed; null } });
continue;
}
const out = flatten(obj);
parsed.push({ json: out });
} catch (e) {
parsed.push({ json: { error: 'Failed to parse Output', message: e?.message || String(e), raw: item.json?.Output ?? null } });
}
}
return parsed; null } });
}
}
return parsed;
Nota sobre el aplanamiento: los arrays y los objetos anidados se convertirán en columnas como issues_0_type, issues_0_severity, etc. Esto facilita almacenarlo en Google Sheets.
8. Guardar resultados
El paso final almacena los resultados de QA en Google Sheets para seguimiento e informes.
n8n node: Google Sheets (append)
Credenciales para conectar: Conecta tu cuenta de Google Sheets
Recurso: Hoja dentro del documento
Operación: Agregar fila
Seleccionar documento: Desde la lista → Selecciona el nombre de la hoja de cálculo
Seleccionar hoja: Desde la lista → Nombre de la pestaña
Modo de mapeo de columnas: Mapea cada columna manualmente
Luego, mapea cada valor al nombre de columna que configuraste en tu hoja, idealmente siguiendo los nombres de columna descritos en la Sección 1.
De dónde proviene cada campo
Desde el disparador del Webhook (metadatos de la conversación):
Mapéalos usando la referencia del nodo Webhook, p. ej.:
{{ $('Webhook').first().json.body.contact_id }}
contact_idconversation_opened_timestampconversation_closed_timestamp
Desde la salida analizada por la IA (Sección 7):
Asócialos usando los campos del resultado analizado, p. ej.:
{{ $json.overall_score }}
overall_scoreresolvedsentimentreasoningengagement_score
Para depuración (recomendado durante la implementación):
raw_ai_output— almacena la respuesta completa de la IA para que puedas revisar los resultados y refinar tu prompt
💡 Consejo: Revisa la salida real de tu nodo Webhook para confirmar los nombres exactos de los campos — pueden variar según la configuración del disparador de tu espacio de trabajo.
Mejoras opcionales
Enmascara la información PII antes de la revisión por la IA (correos electrónicos, números de teléfono, IDs de pedido)
Procesa más de 100 mensajes iterando la paginación hasta que
pagination.nextesté vacíoConversaciones de muestra (p. ej., revisa solo el 10% de las conversaciones cerradas para controlar costos)
Deriva problemas de alta severidad a Slack/Teams para un seguimiento más rápido