Hola a todos ![]()
No soy programador profesional, pero soy autodidacta y algo me defiendo. Llevo un negocio y me pasaba lo de siempre: cada mes, un montón de facturas en PDF y yo copiando a mano proveedor, fecha, concepto e importe a una hoja. Aburrido y con errores.
Me puse con n8n para automatizarlo, pero tenía dos condiciones que me importaban mucho:
- Que las facturas no salieran de mi ordenador. Son datos de proveedores y de mi negocio, no quería subirlos a ninguna API en la nube.
- Que no me costara una suscripción mensual por cada llamada a un modelo.
Así que lo monté con Ollama corriendo en local (qwen2.5-coder:7b) en vez de una IA de pago. Y funciona en un portátil normalito: un Ryzen 7 con 16 GB de RAM, sin gráfica profesional, ~12 segundos por factura y 0 € de coste por proceso.
El flujo es simple, 7 nodos:
- Un disparador (en la demo, manual; en real yo uso un Gmail Trigger con la etiqueta “Facturas”).
- Un nodo Code con 3 facturas de ejemplo embebidas, para que el flujo se pueda probar sin montar nada.
- Un HTTP Request a Ollama pidiéndole el JSON con proveedor, fecha, concepto, total.
- Un nodo que clasifica.
- Un resumen.
- Aviso por Telegram al móvil.
- Un hueco reservado para volcar a Google Sheets.
Lo que más me costó, y lo que más me ha servido: al principio la IA se inventaba datos cuando la factura estaba borrosa o escaneada regular. Un desastre para contabilidad. La solución fue pedirle en el prompt que devolviera también un campo confianza de 0 a 1, y montar una regla sencilla:
si
confianza ≥ 0.8y hay proveedor y total → OK si no → REVISAR
Con eso, las facturas dudosas no se cuelan: se marcan en “amarillo” y las miro yo 10 segundos. Prefiero eso a un error de 300 € pasando en silencio. El prompt lleva temperature: 0 y format: json para que no divague.
Os dejo el workflow completo aquí abajo (en el desplegable), por si a alguien le sirve o quiere destrozarlo y mejorarlo. Es de copiar y pegar directo en n8n (Ctrl+V en el lienzo) — trae las 3 facturas de ejemplo dentro, así lo pruebas sin montar nada.
Un par de cosas que aprendí montándolo, por si os ahorran el rato:
- Si n8n corre en Docker, el nodo de Ollama tiene que apuntar a
http://host.docker.internal:11434, no alocalhost— ahí perdí un buen rato con el “connection refused”. - La primera factura tarda bastante más que el resto (en mi portátil, cerca de un minuto) porque Ollama carga el modelo en memoria la primera vez. A partir de la segunda ya va a ~8-10 segundos. Si vas a procesar un lote, la primera es el peaje y luego vuela.
- Si todo te sale “REVISAR”, el umbral de confianza está demasiado alto; bájalo a 0,6 y prueba.
- Si el PDF es un escaneo sin capa de texto, la IA recibe vacío: hay que meterle un OCR antes.
Dos preguntas para quien haya hecho algo parecido, que ando dándole vueltas:
- ¿Cómo gestionáis vosotros el caso “REVISAR”? ¿lo mandáis a una segunda pasada con otro modelo, o directamente a revisión humana como yo?
- Para producción, ¿tiráis de Gmail Trigger como yo o preferís una carpeta vigilada / webhook?
Cualquier crítica al montaje es bienvenida, que soy más de barra de bar que de código y seguro que se puede afinar ![]()
[details=“Workflow completo - copialo y pegalo directo en n8n (Ctrl+V en el lienzo)”]
{
"id": "FacturasAuto0001",
"name": "facturas-auto (v3: IA local + Telegram)",
"nodes": [
{
"name": "Inicio manual (piloto)",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
0,
0
],
"parameters": {}
},
{
"name": "Facturas de prueba",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
200,
0
],
"parameters": {
"jsCode": "// PILOTO: 3 facturas embebidas. PRODUCCION: sustituir este nodo por Gmail Trigger (etiqueta Facturas).\nconst facturas = [\n 'ACME SUMINISTROS S.L.\\nProveedor: Acme Suministros SL\\nFecha: 02/07/2026\\nConcepto: Material de oficina - pedido 4471\\nTotal: 148,50',\n 'HOSTING CLARO SA DE CV\\nProveedor: Hosting Claro\\nFecha: 05/07/2026\\nConcepto: Servidor VPS mensualidad julio\\nTotal: 29,00',\n '[escaneo de baja calidad]\\nProv__dor: (ilegible)\\nFecha: 06/07/2026\\nConc.: reparacion equipo\\nImporte total 3?0,00'\n];\nreturn facturas.map(t => ({ json: { texto: t } }));"
}
},
{
"name": "Extraer con IA local (Ollama)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
400,
0
],
"parameters": {
"method": "POST",
"url": "http://host.docker.internal:11434/api/generate",
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ model: 'qwen2.5-coder:7b', stream: false, format: 'json', options: { temperature: 0 }, prompt: 'Extrae de esta factura los campos en JSON exacto: proveedor (string), fecha (string), concepto (string), total (string, solo el numero), confianza (0 a 1 segun lo legible que sea). Si un campo es ilegible usa \\\"\\\" y baja la confianza.\\nFACTURA:\\n' + $json.texto }) }}",
"options": {
"timeout": 300000
}
}
},
{
"name": "Clasificar",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
600,
0
],
"parameters": {
"jsCode": "return $input.all().map(item => {\n let d = {};\n try { d = JSON.parse(item.json.response); } catch (e) { d = { confianza: 0 }; }\n const estado = (d.confianza ?? 0) >= 0.8 && d.proveedor && d.total ? 'OK' : 'REVISAR';\n return { json: { proveedor: d.proveedor || 'DESCONOCIDO', fecha: d.fecha || '', concepto: d.concepto || '', total: d.total || '', confianza: d.confianza ?? 0, estado } };\n});"
}
},
{
"name": "Resumen",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
800,
0
],
"parameters": {
"jsCode": "const items = $input.all();\nconst ok = items.filter(i => i.json.estado === 'OK').length;\nconst lineas = items.map(i => `- ${i.json.proveedor}: ${i.json.total || '?'} EUR [${i.json.estado}]`).join('\\n');\nreturn [{ json: { text: `\\u{1F4C4} Facturas procesadas con IA local: ${items.length} (${ok} OK, ${items.length - ok} a revisar)\\n${lineas}` } }];"
}
},
{
"name": "Aviso Telegram",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1000,
0
],
"parameters": {
"chatId": "PON_AQUI_TU_CHAT_ID",
"text": "={{ $json.text }}",
"additionalFields": {}
},
"credentials": {
"telegramApi": {
"id": "telegramBotAvisos",
"name": "Telegram (bot avisos)"
}
}
},
{
"name": "Registro (Sheets: pendiente credencial)",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
1000,
160
],
"parameters": {}
}
],
"connections": {
"Inicio manual (piloto)": {
"main": [
[
{
"node": "Facturas de prueba",
"type": "main",
"index": 0
}
]
]
},
"Facturas de prueba": {
"main": [
[
{
"node": "Extraer con IA local (Ollama)",
"type": "main",
"index": 0
}
]
]
},
"Extraer con IA local (Ollama)": {
"main": [
[
{
"node": "Clasificar",
"type": "main",
"index": 0
}
]
]
},
"Clasificar": {
"main": [
[
{
"node": "Resumen",
"type": "main",
"index": 0
},
{
"node": "Registro (Sheets: pendiente credencial)",
"type": "main",
"index": 0
}
]
]
},
"Resumen": {
"main": [
[
{
"node": "Aviso Telegram",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
},
"active": false
}
[/details]