Ir al contenido

Google Forms y Apps Script

El complemento de Sheets crea documentos cuando usted hace clic en Generar. Para crearlos sin que nadie haga clic, por ejemplo un certificado por cada respuesta de un formulario o un presupuesto cuando se marca una fila, un breve Apps Script envía los datos a la API de documentos de AutomaDoc y hace con el archivo lo que usted quiera: guardarlo en Drive, enviarlo por correo o escribir su enlace en la hoja.

El script se ejecuta en su cuenta de Google, con las reglas habituales de Apps Script de Google. AutomaDoc solo ve el JSON que envía.

  1. Una plantilla. Sirve cualquier plantilla de AutomaDoc: el JSON que envía el script es el registro, así que no se usa la fuente de datos propia de la plantilla. Sus marcadores llevan el nombre de las claves que envía el script (consulte Nombres).
  2. El ID de la plantilla. En el Portal de empresa, abra Plantillas, abra el menú de la plantilla y elija Copiar ID de la plantilla.
  3. Una clave de API. Una clave de API de la empresa, que crea un administrador en el Portal de empresa en Claves de API (empieza por amk_ y se muestra una sola vez), o, para una plantilla de Lark Base, la clave propia de esa plantilla, que solo abre esa plantilla. Cuanto menor sea el alcance de la clave, mejor: cualquiera que pueda editar el script puede leerla.

Receta: un documento por cada respuesta de un formulario

Sección titulada «Receta: un documento por cada respuesta de un formulario»

Este script está vinculado al formulario. Por cada respuesta crea un PDF, lo guarda en una carpeta de Drive y lo envía por correo a la persona que respondió.

  1. Abra el formulario en Google Forms, abra el menú ⋮ y elija Apps Script.
  2. En el editor de Apps Script, abra Configuración del proyecto y añada una propiedad de secuencia de comandos llamada AUTOMADOC_API_KEY con su clave como valor. Así la clave queda fuera del código.
  3. Sustituya el contenido de Code.gs por el script siguiente y rellene TEMPLATE_ID y FOLDER_ID (la última parte de la dirección de la carpeta de Drive, después de /folders/).
  4. Elija setUp en la lista de funciones y haga clic en Ejecutar una vez. Google le pide que permita al script conectarse a un servicio externo, usar su Drive y enviar correos en su nombre.

A partir de entonces, cada envío ejecuta handleSubmit. Sus ejecuciones y errores aparecen en Ejecuciones, en el editor.

const AUTOMADOC_API = 'https://api.automadoc.cc';
const TEMPLATE_ID = 'PASTE-THE-TEMPLATE-ID';
const FOLDER_ID = 'PASTE-THE-DRIVE-FOLDER-ID';
/** Run once: creates the trigger that calls handleSubmit on every response. */
function setUp() {
ScriptApp.newTrigger('handleSubmit')
.forForm(FormApp.getActiveForm())
.onFormSubmit()
.create();
}
function handleSubmit(e) {
const response = e.response;
// One key per question: its title is the placeholder's name.
const data = {};
response.getItemResponses().forEach(function (itemResponse) {
data[itemResponse.getItem().getTitle()] = answerOf(itemResponse.getResponse());
});
data['Submitted'] = Utilities.formatDate(
response.getTimestamp(), Session.getScriptTimeZone(), 'yyyy-MM-dd HH:mm');
const doc = createDocument(data);
const file = DriveApp.getFolderById(FOLDER_ID).createFile(downloadFile(doc));
console.log('Saved ' + file.getName() + ': ' + file.getUrl());
// Needs "Collect email addresses" in the form's settings.
const email = response.getRespondentEmail();
if (email) {
MailApp.sendEmail({
to: email,
subject: doc.filename,
body: 'Thank you. Your document is attached.',
attachments: [file.getBlob()],
});
}
}
/** Grid questions answer with a list of lists, which the API refuses: one text instead. */
function answerOf(value) {
if (Array.isArray(value) && value.some(Array.isArray)) {
return value.map(function (row) { return [].concat(row).join(', '); }).join('; ');
}
return value;
}
/** Fills the template and waits until the document is finished. */
function createDocument(data) {
let doc = callApi(AUTOMADOC_API + '/api/v1/templates/' + TEMPLATE_ID + '/documents?wait=20', {
method: 'post',
contentType: 'application/json',
payload: JSON.stringify({ data: data, format: 'pdf' }),
});
// Not finished within the 20 seconds: read its status every 5 seconds, for up to 4 minutes.
for (let i = 0; doc.status === 'processing' && i < 48; i++) {
Utilities.sleep(5000);
doc = callApi(doc.statusUrl);
}
if (doc.status !== 'completed') {
throw new Error(doc.error || 'The document is still being made: ' + doc.statusUrl);
}
if (doc.warnings) console.log(doc.warnings.join('\n'));
return doc;
}
/** The finished file, as a blob named like the document. */
function downloadFile(doc) {
const response = UrlFetchApp.fetch(doc.statusUrl + '/file', {
headers: { 'X-Webhook-API-Key': apiKey() },
muteHttpExceptions: true,
});
if (response.getResponseCode() !== 200) {
throw new Error('Download failed: ' + response.getContentText());
}
return response.getBlob().setName(doc.filename);
}
function callApi(url, options) {
options = options || {};
options.headers = { 'X-Webhook-API-Key': apiKey() };
options.muteHttpExceptions = true;
const response = UrlFetchApp.fetch(url, options);
const body = JSON.parse(response.getContentText());
if (response.getResponseCode() >= 400) throw new Error(body.code + ': ' + body.error);
return body;
}
function apiKey() {
return PropertiesService.getScriptProperties().getProperty('AUTOMADOC_API_KEY');
}

