Saltar al contenido
/pacoforet

HubSpot no puede hacer snapshots. Así lo resolví con Claude Code y Apps Script

5 min de lectura

El vacío histórico en el pipeline

Al intentar analizar la evolución de los deals en el pipeline durante el último trimestre —cuántos pasaban por cada etapa semana a semana y si el ciclo de venta se estaba acelerando—, surgió un límite claro de HubSpot:

La plataforma almacena el estado presente, no un histórico agregado accesible.

HubSpot indica en qué etapa se encuentra una oportunidad comercial hoy. Si un deal avanzó de Propuesta enviada a Cerrado ganado hace tres semanas, no existe una vista nativa que permita consultar el estado del pipeline global en esa fecha concreta sin recurrir a costosos data warehouses o volcados manuales continuos.

La limitación: HubSpot está diseñado para el tiempo real

No se trata de un defecto, sino de una decisión de arquitectura: el CRM está optimizado para la gestión operativa en vivo (automatizaciones, alertas, seguimiento de actividad). Sin embargo, para análisis de cohortes o velocidad de ventas surgen barreras:

  • Informes de actividad: Registran interacciones (llamadas, correos), no el estado histórico consolidado de los deals.
  • Historial de propiedades: Disponible solo por objeto individual; no permite consultas agregadas como “valor de todos los deals activos al 15 de enero”.
  • Exportaciones manuales: Inviables para un seguimiento semanal continuo y fiable.

La solución requerida era un snapshot periódico: capturar una instantánea completa del pipeline cada semana y almacenarla en una estructura consultable.

Un snapshot congela el estado de los datos en un punto temporal específico, permitiendo comparar semanas y detectar cuellos de botella sin alterar el CRM.

La arquitectura: HubSpot Private App + Google Apps Script

En lugar de contratar conectores de pago o montar un data warehouse para un requerimiento específico, planteé el diseño a Claude Code con esta especificación:

“Cada lunes a las 08:00, consulta todos los deals activos en HubSpot y añade una fila por deal en una Google Sheet con ID, nombre, etapa, importe, propietario y marca temporal del snapshot.”

La solución implementada aprovecha recursos gratuitos y sin servidores propios:

  • Google Apps Script: Motor de ejecución integrado en Google Sheets con triggers cron.
  • HubSpot Private App: Autenticación directa y segura mediante token de API.
  • Google Sheets: Almacenamiento tabular para conectar tablas dinámicas o herramientas de BI.

Script completo en Google Apps Script

const HUBSPOT_TOKEN = PropertiesService.getScriptProperties().getProperty('HUBSPOT_TOKEN');
const SHEET_NAME = 'Snapshots';
const PROPERTIES_TO_FETCH = [
  'dealname',
  'dealstage',
  'amount',
  'hubspot_owner_id',
  'closedate',
  'pipeline'
];

function takeSnapshot() {
  const sheet = getOrCreateSheet_();
  const deals = fetchAllDeals_();
  const timestamp = new Date().toISOString();
  const rows = deals.map(deal => buildRow_(deal, timestamp));

  if (rows.length > 0) {
    sheet.getRange(sheet.getLastRow() + 1, 1, rows.length, rows[0].length)
         .setValues(rows);
  }

  Logger.log(`Snapshot completado: ${rows.length} deals guardados.`);
}

function fetchAllDeals_() {
  const allDeals = [];
  let after = null;

  do {
    const url = buildUrl_(after);
    const response = UrlFetchApp.fetch(url, {
      headers: { Authorization: `Bearer ${HUBSPOT_TOKEN}` },
      muteHttpExceptions: true
    });

    const data = JSON.parse(response.getContentText());
    if (data.results) allDeals.push(...data.results);
    after = data.paging?.next?.after || null;

  } while (after);

  return allDeals;
}

function buildUrl_(after) {
  const base = 'https://api.hubapi.com/crm/v3/objects/deals';
  const props = PROPERTIES_TO_FETCH.join(',');
  let url = `${base}?limit=100&properties=${props}`;
  if (after) url += `&after=${after}`;
  return url;
}

function buildRow_(deal, timestamp) {
  const p = deal.properties;
  return [
    timestamp,
    deal.id,
    p.dealname,
    p.dealstage,
    p.amount ? parseFloat(p.amount) : 0,
    p.hubspot_owner_id,
    p.closedate,
    p.pipeline
  ];
}

function getOrCreateSheet_() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  let sheet = ss.getSheetByName(SHEET_NAME);
  if (!sheet) {
    sheet = ss.insertSheet(SHEET_NAME);
    sheet.appendRow(['timestamp', 'deal_id', 'nombre', 'etapa', 'importe', 'propietario', 'fecha_cierre', 'pipeline']);
    sheet.setFrozenRows(1);
  }
  return sheet;
}

Almacena el token de HubSpot en Configuración del proyecto → Propiedades del script con la clave HUBSPOT_TOKEN, evitando exponer credenciales en el código fuente.

Despliegue en producción en menos de una hora

El flujo de trabajo:

  1. Definición de propiedades: Seleccionar en HubSpot los campos esenciales para el análisis (dealstage, amount, closedate, etc.).
  2. Generación con Claude Code: Especificar la paginación de la API de HubSpot y la escritura en bloque (setValues) para evitar límites de ejecución en Apps Script.
  3. Trigger temporal: Configurar un activador por tiempo en Google Apps Script para que ejecute takeSnapshot() automáticamente cada lunes a primera hora.

La API de HubSpot para Private Apps tiene un límite de 110 llamadas por cada 10 segundos. Si gestionas volúmenes superiores a 10.000 deals activos por snapshot, conviene incorporar pausas controladas (Utilities.sleep()) entre páginas.

Resultados y visibilidad de Revenue Operations

Tras tres meses de ejecución automatizada, la hoja acumula más de 4.000 registros históricos que permiten responder preguntas de negocio inmediatas:

  • Importe total en pipeline en cada semana del trimestre.
  • Tasa de avance entre etapas semana a semana.
  • Deals estancados durante más de 4 semanas en la misma fase.

Todo ello mediante tablas dinámicas estándar, sin coste adicional de software ni dependencias externas de infraestructura.

Conclusión

El valor de estas soluciones no reside en la complejidad del código, sino en identificar con precisión el requerimiento operativo y coordinar herramientas existentes para resolverlo con el menor coste y fricción posibles.