Project Management con Spec Driven DevelopmentBeta

0%
Bloque Técnico-Operativo

Módulo 09

Google Workspace + GCP

5h

Apps Script para automatizar: Forms → Calendar (agendamiento automático), formulario → Google Docs con template. Despliegue en Cloud Run.

Recursos descargables

Automatización Forms → Calendar + Docs

Apps Script que procesa cada respuesta de un Google Form: agenda el kickoff en Calendar, crea el doc de alcance y envía email de confirmación

Apps Script

Conector a Cloud Run

Patrón mínimo para invocar un servicio HTTP externo (Cloud Run, API propia, webhook) desde Google Sheets/Docs/Forms

Apps Script

Patrón de Resiliencia por Lotes

Procesa lotes grandes sin morir por el límite de 6 minutos de Apps Script: guarda el progreso y se reprograma solo hasta terminar

Apps Script
En este módulo
01
Introducción De mecanógrafo a arquitecto: automatizar Google Workspace sin presupuesto

Si tu agencia usa Google Workspace —y todas las agencias lo usan— hay un problema que conoces bien: recibes 50 solicitudes de proyectos por semana en Google Forms, y cada una implica crear un documento de alcance, agendar una reunión y actualizar un calendario. Todo a mano. Ese trabajo no es gestión. Es mecanografía.

Google Apps Script es el puente: conecta Forms con Calendar, Docs, Sheets y Gmail sin salir del ecosistema Google y sin pagar un centavo extra. Pero hay un límite mortal: 6 minutos. Si tu script se ejecuta más tiempo, Google lo mata. Sin piedad. Sin aviso.

En este módulo construyes tu primer flujo Forms → Calendar → Docs y aprendes a burlar el límite con técnicas de los ingenieros de Google: batching, triggers programáticos y ejecución resiliente. Al final tendrás un sistema que hace en 2 segundos lo que hoy te toma 20 minutos.

Vamos.

02
Desarrollo teórico-práctico Apps Script, el muro de los 6 minutos y cuándo escalar a Cloud Run

2.1. ¿Qué es Google Apps Script y Por Qué te Importa?

GAS (Google Apps Script) es JavaScript que se ejecuta en los servidores de Google. No necesitas instalar nada. No necesitas servidor. Solo abres el editor desde Google Sheets, Docs o Forms, y escribes código.

Lo que puedes automatizar como PM:

Disparador (Trigger) Acción Automatizada
Alguien llena un Formulario Crear un evento en Calendar
Se edita una celda en Sheets Enviar un email con los datos
Cada día a las 8 AM Generar un reporte en Docs
Cada hora Revisar una API externa y actualizar Sheets
🧭
Analogía (Agencia de Marketing)

Apps Script es un asistente que trabaja 24/7 en segundo plano. Cuando el cliente llena el brief en Forms, el asistente agenda la reunión de kickoff, crea el documento de alcance y asigna un recurso — todo antes de que termines tu café.

2.2. El Límite Letal: 6 Minutos

El muro de los 6 minutos

Si tu script se ejecuta por más de 6 minutos, Google lo termina forzosamente, a mitad de lote y sin avisarte. Todo el diseño de este módulo gira alrededor de ese muro.

Google impone cuotas estrictas a Apps Script. Las más críticas:

Cuota Límite Qué Significa para tu Script
Tiempo máximo de ejecución 6 minutos por invocación Si tu script tarda más, Google lo termina forzosamente.
Llamadas a UrlFetch 20,000/día Suficiente para consultar APIs, pero no para hacer scraping masivo.
Tiempo total de triggers 90 min/día (cuenta gratuita) Suficiente para automatizaciones diarias.

El problema: si tienes que procesar 500 respuestas de Forms, cada una requiere crear un evento en Calendar y un documento en Docs. El script puede tardar más de 6 minutos. Google lo mata a la mitad. 200 formularios quedan sin procesar y no te enteras.

La solución: el boilerplate de Resiliencia, un patrón que rodea el muro en un ciclo:

Medir
Cronometra el tiempo de ejecución en cada iteración del lote.
Guardar
Al acercarse a los 5 min, guarda el índice en PropertiesService.
Programar
Crea un trigger que relanza la función en +1 min.
Retomar
Termina grácilmente; la siguiente ejecución continúa desde el índice guardado.
El ciclo de resiliencia: el script se pausa solo antes de que Google lo mate y se reanuda donde quedó.

