Saltar al contenido
/pacoforet

Cómo añadir snapshotting a HubSpot con Claude Code + Apps Script (sin volverte loco)

9 min de lectura

Hace un par de años, cuando quería ver “cómo estaba el pipeline el día 1 del mes” o “qué ARR teníamos en forecast hace 3 semanas”, lo resolvía con una mezcla de Excel, capturas mentales y resignación.

En HubSpot, puedes tirar de “property history” para algunas cosas, y hay reporting decente… pero en cuanto necesitas snapshotting real (congelar un estado completo y consultarlo después como si fuera una foto), la historia cambia.

Y el problema se nota justo donde más duele: reporting de revenue, forecasting, cohortes, velocity, auditoría de cambios, SLAs, y cualquier analítica seria.

Yo lo llamo “el agujero negro del snapshotting”: sabes que el dato existió, pero ya no puedes reconstruirlo.

En este post te enseño el patrón que estoy usando para fabricar snapshotting encima de HubSpot usando dos piezas:

  • Claude Code como “Director de Orquesta” para escribir y mantener el pegamento (sin programar como un purista).
  • Google Apps Script como motor barato y fiable para ejecutar snapshots programados, guardar histórico y (si quieres) devolverlo a HubSpot.

No es la solución más glamourosa del mundo. Pero es simple, barata y funciona.

Qué significa “snapshotting” de verdad (y por qué HubSpot flojea)

Snapshotting no es “ver el historial de una propiedad”. Es:

  • Guardar el estado de un conjunto de campos (y a veces un agregado) en un instante.
  • Poder responder preguntas como:
    • “¿Cuántos deals estaban en Stage X el 2026-01-31?”
    • “¿Cuál era el forecast commit por owner el lunes pasado?”
    • “¿Qué porcentaje de cuentas estaban en risk hace 60 días?”
  • Tener datos listos para BI sin recomponer eventos.

HubSpot, por diseño, está optimizado para CRM operativo y reporting estándar. Cuando tu pregunta es temporal (“en aquel momento”), o requiere un “freeze”, normalmente te faltan piezas:

  • Los dashboards suelen mostrar el estado actual, no el pasado congelado.
  • El historial de propiedades existe, pero:
    • no siempre es fácil de consultar a escala,
    • no siempre cubre todo,
    • y reconstruir un estado completo en una fecha es… doloroso.

Si tu empresa toma decisiones (o bonus) con métricas de pipeline, snapshotting no es “nice to have”. Es control interno.

El patrón: “Snapshot Ledger” (un libro mayor de estados)

Piensa en esto como una contabilidad de estados.

Cada día (o cada semana), haces una “foto” de lo que te importa y lo guardas en un ledger externo:

  1. Lees de HubSpot un set de objetos (deals, companies, tickets…).
  2. Te quedas con las propiedades que te importan.
  3. Añades metadatos (fecha snapshot, id, owner, etc.).
  4. Guardas en:
    • Google Sheets (rápido y humano),
    • o BigQuery (si ya eres serio con BI),
    • o ambos.
  5. Opcional: escribes una parte del snapshot de vuelta a HubSpot en un custom object para reporting interno.

El objetivo no es “replicar HubSpot”. Es guardar el mínimo necesario para responder tus preguntas temporales.

Qué vas a construir (arquitectura)

Mi versión simple (la que recomiendo empezar):

  • Google Sheet = base de snapshots (tabla)
  • Apps Script = job diario/semanal
  • HubSpot API = lectura (y opcional escritura)

Flujo:

  • Trigger diario 02:00
  • Apps Script:
    • Busca deals actualizados en las últimas 24h (y/o todos si quieres snapshot total).
    • Crea filas en snapshots_deals con:
      • snapshot_date
      • deal_id
      • pipeline, dealstage, amount, close_date, owner_id, etc.
    • Evita duplicados por (snapshot_date, deal_id)

Si quieres snapshotting “puro” (foto completa del día), haces pull de todos los deals cada día. Si el volumen es grande, haces snapshot incremental + un “full snapshot” semanal.

Empieza por 10-20 propiedades. El snapshotting se estropea cuando intentas capturar “todo HubSpot”. Captura decisión, no “decoración”.

Dónde entra Claude Code (y por qué aquí brilla)

Claude Code me sirve para:

  • Generar el esqueleto del Apps Script con buenas prácticas.
  • Convertir una lista de propiedades de HubSpot en un “mapping” limpio.
  • Escribir funciones de paginación, retries y rate limit.
  • Montar un “backfill” de históricos sin romperlo todo.
  • Documentar el flujo para que alguien más lo mantenga.

