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.
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 |
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
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:
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 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.
-
Preparar el entorno
- Ve a script.google.com.
- Haz clic en "Nuevo proyecto".
- Nómbralo:
Flujo_Proyectos_Agencia. - El editor se abre. Ahí escribes el código.
-
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 FormsTrigger: "Al enviarse un formulario" →procesarFormulario(e)↓ ↓ ↓📅 CalendarEvento de kickoff con fecha del formulario y 1 h de duración.📄 DocsDocumento de alcance generado desde plantilla, en la carpeta del proyecto.✉️ GmailEmail 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.TipNo copies el código a ciegas: el bloque
CONFIGdel 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); } -
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(); } -
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).
- Función:
-
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); } }
4.1. El Flujo Automatizado de una Agencia de Marketing
- El cliente llena un Forms.
- El PM recibe el email.
- El PM abre Calendar, crea evento.
- El PM abre Docs, copia plantilla, escribe.
- El PM escribe email al cliente.
Tiempo: 20 minutos por cliente.
- El cliente llena un Forms.
- Apps Script ejecuta
procesarFormulario. - Calendar: evento creado.
- Docs: documento generado.
- Gmail: confirmación enviada.
Tiempo: 0 minutos del PM.
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
Parte A — Script de flujo base. Un Apps Script que, al recibir un formulario:
- Cree un evento en Calendar.
- Cree un documento en Docs con los datos del formulario.
- Envíe un email de confirmación.
- Código con manejo de errores (
try/catchalrededor 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.
- Error: los índices de
e.valuesno coinciden con las columnas reales del formulario. Corrección: registraLogger.log(e.values)en la primera ejecución y mapea las columnas reales antes de confiar en los índices. - Error: probar
procesarFormulariocon el botón "Ejecutar" del editor — falla porqueellega undefined. Corrección: pruébalo enviando un formulario real, o construye un objetoesimulado 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.
| 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.
- 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.
PropertiesServicees 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
| Archivo | Descripció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 |