2.3. Google Cloud Run: Cuando Apps Script No Alcanza

Apps Script es ideal para automatización ligera dentro de Workspace. Pero si necesitas:

  • Procesar datos masivos (>6 min).
  • Usar librerías Python.
  • Exponer un endpoint HTTP público.

Entonces necesitas Cloud Run: un servicio de GCP que ejecuta contenedores Docker y escala a cero cuando no se usa. El Free Tier de GCP te da 2 millones de peticiones al mes gratuitas.

Apps Script = el pegamento
Conecta Forms, Calendar, Docs y Gmail con disparadores nativos.
Lo configura: un PM, sin ayuda de ingeniería.
Límite: 6 min por ejecución, solo JavaScript.
Cloud Run = el motor pesado
Procesa datos masivos, genera PDFs, corre modelos de IA.
Lo configura: alguien cómodo con Docker y GCP.
Free Tier: 2 M de peticiones/mes.
Regla de oro

Apps Script es el pegamento, Cloud Run es el motor: usa el ligero hasta que pese. En este módulo Apps Script es el protagonista y Cloud Run la alternativa escalable.

03
Paso a paso técnico (Hands-on) Construir el flujo Forms → Calendar → Docs y blindarlo contra el muro de 6 min
  1. Preparar el entorno
    1. Ve a script.google.com.
    2. Haz clic en "Nuevo proyecto".
    3. Nómbralo: Flujo_Proyectos_Agencia.
    4. El editor se abre. Ahí escribes el código.
  2. Script base: Forms → Calendar → Docs

    Escenario: un cliente llena un formulario de solicitud de proyecto y, automáticamente, el sistema dispara tres acciones en paralelo:

    📋 Google Forms
    Trigger: "Al enviarse un formulario" → procesarFormulario(e)
    ↓   ↓   ↓
    📅 Calendar
    Evento de kickoff con fecha del formulario y 1 h de duración.
    📄 Docs
    Documento de alcance generado desde plantilla, en la carpeta del proyecto.
    ✉️ Gmail
    Email de confirmación al cliente con enlace al doc y fecha de reunión.
    El flujo del módulo: 1 trigger, 3 acciones en paralelo, 0 minutos del PM.
    Tip

    No copies el código a ciegas: el bloque CONFIG del inicio centraliza los IDs y textos que debes ajustar (carpeta de Docs, asunto del email). Si algo falla, revisa primero ahí.

    // Configuración del proyecto
    var CONFIG = {
      CALENDAR_ID: CalendarApp.getDefaultCalendar().getId(),
      DOCS_FOLDER_ID: 'ID_DE_LA_CARPETA', // Reemplazar con el ID de tu carpeta
      EMAIL_ASUNTO: 'Confirmación de recepción de proyecto',
      EMAIL_REMITENTE: Session.getActiveUser().getEmail(),
    };
    
    /**
     * Punto de entrada: se ejecuta cuando alguien llena el Formulario.
     * Conecta el script al Formulario desde el editor de Apps Script:
     * Editar → Disparadores del proyecto actual → Agregar disparador
     *   - Elegir función: procesarFormulario
     *   - Desplegar en: Al enviarse un formulario
     */
    function procesarFormulario(e) {
      try {
        // e.values contiene los datos del formulario en orden de columnas
        var datos = e.values;
        var nombreCliente = datos[1];      // Asume que la columna 1 es "Nombre"
        var emailCliente = datos[2];       // Asume que la columna 2 es "Email"
        var descripcion = datos[3];        // Asume que la columna 3 es "Descripción"
        var fechaKickoff = new Date(datos[4]); // Asume que la columna 4 es "Fecha de kickoff"
    
        // Paso 1: Crear evento en Calendar
        var evento = crearEventoCalendar(nombreCliente, fechaKickoff);
        Logger.log('Evento creado: ' + evento.getId());
    
        // Paso 2: Crear documento de alcance
        var doc = crearDocumentoAlcance(nombreCliente, descripcion);
        Logger.log('Documento creado: ' + doc.getUrl());
    
        // Paso 3: Enviar email de confirmación
        enviarEmailConfirmacion(emailCliente, nombreCliente, evento, doc);
        Logger.log('Email enviado a: ' + emailCliente);
    
      } catch (error) {
        Logger.log('Error procesando formulario: ' + error.message);
        // Notificar al PM que algo falló
        MailApp.sendEmail(
          Session.getActiveUser().getEmail(),
          'Error en flujo de proyectos',
          'Ocurrió un error al procesar el formulario:\n\n' + error.message
        );
      }
    }
    
    /**
     * Crea un evento en Google Calendar para la reunión de kickoff.
     */
    function crearEventoCalendar(nombreCliente, fechaKickoff) {
      var calendario = CalendarApp.getDefaultCalendar();
      var evento = calendario.createEvent(
        'Kickoff: ' + nombreCliente,
        fechaKickoff,
        new Date(fechaKickoff.getTime() + 60 * 60 * 1000), // 1 hora de duración
        {
          description: 'Reunión inicial con ' + nombreCliente + ' para definir alcance del proyecto.'
        }
      );
      return evento;
    }
    
    /**
     * Crea un Documento de Google con la plantilla de alcance del proyecto.
     */
    function crearDocumentoAlcance(nombreCliente, descripcion) {
      var carpeta = DriveApp.getFolderById(CONFIG.DOCS_FOLDER_ID);
      var doc = DocumentApp.create('Alcance - ' + nombreCliente);
    
      var body = doc.getBody();
      body.appendParagraph('Documento de Alcance del Proyecto')
          .setHeading(DocumentApp.ParagraphHeading.HEADING1);
      body.appendParagraph('Cliente: ' + nombreCliente);
      body.appendParagraph('Fecha: ' + new Date().toLocaleDateString());
      body.appendParagraph('');
      body.appendParagraph('Descripción del proyecto:')
          .setHeading(DocumentApp.ParagraphHeading.HEADING2);
      body.appendParagraph(descripcion);
      body.appendParagraph('');
      body.appendParagraph('Criterios de Éxito:')
          .setHeading(DocumentApp.ParagraphHeading.HEADING2);
      body.appendParagraph('[Por definir con el cliente en la reunión de kickoff]');
    
      doc.saveAndClose();
    
      // Mover el documento a la carpeta del proyecto
      var archivo = DriveApp.getFileById(doc.getId());
      carpeta.addFile(archivo);
      DriveApp.getRootFolder().removeFile(archivo);
    
      return doc;
    }
    
    /**
     * Envía un email de confirmación al cliente.
     */
    function enviarEmailConfirmacion(email, nombre, evento, doc) {
      var asunto = CONFIG.EMAIL_ASUNTO;
      var cuerpo = 'Hola ' + nombre + ',\n\n';
      cuerpo += 'Hemos recibido tu solicitud de proyecto. Aquí están los detalles:\n\n';
      cuerpo += 'Reunión de kickoff: ' + evento.getStartTime().toLocaleString() + '\n';
      cuerpo += 'Documento de alcance: ' + doc.getUrl() + '\n\n';
      cuerpo += 'Cualquier duda, quedo atento.\n\n';
      cuerpo += 'Saludos,\n';
      cuerpo += Session.getActiveUser().getDisplayName();
    
      MailApp.sendEmail(email, asunto, cuerpo);
    }
    
  3. El Boilerplate de Resiliencia (anti-6-minutos)

    Este es el código que te permite procesar lotes grandes de datos sin que Google mate tu script.

    Crea un archivo nuevo en el mismo proyecto: Resiliencia.gs

    /**
     * BOILERPLATE DE RESILIENCIA
     *
     * Este código permite que un script de Apps Script procese
     * grandes volúmenes de datos sin morir en el intento.
     *
     * Estrategia:
     * 1. Mide el tiempo transcurrido constantemente.
     * 2. Si se acerca a los 5 minutos (limite real: 6),
     *    guarda el progreso en PropertiesService.
     * 3. Programa un trigger para relanzar la función en 1 minuto.
     * 4. Termina la ejecución actual ordenadamente.
     * 5. En la próxima ejecución, retoma desde el índice guardado.
     *
     * Uso: Configura un trigger basado en tiempo (cada hora, por ejemplo)
     *      que ejecute procesarLote().
     */
    
    var NOMBRE_LLAVE_PROGRESO = 'indice_actual';
    var MARGEN_SEGURIDAD_MINUTOS = 5; // Se detiene a los 5 min, dejando 1 min de margen
    
    /**
     * Punto de entrada para el procesamiento por lotes.
     */
    function procesarLote() {
      var TIEMPO_INICIO = new Date().getTime();
      var indice = obtenerProgreso();
      var TOTAL = 1000; // Simula 1000 registros a procesar
    
      Logger.log('Iniciando procesamiento desde índice: ' + indice + ' de ' + TOTAL);
    
      for (var i = indice; i < TOTAL; i++) {
        // --- SIMULACIÓN: Aquí va tu lógica real de procesamiento ---
        procesarRegistro(i);
    
        // --- CONTROL DE TIEMPO: ¿Nos estamos acercando al límite? ---
        var tiempoEjecucion = (new Date().getTime() - TIEMPO_INICIO) / 1000 / 60;
        if (tiempoEjecucion >= MARGEN_SEGURIDAD_MINUTOS) {
          // Guardamos el progreso antes de detenernos
          guardarProgreso(i + 1);
          programarReintento();
          Logger.log('Pausa programada en índice: ' + (i + 1) +
                     '. Ejecutados: ' + (i - indice) + ' registros en ' +
                     tiempoEjecucion.toFixed(2) + ' minutos.');
          return; // <- Termina la ejecución actual, Google no mata el script
        }
      }
    
      // Si llegamos aquí, procesamos todo el lote
      Logger.log('✅ Procesamiento completo: ' + TOTAL + ' registros.');
      limpiarProgreso();
    
      // Opcional: limpiar triggers ya que el trabajo terminó
      limpiarTriggers();
    }
    
    /**
     * Simula el procesamiento de un registro individual.
     * En un caso real, aquí crearías eventos, documentos, etc.
     */
    function procesarRegistro(indice) {
      // Cada registro toma ~0.5 segundos simulados
      Utilities.sleep(500);
    }
    
    /**
     * Lee el progreso guardado desde PropertiesService.
     */
    function obtenerProgreso() {
      var props = PropertiesService.getScriptProperties();
      var valor = props.getProperty(NOMBRE_LLAVE_PROGRESO);
      return valor ? parseInt(valor, 10) : 0;
    }
    
    /**
     * Guarda el progreso actual en PropertiesService.
     * Estos datos persisten entre ejecuciones del script.
     */
    function guardarProgreso(indice) {
      var props = PropertiesService.getScriptProperties();
      props.setProperty(NOMBRE_LLAVE_PROGRESO, indice.toString());
    }
    
    /**
     * Elimina el progreso guardado (se llama cuando el lote terminó).
     */
    function limpiarProgreso() {
      var props = PropertiesService.getScriptProperties();
      props.deleteProperty(NOMBRE_LLAVE_PROGRESO);
    }
    
    /**
     * Programa un trigger para ejecutarse en 1 minuto.
     * Esto asegura que el procesamiento continúe automáticamente.
     */
    function programarReintento() {
      // Primero limpiamos triggers anteriores para no acumular
      limpiarTriggers();
    
      // Creamos un nuevo trigger que ejecutará procesarLote en 1 minuto
      ScriptApp.newTrigger('procesarLote')
        .timeBased()
        .after(60 * 1000) // 60,000 milisegundos = 1 minuto
        .create();
    
      Logger.log('Trigger programado para continuar en 1 minuto.');
    }
    
    /**
     * Limpia todos los triggers basados en tiempo para esta función.
     * Esto evita que se acumulen triggers huérfanos.
     */
    function limpiarTriggers() {
      var triggers = ScriptApp.getProjectTriggers();
      for (var i = 0; i < triggers.length; i++) {
        if (triggers[i].getHandlerFunction() === 'procesarLote') {
          ScriptApp.deleteTrigger(triggers[i]);
        }
      }
    }
    
    /**
     * Función auxiliar para probar el Boilerplate manualmente.
     * Abre Apps Script, selecciona esta función y haz clic en "Ejecutar".
     */
    function probarProcesamiento() {
      Logger.log('🧪 Prueba del Boilerplate de Resiliencia');
      Logger.log('Procesará registros hasta alcanzar el límite de ' +
                  MARGEN_SEGURIDAD_MINUTOS + ' minutos, luego se pausará.');
    
      // Limpia cualquier progreso anterior
      limpiarProgreso();
    
      // Ejecuta el procesamiento
      procesarLote();
    }
    
  4. Configurar los triggers

    Los tres tipos de trigger que existen en Apps Script:

    📋 Formulario
    "Al enviarse un formulario": dispara el flujo en tiempo real.
    ⏰ Tiempo
    "Cada hora / cada día a las 8 AM": para lotes y reportes.
    ✏️ Edición
    "Al editar una celda en Sheets": para validaciones y avisos.

    Trigger 1: Formulario → Script. En el editor de Apps Script, ve a "Triggers" (reloj) y agrega uno nuevo:

    • Función: procesarFormulario
    • Evento: "Al enviarse un formulario"
    • Fuente: el Forms específico (o "Hoja de cálculo" si usas Sheets como destino).

    Trigger 2: Tiempo → Procesamiento por lotes. Agrega otro trigger:

    • Función: procesarLote
    • Tipo: "Basado en tiempo"
    • Intervalo: "Cada hora" (el Boilerplate controla los 6 min internamente).
  5. Extensión a GCP Cloud Run (opcional)

    Si necesitas procesamiento más pesado (generar PDFs, procesar imágenes, correr modelos), puedes conectar Apps Script a Cloud Run con UrlFetch:

    En Apps Script, hacer una petición a Cloud Run:

    function llamarCloudRun() {
      var url = 'https://TU-SERVICIO.run.app/procesar';
      var payload = {
        cliente: 'Nombre Cliente',
        descripcion: 'Descripción del proyecto'
      };
    
      var options = {
        method: 'post',
        contentType: 'application/json',
        payload: JSON.stringify(payload),
        muteHttpExceptions: true
      };
    
      try {
        var respuesta = UrlFetchApp.fetch(url, options);
        Logger.log('Respuesta de Cloud Run: ' + respuesta.getContentText());
      } catch (error) {
        Logger.log('Error llamando Cloud Run: ' + error.message);
      }
    }
    