Yo lo uso como un compañero que “pica piedra” rápido, mientras yo hago lo importante: decidir qué snapshot importa y cómo lo consumo en reporting.

Paso 1: prepara el Google Sheet como tabla

Crea una spreadsheet con una hoja llamada, por ejemplo: snapshots_deals

Columnas recomendadas:

  • snapshot_date (YYYY-MM-DD)
  • snapshot_ts (ISO datetime)
  • deal_id
  • dealname
  • pipeline
  • dealstage
  • amount
  • close_date
  • hubspot_owner_id
  • lastmodifieddate

Esto es tu “ledger”.

Paso 2: Apps Script con autenticación y helpers

Crea un proyecto de Apps Script (desde el propio Sheet) y añade tu token privado (idealmente en Script Properties).

Este es el núcleo:

const HUBSPOT_BASE = "https://api.hubapi.com";
const SHEET_NAME = "snapshots_deals";

function getHubSpotToken_() {
  const token = PropertiesService.getScriptProperties().getProperty("HUBSPOT_TOKEN");
  if (!token) throw new Error("Missing HUBSPOT_TOKEN in Script Properties");
  return token;
}

function hubspotFetch_(path, options = {}) {
  const token = getHubSpotToken_();
  const url = `${HUBSPOT_BASE}${path}`;

  const params = {
    method: options.method || "get",
    muteHttpExceptions: true,
    headers: {
      Authorization: `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    payload: options.payload ? JSON.stringify(options.payload) : undefined,
  };

  const res = UrlFetchApp.fetch(url, params);
  const code = res.getResponseCode();
  const body = res.getContentText();

  if (code >= 200 && code < 300) return JSON.parse(body);

  // Rate limit simple retry
  if (code === 429) {
    Utilities.sleep(1500);
    return hubspotFetch_(path, options);
  }

  throw new Error(`HubSpot API error ${code}: ${body}`);
}

Paso 3: leer deals y escribir snapshots

Aquí hay dos enfoques:

A) Snapshot completo (simple, más caro)

Lees todos los deals paginando.

B) Snapshot incremental (más eficiente)

Lees solo deals actualizados desde la última ejecución y guardas snapshot del día para esos.

Para empezar, yo haría incremental diario + full semanal. Te dejo incremental:

const DEAL_PROPERTIES = [
  "dealname",
  "pipeline",
  "dealstage",
  "amount",
  "closedate",
  "hubspot_owner_id",
  "hs_lastmodifieddate",
];

function snapshotDealsDaily() {
  const snapshotDate = Utilities.formatDate(new Date(), "Europe/Madrid", "yyyy-MM-dd");
  const snapshotTs = new Date().toISOString();

  const sheet = SpreadsheetApp.getActive().getSheetByName(SHEET_NAME);
  if (!sheet) throw new Error(`Missing sheet: ${SHEET_NAME}`);

  const sinceMs = getSinceMs_(); // last run timestamp
  const deals = searchDealsByLastModified_(sinceMs, DEAL_PROPERTIES);

  const existingKeys = loadExistingKeys_(sheet, snapshotDate); // deal_id keys for the day
  const rows = [];

  deals.forEach(d => {
    const dealId = d.id;
    const key = `${snapshotDate}:${dealId}`;
    if (existingKeys.has(key)) return;

    const p = d.properties || {};
    rows.push([
      snapshotDate,
      snapshotTs,
      dealId,
      p.dealname || "",
      p.pipeline || "",
      p.dealstage || "",
      p.amount || "",
      p.closedate || "",
      p.hubspot_owner_id || "",
      p.hs_lastmodifieddate || "",
    ]);
  });

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

  setSinceMs_(Date.now());
}

function searchDealsByLastModified_(sinceMs, properties) {
  const results = [];
  let after = 0;

  while (true) {
    const payload = {
      filterGroups: [{
        filters: [{
          propertyName: "hs_lastmodifieddate",
          operator: "GTE",
          value: String(sinceMs),
        }],
      }],
      properties,
      limit: 100,
      after: after || undefined,
    };

    const data = hubspotFetch_("/crm/v3/objects/deals/search", {
      method: "post",
      payload,
    });

    (data.results || []).forEach(r => results.push(r));

    if (!data.paging || !data.paging.next) break;
    after = data.paging.next.after;
  }

  return results;
}

function loadExistingKeys_(sheet, snapshotDate) {
  const lastRow = sheet.getLastRow();
  if (lastRow < 2) return new Set();

  // Asumimos columna 1 snapshot_date y 3 deal_id
  const values = sheet.getRange(2, 1, lastRow - 1, 3).getValues();
  const set = new Set();
  values.forEach(row => {
    const d = row[0];
    const id = row[2];
    if (String(d) === snapshotDate && id) set.add(`${snapshotDate}:${id}`);
  });
  return set;
}

function getSinceMs_() {
  const v = PropertiesService.getScriptProperties().getProperty("SINCE_MS");
  // default: last 24h
  return v ? Number(v) : (Date.now() - 24 * 60 * 60 * 1000);
}

function setSinceMs_(ms) {
  PropertiesService.getScriptProperties().setProperty("SINCE_MS", String(ms));
}

Paso 4: programa el trigger

En Apps Script:

  • Triggers → Add Trigger
  • Function: snapshotDealsDaily
  • Time-driven: Daily
  • Hora: 02:00 (o cuando menos te moleste)

Apps Script tiene límites (tiempo de ejecución, cuotas de UrlFetch, etc.). Si tu CRM crece mucho, el patrón sigue siendo válido, pero te conviene moverlo a Cloud Functions / Cloud Run. Lo importante es el diseño del ledger, no el motor.

Paso 5: backfill (el “me arrepiento de no haberlo hecho antes”)

La típica: implementas snapshots hoy, y mañana alguien te pregunta por noviembre.

Aquí tienes dos estrategias realistas:

  • Backfill “lo que puedas” con property history (si aplica) para algunas propiedades.
  • Backfill “estado aproximado” a partir de eventos/fechas si tienes un sistema externo (Stripe, ERP, etc.).

Mi recomendación práctica: no prometas backfill perfecto. Promete: “A partir de hoy, esto queda resuelto”.

Snapshotting no es retroactivo por magia. Lo importante es poner el contador a cero y empezar a guardar histórico ya.

Paso 6 (opcional): devolver snapshots a HubSpot para reporting nativo

Esto es útil cuando:

  • quieres dashboards dentro de HubSpot,
  • no quieres depender siempre de Looker/Sheets,
  • o necesitas segmentación y listas basadas en “último snapshot”.

Patrón típico:

  • Creas un custom object tipo deal_snapshot
  • Cada snapshot crea registros:
    • snapshot_date
    • deal_id (o asociación con deal)
    • métricas clave

Y luego reportas sobre ese objeto.

No te pongo aquí el código completo de creación de custom objects (es más largo y depende de tu portal), pero la lógica sería:

  • por cada fila de snapshot → POST a /crm/v3/objects/{customObjectType} con propiedades
  • asociar a deal si lo necesitas

Claude Code te lo escribe rápido, pero tú debes decidir: qué quieres reportar dentro de HubSpot y qué prefieres dejar en BI.

Qué mejoras me han dado más retorno (sin complicarme)

Una vez tienes el ledger, puedes hacer cosas que antes eran “imposibles o caras”:

  • Forecast vs real por semana: cuánto “prometía” el pipeline vs lo que cerró.
  • Pipeline hygiene: deals que rebotan stages, o llevan 30 días sin moverse.
  • Auditoría: cambios de owner, cambios de amount, descuentos raros.
  • Cohortes: conversión de SQL→Closed Won por mes de creación.

Y lo más tonto pero más útil: dejas de discutir sobre el pasado. Hay foto.

Errores típicos (para ahorrarte una semana)

  • Guardar demasiadas propiedades: se vuelve inmanejable y lento.
  • No guardar snapshot_date de forma estable (zona horaria): luego comparas peras con manzanas.
  • No deduplicar por día: acabas con “triplicados” y reporting inflado.
  • No pensar en incremental vs full: o te quedas corto, o revientas cuotas.
  • Convertir esto en un “proyecto de datos” infinito: es un parche productivo, no una tesis.

Cierre: esto es lo que cambia cuando tienes snapshots

Antes, yo veía HubSpot como “el sistema de verdad” y todo lo demás como accesorio.

Ahora lo veo al revés: HubSpot es el sistema operativo del equipo comercial, pero mi ledger de snapshots es la memoria. Y una empresa sin memoria repite errores.

Suena exagerado, lo sé. Yo mismo lo habría pensado hace un año.

Pero en cuanto montas snapshotting, la fricción desaparece casi por completo. Dejas de perseguir “qué pasó” y te dedicas a lo único importante: qué hacemos con lo que pasó.