Un workflow de n8n que scrapea LinkedIn todos los días, scorea cada oferta contra mi CV usando un LLM, genera un CV adaptado por oferta y me avisa por Telegram cuando encuentra un match fuerte — guardando todo organizado en Notion.
En febrero de 2026 me quedé sin trabajo. En vez de pasar el día entero scrolleando LinkedIn a mano, decidí aprovechar el tiempo para meterme de lleno en algo que venía posponiendo: construir automatizaciones reales con n8n, no solo tutoriales.
Este repo es el resultado — con toda la complejidad real que tuvo, no la versión idealizada. Tuve que resolver rate limiting de LinkedIn, sincronización de loops que perdían datos entre pasos, respuestas 503 de la API de Gemini a mitad de una corrida, y varios bugs sutiles de cómo n8n maneja índices dentro de loops. Todo eso está reflejado en el diseño final.
- Busca ofertas en LinkedIn todos los días a las 8am, con varios keywords en paralelo (con rate limiting para no gatillar el bloqueo anti-scraping de LinkedIn)
- Deduplica ofertas que aparecen en más de una búsqueda, y contra las que ya existen en la base de Notion
- Trae el detalle completo de cada oferta nueva (la descripción completa del puesto)
- Scorea cada oferta contra mi perfil usando un LLM (Gemini), que devuelve un score 0-100, una justificación y un CV adaptado a esa oferta específica
- Guarda todo en una base de Notion (título, empresa, score, justificación, CV adaptado, URL)
- Avisa por Telegram cuando el score supera un umbral configurable
Schedule Trigger (8am)
│
├──→ job titles (lista de keywords)
│ │
│ ▼
│ Loop Over Items ──┐
│ │ │
│ ▼ │
│ HTTP Request │ (con Wait entre cada
│ (LinkedIn guest API)│ búsqueda, rate limiting)
│ │ │
│ ▼ │
│ HTML (extraer │
│ listado) │
│ │ │
│ └──────────────┘
│ ▼
│ Code (aplanar resultados)
│ │
│ ▼
│ Code1 (dedup dentro del batch)
│ │
│ ├──→ Get many database pages (Notion) → ExistingIds
│ │
│ ▼
│ Merge (sincroniza ambas ramas)
│ │
│ ▼
│ Code2 (marca existe: true/false)
│ │
│ ▼
│ If (existe == false)
│ │
│ ▼
│ Loop Detalle Ofertas ──┐
│ │ │
│ ▼ │
│ Guardar original │
│ │ │
│ ▼ │
│ HTTP Request (detalle) │ (con Wait, rate limiting)
│ │ │
│ ▼ │
│ HTML (extraer descripción)│
│ │ │
│ ▼ │
│ Code (combinar con original)
│ │ │
│ └───────────────────┘
│ ▼
├──→ Merge1 (junta con "Mi Perfil")
│ │
│ ▼
│ Loop Gemini ──┐
│ │ │
│ ▼ │
│ Guardar antes │
│ de Gemini │
│ │ │
│ ▼ │
│ GEMINI (scoring) │ (con retry en caso de error 503)
│ │ │
│ ▼ │
│ parser │
│ │ │
│ └──────────┘
│ ▼
│ If1 (score >= umbral)
│ │
│ ▼
│ Create a database page (Notion)
│ │
│ ▼
│ Send a text message (Telegram)
│
└──→ Mi Perfil (CV en texto, para el prompt de Gemini)
-
Rate limiting real: el endpoint no oficial de búsqueda de LinkedIn banea la IP si le pegás muchas veces seguidas. Solución: un
Loop Over Itemscon batch size 1 + un nodoWaitentre cada iteración, tanto para las búsquedas como para el detalle de cada oferta y las llamadas a Gemini. -
$itemIndexno significa lo que uno espera dentro de un loop de a 1 item: cuando unSplit In Batchestiene batch size 1,$itemIndexdentro de esa iteración siempre vale 0 — no representa la posición global dentro del batch total. Usar.all()[$itemIndex]para recuperar datos de otro nodo casi siempre trae el ítem equivocado. La solución que terminé usando: un nodo "Guardar original" justo antes de cualquier paso que sobrescriba eljson(como un HTTP Request), y después recuperarlo con.first()en vez de por índice — porque con batch size 1, solo hay un ítem viajando por esa rama en cada vuelta. -
Los nodos HTTP Request reemplazan todo el
jsonde entrada con la respuesta: si necesitás conservar datos que traías antes de la llamada, hay que guardarlos explícitamente en un campo separado (_original) antes de que el HTTP Request los pise. -
Merge en modo "Choose Branch" como semáforo: cuando dos ramas necesitan terminar antes de que el flujo continúe (por ejemplo, traer los IDs existentes de Notion antes de comparar), un nodo
Mergeen modochooseBranchobliga a n8n a esperar ambas ramas, aunque solo te quedes con los datos de una. -
Reintentos ante errores transitorios de LLM APIs: la API de Gemini devuelve ocasionalmente
503 - model overloaded. En vez de perder esa oferta silenciosamente, la rama de error del nodo vuelve a meter el ítem en el loop (con un contador de intentos para no reintentar infinito).
- Una instancia de n8n (self-hosted o cloud)
- Una cuenta de Notion con una integración creada y una database compartida con ella
- Una API key de Google AI Studio (tier gratuito de Gemini)
- Un bot de Telegram (vía @BotFather) y tu Chat ID
- Importá
linkedin-job-alerts.jsonen tu instancia de n8n - Creá una database en Notion con estas columnas:
Titulo(Title),Empresa(Text),URL(URL),id_externo(Text),Score(Number),Justificación(Text),CV Adaptado(Text),Fecha detección(Date) - Compartí la database con tu integración de Notion (
•••→ Connections) - En el nodo
Get many database pagesyCreate a database page: seleccioná tu database y tu credential de Notion - En el nodo
GEMINI: reemplazáYOUR_GEMINI_API_KEYen la URL por tu API key real - En el nodo
Mi Perfil: pegá tu propio CV/perfil en texto plano - En el nodo
Send a text message: reemplazáYOUR_TELEGRAM_CHAT_IDy configurá tu credential de Telegram - En el nodo
job titles: ajustá los keywords de búsqueda a tu perfil - En el nodo
HTTP Request(búsqueda de LinkedIn): ajustáYOUR_LOCATIONa tu ubicación - Ajustá el umbral de score en el nodo
If1(por defecto: 85) - Activá el workflow
Si tu instancia de n8n soporta variables de entorno, es más seguro usar {{ $env.GEMINI_API_KEY }} en vez de pegar la key directamente en el nodo.
- n8n — orquestación del flujo
- LinkedIn guest API (no oficial) — scraping de ofertas
- Google Gemini (
gemini-3.5-flash-lite) — scoring y generación de CV adaptado - Notion API — almacenamiento
- Telegram Bot API — alertas
Este flujo usa el endpoint no oficial de búsqueda de LinkedIn (jobs-guest/jobs/api), que no es una API pública soportada. Puede cambiar o dejar de funcionar sin aviso, y un uso agresivo puede resultar en bloqueos temporales de IP. Usalo con criterio y respetando los términos de servicio de LinkedIn.
MIT — usalo, adaptalo, rómpelo y arreglalo como quieras.