04
Cómo usarlo en tu día a día El flujo de agencia, el autodiagnóstico y el entregable

4.1. El Flujo Automatizado de una Agencia de Marketing

Antes (manual)
  1. El cliente llena un Forms.
  2. El PM recibe el email.
  3. El PM abre Calendar, crea evento.
  4. El PM abre Docs, copia plantilla, escribe.
  5. El PM escribe email al cliente.

Tiempo: 20 minutos por cliente.

Después (automatizado)
  1. El cliente llena un Forms.
  2. Apps Script ejecuta procesarFormulario.
  3. Calendar: evento creado.
  4. Docs: documento generado.
  5. Gmail: confirmación enviada.

Tiempo: 0 minutos del PM.

20 min → 2 s
por solicitud procesada
16.7 h → 0 h
con 50 clientes a la semana
2 M
peticiones/mes gratis en el Free Tier de Cloud Run
6 min
el muro que tu script debe esquivar

4.2. Diagnóstico Rápido

  • Autodiagnóstico: ¿necesitas este módulo hoy?
  • ¿Pasas más de 2 horas semanales copiando datos de un Forms a otras herramientas? → Urgente automatizar. (¿Menos de 30 min? Aun así: ¿ese tiempo no vale más en estrategia?)
  • ¿Tu equipo usa Google Workspace pero nadie ha abierto Apps Script? → Están dejando dinero sobre la mesa: el 90% de las automatizaciones que necesitas se hacen desde el navegador.
  • ¿Has tenido scripts que se "cuelgan" sin explicación? → Probablemente llegaron al límite de 6 minutos: implementa el Boilerplate de Resiliencia.

