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

> HubSpot no guarda el estado histórico de tus deals y propiedades. Te cuento cómo construí un sistema de snapshotting automático con Claude Code y Google Apps Script sin escribir una sola línea de código manualmente.

- URL: https://pacoforet.com/posts/hubspot-snapshots-claude-code-apps-script/
- Autor: Paco Foret
- Publicado: 2026-03-02
- Temas: RevOps, Automatización, Claude Code

## 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.

> [!INFO]
> 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

```javascript title="HubSpot Snapshot — Code.gs" ln
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;
}
```

> [!TIP]
> 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.

> [!WARNING]
> 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.
