Saltar al contenido
/pacoforet

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

7 min de lectura

El día que perdí tres meses de datos

Hace unos meses, mi equipo comercial me pidió un análisis muy concreto: quería saber cómo habían evolucionado los deals en el pipeline durante el último trimestre. Cuántos estaban en cada etapa semana a semana. Si el ciclo de venta se estaba acortando o alargando.

Abrí HubSpot. Busqué. Filtré. Y entonces llegó el golpe:

HubSpot no guarda eso.

O mejor dicho: HubSpot te dice en qué etapa está un deal ahora mismo. Pero no te dice en qué etapa estaba hace tres semanas. No hay histórico granular de propiedades. No hay “replay” del pipeline. Si un deal pasó de Propuesta enviada a Cerrado ganado y no lo viste en el momento exacto, esa información desapareció para siempre.

Eso, para cualquier análisis de negocio serio, es un problema enorme.

El problema real: HubSpot está diseñado para el presente

No es un bug, es una decisión de diseño. HubSpot está optimizado para gestionar lo que pasa ahora: automatizaciones, notificaciones, pipelines en vivo. Pero cuando necesitas responder preguntas del tipo “¿cómo estaba el pipeline hace 6 semanas?”, chocas con un muro.

Las únicas salidas que ofrece la plataforma son limitadas:

  • Informes de actividad: Registran acciones (emails, llamadas), no estados de propiedades.
  • Historial de propiedades: Existe, pero es por objeto individual. No puedes hacer un query agregado de “dame el valor de esta propiedad para todos mis deals en una fecha concreta”.
  • Exportaciones manuales: La solución manual que nadie quiere hacer cada semana.

Lo que yo necesitaba era un snapshot: una foto del estado completo de todos mis deals en un momento dado, guardada en algún lugar que pueda consultar después.

Un “snapshot” en este contexto es simplemente una copia del estado actual de tus datos en un momento específico. Como hacer una foto del tablero de ajedrez después de cada turno, en lugar de intentar recordar cómo estaba hace diez movimientos.

La solución: un fotógrafo automático con Claude Code

Mi primera reacción fue buscar integraciones en el HubSpot Marketplace. Hay cosas interesantes, pero o son caras, o requieren un data warehouse, o son demasiado complejas para lo que quería: una Google Sheet que se auto-rellene cada semana con el estado de mis deals.

Así que le planteé el problema a Claude Code.

No le pedí que “programara algo”. Le describí el problema como se lo describiría a un colega listo:

“Tengo deals en HubSpot. Cada semana quiero una fila nueva en una Google Sheet con el ID del deal, su nombre, etapa, importe, propietario y fecha del snapshot. Quiero que esto pase automáticamente todos los lunes a las 8 de la mañana.”

Lo que salió al otro lado me dejó con la boca abierta.

El stack: HubSpot API + Google Apps Script

Claude Code me propuso una solución elegante y completamente gratuita:

  • Google Apps Script como motor de ejecución (vive dentro de Google Sheets, no necesitas servidores).
  • HubSpot Private App para autenticación con la API (nada de OAuth complejo).
  • Google Sheets como base de datos del histórico.
  • Trigger temporal de Apps Script para automatizar la ejecución semanal.

La lógica es simple: cada lunes, el script llama a la API de HubSpot, pide todos los deals activos con las propiedades que me interesan, y añade una fila por deal a la Sheet con la fecha de hoy como marca temporal.

Así, en 3 meses, tengo 12 semanas de snapshots. Puedo filtrar por fecha y ver exactamente cómo estaba el pipeline en cualquier lunes concreto.

Esto es el script que generó Claude Code (con mínimas adaptaciones mías para añadir las propiedades específicas de mi CRM):

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;
}

Para guardar el token de HubSpot de forma segura, usa PropertiesService.getScriptProperties() en Apps Script. Ve a Proyecto → Propiedades del script y añade HUBSPOT_TOKEN con tu clave. Nunca la escribas directamente en el código.

El proceso: de problema a solución en menos de una hora

Lo que más me sorprende cuando miro hacia atrás es la velocidad. Este es el flujo real que seguí:

  • Describí el problema a Claude Code en lenguaje natural.
  • Revisé la propuesta de arquitectura (HubSpot API + Apps Script). Tenía sentido.
  • Pedí el código completo. Lo generó en segundos.
  • Copié el script a Google Apps Script y configuré el token.
  • Probé manualmente la función takeSnapshot(). Funcionó a la primera.
  • Configuré el trigger semanal desde la interfaz de Apps Script (sin código: es un menú visual).

El único momento donde tuve que pensar fue al revisar qué propiedades de HubSpot quería capturar. Eso es criterio de negocio, no código. Y eso sí es mi trabajo.

La API de HubSpot tiene límites de rate: 110 llamadas por 10 segundos para Private Apps. Si tienes más de 10.000 deals activos, necesitarás añadir pausas (Utilities.sleep()) entre páginas. Claude Code lo sabe y lo incluirá si se lo dices.

Lo que tengo ahora (y lo que antes era imposible)

Tres meses después de poner esto en marcha, tengo una Sheet con más de 4.000 filas. Y puedo responder preguntas que antes eran impensables:

  • ¿Cuántos deals estaban en “Propuesta enviada” el primer lunes de enero?
  • ¿Cuál fue el importe total del pipeline en cada semana del Q1?
  • ¿Qué deals lleva más de 4 semanas consecutivas en la misma etapa sin avanzar?

Con una simple tabla dinámica en Google Sheets. Sin Tableau. Sin un data warehouse. Sin un equipo de datos.

Eso es exactamente lo que significa ser Director de Orquesta: yo no toqué el violín. Pero supe qué música necesitaba sonar.

Reflexión final: el poder de saber qué preguntar

Antes de Claude Code, este problema tenía dos salidas: contratar a un desarrollador para que lo construyera, o resignarse a vivir sin el dato.

Ahora hay una tercera: entender el problema con suficiente claridad como para explicárselo a una IA, y tener el criterio para evaluar si la solución que propone tiene sentido.

Eso no es magia. Es una habilidad nueva que vale la pena cultivar.

La barrera técnica sigue existiendo, pero ya no la tengo que saltar yo solo. Y eso, honestamente, ha cambiado cómo pienso sobre qué proyectos son posibles.