4.3. Entregable del Módulo

🛠 Actividad: sistema Forms → Calendar → Docs con resiliencia

Parte A — Script de flujo base. Un Apps Script que, al recibir un formulario:

  1. Cree un evento en Calendar.
  2. Cree un documento en Docs con los datos del formulario.
  3. Envíe un email de confirmación.
  • Código con manejo de errores (try/catch alrededor del bloque principal).
  • Trigger configurado para ejecutarse al enviarse el formulario.

Parte B — Boilerplate de Resiliencia. El archivo Resiliencia.gs funcional en el mismo proyecto, con una ejecución de prueba que demuestre la pausa y reanudación, y logs visibles: "Iniciando procesamiento desde índice X" y "Pausa programada en índice Y".

Parte C — Grabación de pantalla. GIF o capturas (máx. 2 min) mostrando: el formulario enviado, el evento en Calendar, el documento en Docs y (opcional) el email de confirmación.

Formato de entrega: link al proyecto de Apps Script compartido + grabación. Si no puedes compartir el script por políticas de la empresa, capturas del código y de los logs. Kit del módulo: procesarFormulario.gs, Resiliencia.gs, cloud-run-connector.gs.

⚠ Errores comunes
  • Error: los índices de e.values no coinciden con las columnas reales del formulario. Corrección: registra Logger.log(e.values) en la primera ejecución y mapea las columnas reales antes de confiar en los índices.
  • Error: probar procesarFormulario con el botón "Ejecutar" del editor — falla porque e llega undefined. Corrección: pruébalo enviando un formulario real, o construye un objeto e simulado para tests.
  • Error: elegir el evento de trigger equivocado ("Al abrir" en vez de "Al enviarse un formulario"). Corrección: verifica función + evento + fuente en la pantalla de Triggers antes de guardar.
  • Error: creer que el script falló cuando en realidad faltaba autorizar los permisos de Google la primera vez. Corrección: ejecuta la función una vez manualmente y acepta la pantalla de autorización OAuth.
  • Error: acumular triggers huérfanos en cada reintento del boilerplate. Corrección: llama siempre limpiarTriggers() antes de programar el siguiente — como hace el kit.
