Convertir una fila aprobada de Google Sheets en un borrador de Blogger

Una hoja editorial puede funcionar como una cola de revisión sencilla: una persona prepara el título y el HTML, cambia el estado a aprobado y n8n crea un borrador en Blogger. Este tutorial usa la API oficial de Blogger mediante HTTP Request y fuerza isDraft=true, de modo que la revisión final sigue siendo manual.

El diseño también guarda el ID que devuelve Blogger en la misma fila. Tras una ejecución serial satisfactoria, esa marca evita que una nueva ejecución procese de nuevo el mismo content_id. No cubre un fallo entre crear el borrador y guardar el ID, ni dos ejecuciones simultáneas.

Estado de verificación: la prueba controlada quedó registrada el 2026-09-13 con n8n 2.22.5. Se usó un blog DEMO privado, una fila aprobada y una credencial dedicada con la cuenta conectada y el alcance de Blogger. La segunda ejecución serial no creó otro borrador. La prueba no acredita seguridad ante ejecuciones concurrentes ni autoriza activar el Schedule.

Resultado que obtendrás

  • Una hoja Editorial con una fila de ejemplo aprobada y sin blogger_post_id.
  • Un workflow que selecciona solo filas aprobadas que aún no tienen ID de Blogger.
  • Una llamada POST a posts.insert con isDraft=true y una actualización de la fila con el ID devuelto.
  • Un control serial para no publicar automáticamente ni volver a enviar una fila que ya tenga un ID guardado.

Antes de comenzar

  • Una instancia de n8n y un documento de Google Sheets de prueba.
  • Un blog de Blogger de prueba y su blogId. En este artículo se usa el marcador REEMPLAZAR_BLOG_ID; no lo sustituyas por un ID real dentro de una captura o exportación compartida.
  • Una credencial dedicada de Google para el nodo HTTP Request, configurada como Predefined Credential Type con Credential Type: Google OAuth2 API, autorizada para Blogger con el alcance https://www.googleapis.com/auth/blogger. La cuenta debe mostrar Account connected y tener acceso al blog de prueba. La autorización se configura en tu entorno; no se incluye ningún secreto.
  • Una pestaña Editorial con estos encabezados:
    content_id | titulo | html | etiquetas | estado | blogger_post_id | draft_url
    
  • Una sola fila de prueba:
    contenido-demo-001 | Webhooks de prueba | <h2>Una prueba controlada</h2><p>Contenido sin datos personales.</p> | n8n, borrador | aprobado |  |
    
  • La API de Blogger permite isDraft=true en posts.insert; no uses el endpoint de publicación en este flujo.

Flujo completo

Manual Trigger
      |
      v
Google Sheets: Get Row(s) de Editorial
      |
      v
Code: estado aprobado y sin ID
      |
      +------------------------------+
      |                              |
      v                              |
HTTP Request: Blogger posts.insert  +--> Merge: combinar respuesta y fila
                                     |
                                     v
                         Code: preparar actualización
                                     |
                                     v
                  Google Sheets: Update Row
                         (clave = content_id)

El Merge conserva los datos de la fila mientras incorpora la respuesta de Blogger. Si tu versión de n8n ofrece una opción equivalente para incluir los datos de entrada en HTTP Request, puedes usarla; en ambos casos, comprueba que content_id siga presente antes de escribir en Sheets.

Paso 1: leer la cola editorial

  1. Añade un Manual Trigger. No uses Schedule Trigger hasta completar la prueba manual.
  2. Conecta un nodo Google Sheets y selecciona la operación Get Row(s) (el nombre puede aparecer como “Get Rows”).
  3. Elige el documento de prueba y la pestaña Editorial.
  4. Usa la credencial de Google autorizada para ese documento.

La primera fila debe contener los encabezados. No incluyas una fila vacía antes de ellos. Devuelve una fila a la vez si tu documento contiene más datos durante la prueba.

Paso 2: aplicar la compuerta de idempotencia

Conecta un nodo Code después de Sheets, selecciona Run Once for All Items y pega este filtro:

const candidates = $input.all().filter((item) => {
  const estado = String(item.json?.estado ?? '').trim().toLowerCase();
  const postId = String(item.json?.blogger_post_id ?? '').trim();
  return estado === 'aprobado' && !postId;
});

return candidates.map((item) => {
  const row = item.json ?? {};
  const contentId = String(row.content_id ?? '').trim();
  const titulo = String(row.titulo ?? '').trim();
  const html = String(row.html ?? '').trim();
  const missing = [];

  if (!contentId) missing.push('content_id');
  if (!titulo) missing.push('titulo');
  if (!html) missing.push('html');

  if (missing.length) {
    throw new Error(`La fila aprobada no puede enviarse a Blogger; faltan: ${missing.join(', ')}`);
  }

  return {
    ...item,
    json: {
      ...row,
      content_id: contentId,
      titulo,
      html,
    },
  };
});

