De RSS a Telegram y X: cómo automatizar un flujo de publicación sin perder control editorial

Tutorial para automatizar la publicación de contenido desde RSS a Telegram y X, con validación humana y control editorial real.

Cover for De RSS a Telegram y X: cómo automatizar un flujo de publicación sin perder control editorial
Actualizado: 18 may 2026

Tengo un canal de Telegram y una cuenta de X donde publico contenido técnico curado. Durante un tiempo lo hacía todo a mano: leía artículos, seleccionaba los que me parecían interesantes, escribía un breve comentario y los publicaba. Funcionaba, pero me comía entre 30 y 45 minutos cada día solo en eso. Y lo peor no era el tiempo: era la inconsistencia. Los días que no tenía ganas, no publicaba. Y un canal que publica de forma irregular pierde audiencia rápido.

Así que monté un sistema automatizado. No para que publicara solo, sino para que me preparara todo y yo solo tuviera que aprobar o descartar. Esa diferencia es importante: no busco un bot que spamee enlaces. Busco un asistente que me ahorre la parte mecánica y me deje centrarme en el criterio editorial.

Este artículo explica el flujo completo que uso, basado en lo que construí para Rolsfera. Si gestionas un canal de contenido técnico y quieres automatizar sin perder el control, esto te va a ahorrar bastantes horas de prueba y error.


El flujo completo

Antes de entrar en cada pieza, este es el flujo de principio a fin:

RSS Feeds → Parseo → Filtrado → Resumen (IA) → Cola de revisión → Aprobación humana → Formateo → Publicación (Telegram / X)

Parece simple. Y conceptualmente lo es. La complejidad real está en los detalles de cada paso: cómo filtras, cómo resumes, cómo formateas para cada plataforma, cómo gestionas errores y duplicados.

Vamos pieza por pieza.


Paso 1: Recoger contenido desde RSS

El punto de entrada son feeds RSS. Tengo una lista de unas 40 fuentes que sigo: blogs técnicos, medios especializados, newsletters que publican vía RSS, repos de GitHub con releases feed y algún subreddit vía RSS.

# feeds.py - Lista de fuentes con metadatos
FEEDS = [
    {
        "url": "https://blog.pragmaticengineer.com/rss/",
        "name": "Pragmatic Engineer",
        "category": "engineering",
        "priority": "high",
    },
    {
        "url": "https://martinfowler.com/feed.atom",
        "name": "Martin Fowler",
        "category": "architecture",
        "priority": "high",
    },
    {
        "url": "https://news.ycombinator.com/rss",
        "name": "Hacker News",
        "category": "general",
        "priority": "medium",
    },
    # ... 37 fuentes más
]

Cada fuente tiene una categoría y una prioridad. La prioridad no es arbitraria: la ajusto según el histórico de artículos que acabo aprobando de cada fuente. Si el 80% de lo que publica Pragmatic Engineer me parece publicable, es high. Si de Hacker News solo publico un 10%, es medium.

El parseo lo hago con feedparser en Python. n8n dispara el proceso cada 30 minutos a través de un cron trigger que llama a un endpoint HTTP de mi servicio.

import feedparser
from datetime import datetime, timedelta

def fetch_new_articles(feed_url: str, since_hours: int = 2) -> list[dict]:
    feed = feedparser.parse(feed_url)
    cutoff = datetime.utcnow() - timedelta(hours=since_hours)
    articles = []

    for entry in feed.entries:
        published = entry.get("published_parsed")
        if published:
            pub_date = datetime(*published[:6])
            if pub_date < cutoff:
                continue

        articles.append({
            "title": entry.get("title", "").strip(),
            "url": entry.get("link", ""),
            "summary": entry.get("summary", ""),
            "published": entry.get("published", ""),
        })

    return articles

Paso 2: Filtrado y deduplicación

No todo lo que entra vale la pena procesarlo. El filtrado tiene dos niveles:

Deduplicación. Si el mismo artículo ya está en la base de datos (por URL o por hash de contenido), se descarta. Esto es crítico porque muchos feeds comparten las mismas noticias.

Filtrado por relevancia básica. Antes de gastar tokens de IA, aplico filtros simples:

# Palabras clave que indican contenido relevante para mi audiencia
INCLUDE_KEYWORDS = [
    "python", "backend", "api", "architecture", "kubernetes",
    "database", "automation", "scraping", "llm", "self-hosted",
    "devops", "data engineering", "microservices",
]

# Contenido que normalmente descarto
EXCLUDE_PATTERNS = [
    "sponsored", "advertisement", "podcast episode",
    "weekly roundup",  # demasiado genérico
]

def passes_basic_filter(article: dict) -> bool:
    text = f"{article['title']} {article['summary']}".lower()

    for pattern in EXCLUDE_PATTERNS:
        if pattern in text:
            return False

    for keyword in INCLUDE_KEYWORDS:
        if keyword in text:
            return True

    return False  # si no coincide con nada relevante, no pasa

Este filtrado es tosco. Lo sé. Pero reduce el volumen de artículos que llegan al paso de IA en un 60-70%, lo que tiene un impacto directo en coste y tiempo de procesamiento.


Paso 3: Resumen con IA

Los artículos que pasan el filtro se envían a un LLM para generar un resumen corto y una clasificación. El prompt está diseñado para obtener exactamente lo que necesito para decidir si publicar:

def generate_summary(article: dict) -> dict:
    prompt = f"""Eres un editor técnico. Analiza este artículo y responde en JSON:

Título: {article['title']}
Contenido: {article['content'][:2500]}

Responde con:
{{
  "summary": "Resumen de 2-3 frases, directo, sin relleno",
  "topic": "Tema principal (ej: Python, DevOps, arquitectura)",
  "is_actionable": true/false,  // ¿aporta algo práctico?
  "suggested_comment": "Frase de 1 línea que usaría al compartirlo"
}}

NO incluyas frases genéricas tipo 'Este artículo explora...'
Sé directo y concreto."""

    response = call_llm(prompt, model="gpt-4o-mini")
    return json.loads(response)

Uso gpt-4o-mini para esta tarea porque no necesito el modelo más potente. Un resumen de 2-3 frases y una clasificación es algo que los modelos pequeños hacen bien. Reservo modelos más grandes para cuando necesito generar contenido original o hacer análisis más complejos.

El prompt importa más de lo que parece. Añadir “NO incluyas frases genéricas” fue la diferencia entre obtener resúmenes útiles y obtener relleno tipo “Este interesante artículo examina…”.


Paso 4: Cola de revisión

Los artículos procesados llegan a una cola donde los reviso. En la práctica es una tabla en PostgreSQL con una interfaz web mínima encima:

SELECT
    a.title,
    a.url,
    a.ai_metadata->>'summary' AS summary,
    a.ai_metadata->>'suggested_comment' AS comment,
    a.ai_metadata->>'topic' AS topic,
    a.source_name,
    a.created_at
FROM articles a
WHERE a.status = 'pending'
ORDER BY
    CASE
        WHEN a.source_priority = 'high' THEN 1
        WHEN a.source_priority = 'medium' THEN 2
        ELSE 3
    END,
    a.created_at DESC;

La revisión me lleva entre 5 y 10 minutos al día. Los artículos de fuentes de alta prioridad aparecen primero. Para cada uno, hago una de tres cosas: aprobar (a veces editando el comentario sugerido por la IA), descartar o guardar para más tarde.


Paso 5: Formateo por plataforma

Cada plataforma tiene sus reglas. Lo que funciona en Telegram no funciona en X y viceversa.

Telegram permite mensajes largos, Markdown, emojis y enlaces con preview. Mi formato típico:

def format_for_telegram(article: dict) -> str:
    comment = article["ai_metadata"]["suggested_comment"]
    title = article["title"]
    url = article["url"]
    topic = article["ai_metadata"]["topic"]

    return f"""🔗 *{title}*

{comment}

📌 Tema: {topic}

👉 [Leer artículo]({url})"""

X (Twitter) tiene límite de 280 caracteres. El formato es más compacto:

def format_for_x(article: dict) -> str:
    comment = article["ai_metadata"]["suggested_comment"]
    url = article["url"]
    topic = article["ai_metadata"]["topic"]

    # X cuenta caracteres, los URLs ocupan ~23
    max_comment_len = 280 - 23 - len(f" #{topic}") - 5
    if len(comment) > max_comment_len:
        comment = comment[:max_comment_len - 3] + "..."

    return f"{comment} #{topic} {url}"

Paso 6: Publicación

La publicación la gestiona n8n. Cuando marco un artículo como aprobado, su estado cambia en la base de datos. Un workflow de n8n que corre cada 5 minutos detecta artículos aprobados y los publica.

El flujo en n8n es simple:

  1. Trigger cron cada 5 minutos
  2. HTTP Request al endpoint que devuelve artículos aprobados
  3. Split In Batches para no publicar todo de golpe
  4. Telegram Node para enviar al canal
  5. HTTP Request a la API de X para publicar el tweet
  6. HTTP Request para actualizar el estado a published

La parte de “no publicar todo de golpe” es importante. Si apruebo 8 artículos por la mañana y se publican todos a la vez, la experiencia para los seguidores es mala. Uso un sistema de slots temporales: máximo 2 publicaciones por hora, con un mínimo de 20 minutos entre ellas.

# Lógica de scheduling simplificada
from datetime import datetime, timedelta

def get_next_publish_slot(last_published_at: datetime) -> datetime:
    min_gap = timedelta(minutes=20)
    next_slot = last_published_at + min_gap

    # No publicar entre las 23:00 y las 08:00
    if next_slot.hour >= 23 or next_slot.hour < 8:
        next_slot = next_slot.replace(hour=8, minute=0, second=0)
        if next_slot <= last_published_at:
            next_slot += timedelta(days=1)

    return next_slot

Dónde usar n8n y dónde Python

Una duda que me surgió al principio fue hasta dónde llevar la lógica en n8n y cuándo moverla a Python. Después de varias iteraciones, mi regla es esta:

TareaHerramientaPor qué
Orquestación (triggers, scheduling)n8nInterfaz visual, fácil de modificar
Llamadas a APIs externas simplesn8nNodos nativos para Telegram, HTTP
Parseo de RSSPythonfeedparser es más robusto que el nodo RSS de n8n
Filtrado y deduplicaciónPythonLógica compleja con acceso a BD
Procesamiento con IAPythonControl fino sobre prompts y respuestas
Formateo de contenidoPythonLógica de plantillas y truncado
Monitorización y alertasn8nError handling visual, fácil de depurar

La regla general: n8n para orquestar, Python para procesar. Cuando intentas meter lógica de procesamiento compleja en nodos de n8n, acabas con un workflow ilegible. Y cuando intentas replicar la orquestación visual de n8n en código, acabas reinventando un scheduler.


Errores comunes (y cómo los gestiono)

Rate limits

Tanto la API de Telegram como la de X tienen límites de peticiones. Telegram es bastante permisivo con bots, pero X es estricto. Mi solución: un sistema de cola con reintentos exponenciales.

import time

def publish_with_retry(publish_fn, content, max_retries=3):
    for attempt in range(max_retries):
        try:
            return publish_fn(content)
        except RateLimitError as e:
            wait_time = (2 ** attempt) * 30  # 30s, 60s, 120s
            print(f"Rate limit. Reintentando en {wait_time}s...")
            time.sleep(wait_time)
    raise PublishError(f"Fallo tras {max_retries} intentos")

Duplicados que se cuelan

A veces el mismo artículo aparece en varios feeds con URLs ligeramente distintas (con parámetros UTM, por ejemplo). La solución es normalizar URLs antes de comparar:

from urllib.parse import urlparse, urlunparse, parse_qs, urlencode

def normalize_url(url: str) -> str:
    parsed = urlparse(url)
    # Eliminar parámetros de tracking
    params = parse_qs(parsed.query)
    clean_params = {
        k: v for k, v in params.items()
        if not k.startswith("utm_")
    }
    clean_query = urlencode(clean_params, doseq=True)
    return urlunparse(parsed._replace(query=clean_query, fragment=""))

Formato roto en publicaciones

Markdown que se renderiza mal en Telegram, tweets que exceden el límite de caracteres, enlaces que no generan preview. Esto lo descubrí con el tiempo. La solución fue añadir validaciones antes de publicar:

def validate_telegram_message(text: str) -> bool:
    if len(text) > 4096:
        return False
    # Verificar que el Markdown está balanceado
    if text.count("*") % 2 != 0:
        return False
    if text.count("_") % 2 != 0:
        return False
    return True

Bots baneados

Me pasó una vez con X. Publicaba demasiado rápido y la cuenta fue suspendida temporalmente. Desde entonces: máximo 10 publicaciones al día, con distribución horaria, y nunca publico exactamente en intervalos regulares (añado un jitter aleatorio de 1-5 minutos para no parecer un bot).


La revisión humana: por qué no lo hago todo automático

Podría quitar el paso de revisión y dejar que el sistema publique todo lo que pase los filtros. Técnicamente es trivial. Pero no lo hago por tres razones:

  1. La IA se equivoca. No siempre, pero lo suficiente como para que un canal sin supervisión acabe publicando contenido irrelevante o mal clasificado. Un resumen que parece correcto puede estar sacando de contexto la conclusión del artículo.

  2. El criterio editorial es lo que diferencia el canal. Cualquiera puede montar un bot que publique todo lo que sale en Hacker News. Lo que hace valioso un canal de contenido es que alguien con criterio ha decidido que eso merece la pena.

  3. Me mantiene conectado con el contenido. Si automatizo todo, pierdo contacto con lo que se está publicando en mi sector. La revisión diaria de 10 minutos es también mi sesión de lectura rápida.

Automatizar no es eliminar al humano. Es liberar al humano de las tareas mecánicas para que se concentre en lo que aporta valor: el criterio.


Costes reales del sistema

Para que no quede todo en abstracto:

ConceptoCoste mensual aprox.
VPS (n8n + servicios)~10€
API LLM (resúmenes, clasificación)~15-25€
API de X (tier básico)0€ (free tier)
Telegram Bot API0€
PostgreSQL (self-hosted en VPS)0€ (incluido en VPS)
Total~25-35€/mes

No es gratis, pero es menos de lo que cuesta una suscripción a cualquier herramienta SaaS de gestión de contenido. Y el sistema es mío, lo controlo y lo puedo modificar sin depender de que una empresa cambie sus precios o cierre su API.


Conclusión

Montar este flujo me llevó unas dos semanas de trabajo intermitente. La primera versión era mucho más simple: un script de Python que leía RSS, generaba un resumen y lo mandaba a Telegram. Todo automático, sin revisión. El resultado era mediocre: publicaciones irrelevantes, duplicados y un tono genérico que no representaba mi criterio.

La versión actual es mejor porque acepta que la automatización total no es el objetivo. El objetivo es reducir el trabajo mecánico al mínimo y dejar que la decisión editorial siga siendo humana.

Si estás pensando en montar algo similar, mi consejo es que empieces por el paso más simple (RSS → Telegram, sin IA, sin filtrado complejo) y vayas añadiendo capas según necesites. La complejidad de este sistema no vino de un diseño inicial brillante, sino de ir resolviendo problemas reales una semana tras otra.

OshyTech

Ingeniería backend y de datos orientada a sistemas escalables, automatización e IA.

Navegación

Copyright 2026 OshyTech. Todos los derechos reservados