05
Rúbrica de evaluación Cómo saber si dominaste el módulo
Criterio No Aprobado (0) Aprobado (1) Sobresaliente (2)
1. El flujo Forms → Calendar → Docs funciona El script tiene errores de sintaxis, no se ejecuta, o no completa las 3 acciones (crear evento, crear doc, enviar email) Las 3 acciones se ejecutan pero hay errores menores (formato de fecha incorrecto, documento en la carpeta equivocada, email sin destinatario claro) Las 3 acciones se ejecutan correctamente, el evento tiene la duración y descripción correctas, el documento está en la carpeta adecuada, y el email llega al cliente con la información completa
2. El Boilerplate de Resiliencia pausa y reanuda No existe el Boilerplate o tiene errores que impiden la ejecución El Boilerplate existe pero la pausa no se activa (nunca llega a los 5 minutos) o se activa pero no guarda el progreso correctamente El Boilerplate guarda progreso en PropertiesService, programa un trigger, se detiene antes de los 6 minutos, y en una segunda ejecución retoma desde el índice correcto. Los logs demuestran el ciclo completo
3. El sistema está configurado con triggers reales No hay triggers configurados o los triggers apuntan a funciones incorrectas Hay al menos 1 trigger (Forms) pero el segundo (tiempo para lotes) no está configurado o no funciona Ambos triggers están configurados: el de formulario se ejecuta al recibir un submit, el de tiempo ejecuta el procesamiento por lotes periódicamente. La grabación demuestra el funcionamiento

Aprobación: 2 de 3 criterios en "Aprobado" o superior.

🔑 Lo esencial del módulo
  • Apps Script es JavaScript en los servidores de Google: cero instalación, cero costo, triggers nativos de Workspace.
  • El muro de los 6 minutos es real: todo script de lotes debe medir su tiempo y saber pausarse.
  • PropertiesService es la memoria entre ejecuciones: guarda el índice, retoma donde quedaste.
  • Apps Script es el pegamento; Cloud Run, el motor pesado. Usa el ligero hasta que pese.
  • 50 formularios a la semana: 16.7 horas manuales → 0 horas con el flujo automatizado.

Kit: Google Apps Script + GCP

ArchivoDescripción
⬇ procesarFormulario.gs Apps Script completo: Forms → Calendar → Docs
⬇ Resiliencia.gs Boilerplate anti-6-minutos para procesamiento por lotes
⬇ cloud-run-connector.gs Fragmento conector a Cloud Run

📁 kits/m09-apps-script/