Si ninguna fila cumple las condiciones, la salida será vacía y no habrá llamada a Blogger. Una fila aprobada que sí pasaría el filtro, pero no tiene content_id, titulo o html, detiene el nodo antes del POST y muestra los campos faltantes. Eso evita enviar un borrador incompleto.

La validación detiene el flujo antes del envío, pero en un flujo editorial puedes añadir una rama de revisión que marque el error en otra columna. No borres silenciosamente contenido real mientras ajustas el prototipo.

Paso 3: preparar HTTP Request hacia Blogger

Conecta un nodo HTTP Request al Code. Configura:

  • Method: POST.
  • URL: https://www.googleapis.com/blogger/v3/blogs/REEMPLAZAR_BLOG_ID/posts.
  • Send Query Parameters: activado, con nombre isDraft y valor true.
  • Authentication: Predefined Credential Type.
  • Credential Type: Google OAuth2 API (googleOAuth2Api). Selecciona la credencial dedicada que muestre Account connected y que fue autorizada con el alcance Blogger indicado en los prerrequisitos. No añadas un Authorization: Bearer ... manual.
  • Send Body: activado.
  • Body Content Type: Raw; Content Type: application/json.
  • Body, en modo expresión:
    ={{ JSON.stringify({
      title: String($json.titulo ?? ''),
      content: String($json.html ?? ''),
      labels: String($json.etiquetas ?? $json.labels ?? '')
        .split(',')
        .map((label) => label.trim())
        .filter(Boolean),
    }) }}
    
  • Headers: Content-Type: application/json.

El blogId es un parámetro obligatorio. isDraft=true es obligatorio para el objetivo de este artículo; no lo elimines ni lo cambies a false. La API espera un recurso Post cuyo content puede contener HTML. Usa únicamente HTML editorial confiable y revisado; no trates HTML recibido de usuarios como seguro por defecto.

Conecta también la salida del Code de filtro a la primera entrada del nodo Merge y la salida de HTTP Request a la segunda. Configura Merge en Combine → By Position (o la opción equivalente “por posición”). Con una fila, la respuesta y la entrada ocupan la misma posición y se pueden combinar. Antes de continuar, inspecciona la salida: debe conservar content_id y mostrar el id devuelto por Blogger.

Paso 4: guardar el resultado en Sheets

Después de Merge, añade un nodo Code con Run Once for Each Item:

const postId = String($json.id ?? '').trim();
const contentId = String($json.content_id ?? '').trim();

if (!contentId) {
  throw new Error('Merge no conservó content_id; no se debe actualizar la fila');
}

if (!postId) {
  throw new Error('Blogger no devolvió id; no se debe marcar la fila como procesada');
}

return {
  json: {
    content_id: contentId,
    titulo: String($json.titulo ?? '').trim(),
    html: String($json.html ?? '').trim(),
    estado: 'borrador_creado',
    blogger_post_id: postId,
    draft_url: String($json.url ?? $json.selfLink ?? '').trim(),
  },
};

Conecta otro nodo Google Sheets:

  1. Selecciona la misma credencial, documento y pestaña Editorial.
  2. Usa Update Row para actualizar la fila existente, sin añadir otra.
  3. En Column to Match On, selecciona content_id.
  4. Usa Map Each Column Manually y conserva solo estos cuatro campos: content_id, estado, blogger_post_id y draft_url, con sus valores ={{ $json.nombre_del_campo }}. Retira titulo, html, etiquetas y row_number del mapeo si aparecen. El título y HTML ya pertenecen a la fila y se usan para construir el POST.

Si Blogger responde con error, el Code no debe ejecutarse y la fila no debe marcarse como borrador_creado. Investiga el error antes de volver a procesar la fila. Si la API creó el borrador pero la escritura en Sheets falló, busca el ID en la respuesta de la ejecución antes de reintentar para no crear un segundo borrador; la compuerta no puede adivinar un ID que aún no se guardó.

Paso 5: configurar la prueba y la programación

Usa Manual Trigger hasta comprobar el resultado. Cuando la prueba sea satisfactoria, puedes sustituirlo por un Schedule Trigger con la frecuencia que necesites. Un workflow programado debe estar activo/publicado para que el disparador se ejecute; la activación no forma parte de esta prueba y requiere una decisión editorial.

No conectes ningún nodo de Blogger publish, ni llames a posts/{postId}/publish. El siguiente paso, publicar, debe ser una acción separada con revisión humana.