Qué ocurre en cada llamada:

  • POST /api/v1/templates/{templateId}/documents?wait=20 envía { "data": …, "format": "pdf" }. Con wait, la respuesta llega cuando el documento está terminado (200) o al cabo de 20 segundos (202, todavía processing). format también puede ser "native" (el formato propio de la plantilla: Word, Excel o PowerPoint) o "png", y filename define un patrón de nombre de archivo para esta llamada.
  • GET /api/v1/documents/{generationId} (la statusUrl de la respuesta) lee su status: processing, completed o failed, con el motivo en error. Un documento completado tiene un filename y una downloadUrl.
  • GET /api/v1/documents/{generationId}/file devuelve el propio archivo, con la misma clave.

Si solo necesita un enlace, omita downloadFile: doc.downloadUrl abre el archivo sin clave, mientras su empresa conserve los documentos generados (30 días si se conservan indefinidamente). Cada lectura del estado da uno nuevo.

Receta: un documento al marcar una casilla en una hoja

Sección titulada «Receta: un documento al marcar una casilla en una hoja»

Los mismos auxiliares funcionan desde una hoja de cálculo. Ábrala, elija Extensiones → Apps Script, añada la propiedad de secuencia de comandos y pegue los auxiliares anteriores (createDocument, downloadFile, callApi, apiKey y las tres constantes) con esto en lugar de setUp y handleSubmit.

Aquí la pestaña se llama Orders. La fila 1 contiene los nombres de las columnas; una columna de casillas llamada Generate inicia un documento, y una columna llamada Document recibe su enlace.

const SHEET_NAME = 'Orders';
/** Run once: creates the trigger that calls handleEdit on every edit. */
function setUp() {
ScriptApp.newTrigger('handleEdit')
.forSpreadsheet(SpreadsheetApp.getActive())
.onEdit()
.create();
}
function handleEdit(e) {
const sheet = e.range.getSheet();
// Only a single checkbox ticked on a data row of the right tab
if (sheet.getName() !== SHEET_NAME || e.range.getRow() < 2 || e.value !== 'TRUE') return;
const headers = sheet.getRange(1, 1, 1, sheet.getLastColumn()).getDisplayValues()[0];
if (headers[e.range.getColumn() - 1] !== 'Generate') return;
const row = e.range.getRow();
const values = sheet.getRange(row, 1, 1, headers.length).getDisplayValues()[0];
const data = {};
headers.forEach(function (header, i) {
if (header && header !== 'Generate' && header !== 'Document') data[header] = values[i];
});
const doc = createDocument(data);
const file = DriveApp.getFolderById(FOLDER_ID).createFile(downloadFile(doc));
const linkColumn = headers.indexOf('Document') + 1;
if (linkColumn > 0) sheet.getRange(row, linkColumn).setValue(file.getUrl());
}

El activador debe ser el instalable que crea setUp. Una función llamada onEdit se ejecutaría como activador simple, y Google no permite que un activador simple llame a servicios externos.

getDisplayValues() envía cada celda tal como la muestra la hoja, así que los números llegan como texto con el formato de la hoja. Para usar filtros como number, currency o sum en una columna, envíe el valor de esa columna desde getValues().

Cada clave se convierte en el nombre de un marcador igual que lo hace un nombre de columna o de campo: los espacios, los puntos y otros símbolos se convierten en _, se conservan las letras y los dígitos de cualquier idioma, y se distinguen mayúsculas y minúsculas.

Pregunta o columna Marcador
Full name {Full_name}
Invoice No. {Invoice_No_}
Submitted (añadido por el script) {Submitted}

Un título sin ninguna letra ni dígito se rechaza (DATA_INVALID_NAME), y dos títulos que se convierten en el mismo nombre se rechazan (DATA_NAME_COLLISION). Una pregunta de casillas responde con una lista, que se imprime unida con comas.

Las fechas enviadas como texto sin zona horaria, como 2026-10-17 09:30, se leen en la zona horaria de la plantilla, así que el filtro date puede darles otro formato.

Las imágenes deben ser direcciones web públicas (https://, o http://), enviadas donde la plantilla tiene un marcador de imagen. Los archivos subidos mediante una pregunta de un formulario son archivos privados en el Drive del propietario del formulario: AutomaDoc no puede obtenerlos, y su respuesta es solo los ID de los archivos. Deje esas preguntas fuera de la plantilla.

Solicitudes por plantilla 60 por minuto
Documentos por empresa 600 por minuto, sumando todas las automatizaciones
Lecturas de estado por documento 60 por minuto; las descargas de archivos tienen sus propias 60
wait 25 segundos como máximo
Cuerpo de la solicitud 1 MB
Caracteres en un valor 100 000

Un rechazo llega con un code y un error, y el script anterior se detiene con ambos en su registro de Ejecuciones. RATE_LIMITED (429) significa demasiadas llamadas en el minuto: un formulario que puede recibir muchas respuestas a la vez debería capturarlo y volver a intentarlo un poco más tarde. La referencia de la API enumera todos los códigos.

Google detiene una ejecución de un script a los 6 minutos; por eso el script deja de leer el estado a los 4. Un documento que todavía se esté creando en ese momento no se pierde: su statusUrl, incluida en el error, sigue respondiendo.

Los documentos creados así son documentos automatizados. Se descuentan primero de la cuota de automatización de sus puestos Business, después de la cuota mensual gratuita y después de sus paquetes de documentos; cuando las tres se agotan, la API responde QUOTA_EXCEEDED. En el plan Gratuito llevan el pie de página “Generated with AutomaDoc”, salvo que su empresa tenga un paquete comprado.