Cómo comprobar que funciona: resultado de la prueba controlada

La prueba real se ejecutó el 2026-09-13 en n8n 2.22.5 con una sola fila editorial aprobada sin ID previo. El blog DEMO era privado y no tenía entradas antes de la prueba. La credencial dedicada de Google OAuth2 API mostró Account connected con el alcance Blogger.

  1. La primera ejecución dejó pasar una fila, hizo un POST con isDraft=true, creó un borrador y no publicó ninguna entrada.
  2. La respuesta devolvió el ID de Blogger; la actualización de Sheets conservó titulo, html y etiquetas, guardó ese ID y cambió estado a borrador_creado.
  3. La segunda ejecución serial devolvió No output data returned en el filtro porque la fila ya tenía ID. No hubo un segundo POST, Sheets permaneció idéntico y Blogger conservó un borrador y cero publicaciones.
  4. La comprobación del borrador se hizo desde Blogger por su ID. En esta respuesta de la API, draft_url contenía la portada del blog, no un enlace directo de edición; no uses ese campo como enlace de edición sin verificarlo en Blogger.

Al finalizar, el workflow quedó sin publicar, el Schedule deshabilitado y los tres nodos de escritura desactivados. El resultado prueba el reprocesamiento serial de esta fila; no prueba seguridad ante ejecuciones concurrentes.

Errores habituales

Blogger responde 401 o 403

  • Causa probable: la credencial no está autorizada, falta el alcance https://www.googleapis.com/auth/blogger o la cuenta no administra el blog.
  • Comprobación: revisa la credencial y la cuenta en n8n, y consulta el detalle de la respuesta sin copiar tokens a un registro público.
  • Corrección mínima: vuelve a autorizar una cuenta de prueba con acceso al blog y el alcance correcto. No sustituyas OAuth por una API key para una operación de escritura.

Blogger responde 404

  • Causa probable: REEMPLAZAR_BLOG_ID no se cambió por el ID correcto o la cuenta autorizada no puede ver ese blog.
  • Comprobación: confirma el ID desde la cuenta de prueba y revisa que la URL conserve /blogs/{blogId}/posts.
  • Corrección mínima: usa el ID del blog de prueba en la credencial correcta; no publiques ni compartas ese dato si es privado.

La respuesta no contiene id

  • Causa probable: el cuerpo no se interpretó como JSON, el Merge ocultó la respuesta o la API devolvió un error que se trató como éxito.
  • Comprobación: inspecciona la salida cruda de HTTP Request y del Merge.
  • Corrección mínima: revisa el cuerpo JSON, las dos entradas del Merge y conserva el throw del Code para no actualizar la fila sin ID.

Se crea más de un borrador para la misma fila

  • Causa probable: blogger_post_id no se escribió, la columna de coincidencia no es content_id o dos ejecuciones concurrentes pasaron la compuerta antes de actualizar Sheets.
  • Comprobación: busca el content_id y los IDs en Blogger y revisa el historial de ejecuciones.
  • Corrección mínima: detén el Schedule, conserva el ID del primer borrador y vuelve a dejar la fila fuera de la cola. Para alta concurrencia, añade un mecanismo de bloqueo o reserva transaccional; Sheets por sí solo no ofrece esa garantía.

La fila se marca como creada pero no aparece en Borradores

  • Causa probable: se usó isDraft=false, se llamó un endpoint de publicación o se revisó otro blog/cuenta.
  • Comprobación: inspecciona la URL completa y la cuenta que recibe la respuesta.
  • Corrección mínima: fuerza isDraft=true, elimina cualquier paso de publicación y vuelve a probar solo con una fila.

Seguridad y privacidad

  • Blogger es una API de escritura: trata la credencial OAuth como un secreto y no la exportes con el workflow. La API requiere autorización para insertar posts.
  • Usa REEMPLAZAR_BLOG_ID en documentación, capturas y plantillas públicas. Los IDs reales y URLs privadas deben permanecer en el entorno del propietario.
  • No envíes HTML sin revisar. Sanitiza o limita el contenido si la hoja puede ser editada por personas que no pertenecen al equipo editorial.
  • Mantén el modo borrador y una revisión humana hasta validar títulos, enlaces, imágenes, etiquetas y contenido.
  • Separa la credencial de Blogger de la de Sheets cuando los permisos deban tener dueños distintos. Concede acceso de edición solo al documento de prueba durante la configuración.
  • Un reintento después de una respuesta ambigua puede crear otro borrador. Registra el resultado de la ejecución y busca el id antes de reintentar.
  • No programes ni actives el workflow como parte de la primera prueba. La activación es una acción externa que puede afectar contenido editorial.

Fuentes oficiales