This workflow follows the HTTP Request → Postgres recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Calidad de Lata \u2014 2) Procesamiento IA (Cola \u2192 Resultado)",
"nodes": [
{
"id": "rmq-trg",
"name": "Tomar tarea (RabbitMQ)",
"type": "n8n-nodes-base.rabbitmqTrigger",
"typeVersion": 1,
"position": [
0,
0
],
"parameters": {
"queue": "calidad.lata.procesar",
"options": {}
}
},
{
"id": "code-read",
"name": "Leer tarea",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
220,
0
],
"parameters": {
"jsCode": "const j = $input.first().json;\nlet body = j.content !== undefined ? j.content : j;\nif (typeof body === 'string') { try { body = JSON.parse(body); } catch (e) {} }\nreturn [{ json: body }];"
}
},
{
"id": "s3-dl",
"name": "Descargar imagen",
"type": "n8n-nodes-base.s3",
"typeVersion": 1,
"position": [
440,
0
],
"retryOnFail": true,
"maxTries": 3,
"parameters": {
"resource": "file",
"operation": "download",
"bucketName": "calidad-lata",
"fileKey": "={{ $('Leer tarea').item.json.imagen_ref }}"
}
},
{
"id": "code-prep",
"name": "Preparar visi\u00f3n",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
660,
0
],
"parameters": {
"jsCode": "const buf = await this.helpers.getBinaryDataBuffer(0, 'data');\nconst b64 = buf.toString('base64');\nconst ev = $('Leer tarea').item.json;\nconst prompt = 'Sos control de calidad de una embotelladora (latas). Analiza la foto y devolve SOLO un JSON valido con estas claves:\\n- tipo_foto: \"tapa\" | \"fondo_impresion\" | \"pantalla_contador\" | \"frente\" | \"otro\" (tapa de lata, impresion del fondo de la lata, pantalla/tablero de la maquina con contadores, frente de la lata, u otra cosa)\\n- contadores: array de numeros leidos (solo si es pantalla_contador; sino [])\\n- hora_pantalla: hora y/o fecha visible EN la pantalla fotografiada, como string (solo pantalla_contador; sino null)\\n- textos: array de strings con TODO texto impreso legible (ej lote \"L:117 13:55\", vencimiento \"V:24/10/26\")\\n- etiquetas: array de etiquetas/marcas visibles\\n- calidad_impresion: \"buena\" | \"mala\" | null (solo fondo_impresion: evalua nitidez, legibilidad y completitud de la impresion AUNQUE el dato sea correcto)\\n- defectos: array con cualquiera de: impresion, centrado, inclinacion, deformacion, etiqueta, contraetiqueta\\n- resultado: \"OK\" | \"No OK\"\\n- confianza: numero 0..1\\nFormatos tipicos impresos en el fondo de lata: \\\"L:<lote> <hh:mm>\\\" con \\\"V:<dd/mm/aa>\\\", o \\\"LOTE:<codigo>\\\" con \\\"VTO <dd/mm/aa>\\\" \u2014 transcribi EXACTAMENTE lo impreso, sin corregirlo ni completarlo. Si la impresion esta tenue, desgastada, incompleta o dudosa de leer, reduci la confianza por debajo de 0.85 y considera calidad_impresion=\\\"mala\\\". Sin texto fuera del JSON.';\nconst body = { model: 'gpt-4o', max_tokens: 1024, response_format: { type: 'json_object' }, messages: [{ role: 'user', content: [{ type: 'text', text: prompt }, { type: 'image_url', image_url: { url: 'data:image/jpeg;base64,' + b64 } }] }] };\nreturn [{ json: { ...ev, visionBody: body } }];"
}
},
{
"id": "http-vision",
"name": "Visi\u00f3n IA (OpenAI)",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
880,
0
],
"retryOnFail": true,
"maxTries": 3,
"alwaysOutputData": true,
"parameters": {
"method": "POST",
"url": "https://api.openai.com/v1/chat/completions",
"authentication": "genericCredentialType",
"genericAuthType": "httpHeaderAuth",
"sendHeaders": false,
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify($json.visionBody) }}",
"options": {}
}
},
{
"id": "code-parse",
"name": "Parsear resultado",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1100,
0
],
"parameters": {
"jsCode": "const resp = $input.first().json;\nconst ev = $('Leer tarea').item.json;\nlet raw = '';\ntry { raw = resp.choices[0].message.content; } catch (e) { raw = ''; }\nlet parsed = {};\ntry { const mt = raw.match(/\\{[\\s\\S]*\\}/); parsed = JSON.parse(mt ? mt[0] : raw); } catch (e) { parsed = { _parse_error: true }; }\nconst tokens = (resp.usage && resp.usage.total_tokens) || 0;\nlet umbral = 0.85;\ntry { const cfg = $('Leer config').all().map(i => i.json); const u = cfg.find(x => x && x.clave === 'umbral_confianza'); if (u && !isNaN(parseFloat(u.valor))) umbral = parseFloat(u.valor); } catch (e) {}\nconst confianza = Number(parsed.confianza ?? 0);\nconst revision_manual = !(confianza >= umbral);\nconst resultado = revision_manual ? null : (parsed.resultado ?? null);\nconst calidad_impresion = parsed.calidad_impresion ?? null;\nconst requiere_atencion = revision_manual || resultado === 'No OK' || calidad_impresion === 'mala';\nconst motivo = calidad_impresion === 'mala' ? 'calidad de impresion mala' : null;\nconst estado = revision_manual ? 'revision_manual' : 'procesado';\nreturn [{ json: { evidence_id: ev.evidence_id, proceso: ev.proceso, linea: ev.linea, equipo: ev.equipo, capturado_en: ev.capturado_en, imagen_ref: ev.imagen_ref, tipo_foto: parsed.tipo_foto ?? 'otro', contadores: parsed.contadores ?? [], textos: parsed.textos ?? [], etiquetas: parsed.etiquetas ?? [], hora_pantalla: parsed.hora_pantalla ?? null, calidad_impresion, defectos: parsed.defectos ?? [], resultado, confianza, revision_manual, requiere_atencion, motivo, estado, modelo: (resp.model || 'gpt-4o'), tokens, evaluado_en: new Date().toISOString() } }];"
}
},
{
"id": "pg-lookup",
"name": "Buscar contador reciente",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
1100,
192
],
"alwaysOutputData": true,
"retryOnFail": true,
"maxTries": 2,
"parameters": {
"resource": "database",
"operation": "executeQuery",
"query": "=SELECT r.evidence_id AS cont_evidence_id, r.hora_pantalla AS cont_hora_pantalla, r.textos AS cont_textos, r.evaluado_en AS cont_evaluado_en FROM resultados r JOIN evidencias e ON e.evidence_id = r.evidence_id WHERE r.tipo_foto = 'pantalla_contador' AND e.linea = '{{ $('Parsear resultado').first().json.linea }}' AND e.capturado_en >= now() - (interval '1 minute' * COALESCE((SELECT valor::int FROM config WHERE clave='ventana_comparacion_min'), 90)) ORDER BY e.capturado_en DESC LIMIT 1;",
"options": {}
}
},
{
"id": "code-coherencia",
"name": "Evaluar coherencia",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1320,
192
],
"parameters": {
"jsCode": "const r = $('Parsear resultado').first().json;\nlet coherencia = null;\nlet motivo = r.motivo || null;\nif (r.tipo_foto === 'fondo_impresion') {\n const rows = $input.all().map(i => i.json).filter(j => j && j.cont_evidence_id);\n if (rows.length) {\n const reHora = /([01]?\\d|2[0-3]):[0-5]\\d/g;\n const lataAll = (r.textos || []).join(' ').toUpperCase();\n let contTextos = rows[0].cont_textos || [];\n if (typeof contTextos === 'string') { try { contTextos = JSON.parse(contTextos); } catch (e) { contTextos = [contTextos]; } }\n const contAll = (Array.isArray(contTextos) ? contTextos : [String(contTextos)]).join(' ').toUpperCase();\n const fallas = [];\n let checks = 0;\n // 1) LOTE: formato corto \"L:117\" o largo \"LOTE:10010-5A\" vs pantalla \"LOTE...: xxx\"\n const mLoteLargo = lataAll.match(/LOTE\\s*[:\\.]?\\s*([0-9A-Z][0-9A-Z-]{2,})/);\n const mLoteCorto = lataAll.match(/L\\s*[:\\.]\\s*([0-9]{2,6})/);\n const lataLote = (mLoteLargo || mLoteCorto || [])[1] || null;\n const contLote = (contAll.match(/LOTE[^:]{0,25}:\\s*([0-9A-Z][0-9A-Z-]*)/) || [])[1] || null;\n if (lataLote && contLote) {\n checks++;\n const a = lataLote.replace(/^0+/, '');\n const b = contLote.replace(/^0+/, '');\n if (a !== b && !b.split('-').includes(a) && !a.split('-').includes(b)) fallas.push('lote impreso ' + lataLote + ' distinto del lote en pantalla ' + contLote);\n }\n // 2) VTO: \"V:24/10/26\" o \"VTO 29/03/27\" vs pantalla \"VTO/VENC...: dd/mm/aa\"\n const norm = s => s.replace(/(\\d{1,2})\\/(\\d{1,2})\\/(\\d{2,4})/, (m, d, mo, y) => ('' + d).padStart(2, '0') + '/' + ('' + mo).padStart(2, '0') + '/' + ('' + y).slice(-2));\n const lataVto = (lataAll.match(/V(?:TO)?\\.?\\s*[:\\.]?\\s*(\\d{1,2}\\/\\d{1,2}\\/\\d{2,4})/) || [])[1] || null;\n const contVto = (contAll.match(/(?:VTO|VENC)[^:]{0,25}:\\s*(\\d{1,2}\\/\\d{1,2}\\/\\d{2,4})/) || [])[1] || null;\n if (lataVto && contVto) {\n checks++;\n if (norm(lataVto) !== norm(contVto)) fallas.push('vencimiento impreso ' + lataVto + ' distinto del de pantalla ' + contVto);\n }\n // 3) HORA impresa vs hora en pantalla (tolerancia 90 min)\n const horasLata = lataAll.match(reHora) || [];\n const horasPant = ((rows[0].cont_hora_pantalla || '').toString().match(reHora)) || [];\n if (horasLata.length && horasPant.length) {\n checks++;\n const toMin = s => { const p = s.split(':').map(Number); return p[0] * 60 + p[1]; };\n let diff = Infinity;\n for (const hl of horasLata) { for (const hp of horasPant) { let d = Math.abs(toMin(hl) - toMin(hp)); d = Math.min(d, 1440 - d); if (d < diff) diff = d; } }\n if (diff > 90) fallas.push('hora impresa (' + horasLata.join(',') + ') lejos de la pantalla (' + horasPant.join(',') + '), dif ' + diff + ' min');\n }\n if (checks > 0) {\n coherencia = fallas.length === 0;\n if (fallas.length) motivo = fallas.join(' | ');\n }\n }\n}\nconst requiere_atencion = r.requiere_atencion || coherencia === false;\nconst resultado = coherencia === false ? 'No OK' : r.resultado;\nreturn [{ json: { ...r, coherencia, motivo, resultado, requiere_atencion } }];"
}
},
{
"id": "pg-up",
"name": "Guardar resultado",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
1320,
0
],
"retryOnFail": true,
"maxTries": 3,
"parameters": {
"resource": "database",
"operation": "executeQuery",
"query": "=INSERT INTO resultados (evidence_id, estado, resultado, confianza, revision_manual, contadores, defectos, textos, etiquetas, tipo_foto, hora_pantalla, calidad_impresion, coherencia, motivo, tokens, evaluado_en) VALUES ('{{ $json.evidence_id }}', '{{ $json.estado }}', NULLIF('{{ $json.resultado || '' }}', ''), {{ $json.confianza }}, {{ $json.revision_manual }}, '{{ JSON.stringify($json.contadores).replace(/'/g, \"''\") }}'::jsonb, '{{ JSON.stringify($json.defectos).replace(/'/g, \"''\") }}'::jsonb, '{{ JSON.stringify($json.textos).replace(/'/g, \"''\") }}'::jsonb, '{{ JSON.stringify($json.etiquetas).replace(/'/g, \"''\") }}'::jsonb, '{{ $json.tipo_foto }}', NULLIF('{{ ($json.hora_pantalla || '').replace(/'/g, \"''\") }}', ''), NULLIF('{{ $json.calidad_impresion || '' }}', ''), {{ $json.coherencia === null || $json.coherencia === undefined ? 'NULL' : $json.coherencia }}, NULLIF('{{ ($json.motivo || '').replace(/'/g, \"''\") }}', ''), {{ $json.tokens || 0 }}, '{{ $json.evaluado_en }}'::timestamptz) ON CONFLICT (evidence_id) DO UPDATE SET estado = EXCLUDED.estado, resultado = EXCLUDED.resultado, confianza = EXCLUDED.confianza, revision_manual = EXCLUDED.revision_manual, contadores = EXCLUDED.contadores, defectos = EXCLUDED.defectos, textos = EXCLUDED.textos, etiquetas = EXCLUDED.etiquetas, tipo_foto = EXCLUDED.tipo_foto, hora_pantalla = EXCLUDED.hora_pantalla, calidad_impresion = EXCLUDED.calidad_impresion, coherencia = EXCLUDED.coherencia, motivo = EXCLUDED.motivo, tokens = EXCLUDED.tokens, evaluado_en = EXCLUDED.evaluado_en;",
"options": {}
}
},
{
"id": "if-atn",
"name": "\u00bfRequiere atenci\u00f3n?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1540,
0
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"combinator": "and",
"conditions": [
{
"id": "a1",
"leftValue": "={{ $('Evaluar coherencia').first().json.requiere_atencion }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
]
}
}
},
{
"id": "tg-alert",
"name": "Alertar (desv\u00edo/revisi\u00f3n)",
"type": "n8n-nodes-base.telegram",
"typeVersion": 1.2,
"position": [
1760,
-80
],
"retryOnFail": true,
"maxTries": 3,
"parameters": {
"resource": "message",
"operation": "sendMessage",
"chatId": "-1003908341093",
"text": "={{ $('Evaluar coherencia').first().json.estado === 'revision_manual' ? '\ud83d\udd0d' : '\ud83d\udea8' }} <b>CALIDAD DE LATA \u2014 {{ $('Evaluar coherencia').first().json.estado === 'revision_manual' ? 'Revisi\u00f3n manual (baja confianza)' : 'Desv\u00edo No OK' }}</b>\n\n\ud83d\udcf7 <b>Tipo de foto:</b> {{ $('Evaluar coherencia').first().json.tipo_foto }}\n\ud83c\udfed <b>L\u00ednea:</b> {{ $('Evaluar coherencia').first().json.linea }}\n\u2699\ufe0f <b>Equipo:</b> {{ $('Evaluar coherencia').first().json.equipo || '-' }}\n\n\ud83d\udcdd <b>Textos le\u00eddos:</b> <code>{{ ($('Evaluar coherencia').first().json.textos || []).join(' | ') || '-' }}</code>\n\ud83d\udd50 <b>Hora en pantalla:</b> {{ $('Evaluar coherencia').first().json.hora_pantalla || '-' }}\n\ud83d\udda8 <b>Calidad de impresi\u00f3n:</b> {{ $('Evaluar coherencia').first().json.calidad_impresion || '-' }}\n\u2757 <b>Defectos:</b> {{ ($('Evaluar coherencia').first().json.defectos || []).join(', ') || '-' }}\n\ud83d\udccc <b>Motivo:</b> {{ $('Evaluar coherencia').first().json.motivo || '-' }}\n\ud83c\udfaf <b>Confianza:</b> {{ Math.round(($('Evaluar coherencia').first().json.confianza || 0) * 100) }}%\n\n\ud83d\uddc2 <b>Evidencia:</b> <code>{{ $('Evaluar coherencia').first().json.evidence_id }}</code>\n\ud83d\uddbc <b>Imagen:</b> <code>{{ $('Evaluar coherencia').first().json.imagen_ref }}</code>\n\ud83d\udd52 {{ $now.format('dd/MM HH:mm') }}",
"additionalFields": {
"parse_mode": "HTML"
}
}
},
{
"id": "noop-ok",
"name": "Fin OK",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
1760,
120
],
"parameters": {}
},
{
"id": "sticky-2",
"name": "Nota-proc",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-40,
-220
],
"parameters": {
"content": "### 2) Procesamiento IA (v2 \u2014 minuta 25/06)\nConsume la cola \u2192 descarga de S3 \u2192 visi\u00f3n IA (OpenAI gpt-4o-mini, JSON estricto): clasifica el TIPO de foto (tapa / fondo_impresion / pantalla_contador / frente), extrae contadores + hora de pantalla, textos impresos y calidad de impresi\u00f3n \u2192 compara fondo_impresion contra la \u00faltima pantalla_contador de la misma l\u00ednea (\u00b190 min) \u2192 guarda todo (incl. tokens) \u2192 alerta si es No OK, baja confianza, mala impresi\u00f3n o incoherencia.\n\n**Umbral (0.85), modelo y ventana (\u00b190 min)** se ajustan en 'Parsear resultado' / 'Preparar visi\u00f3n' / 'Buscar contador reciente'.",
"color": 5,
"height": 240,
"width": 600
}
},
{
"id": "pg-config",
"name": "Leer config",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
330,
0
],
"alwaysOutputData": true,
"retryOnFail": true,
"maxTries": 2,
"parameters": {
"resource": "database",
"operation": "executeQuery",
"query": "SELECT clave, valor FROM config;",
"options": {}
}
}
],
"connections": {
"Tomar tarea (RabbitMQ)": {
"main": [
[
{
"node": "Leer tarea",
"type": "main",
"index": 0
}
]
]
},
"Leer tarea": {
"main": [
[
{
"node": "Leer config",
"type": "main",
"index": 0
}
]
]
},
"Descargar imagen": {
"main": [
[
{
"node": "Preparar visi\u00f3n",
"type": "main",
"index": 0
}
]
]
},
"Preparar visi\u00f3n": {
"main": [
[
{
"node": "Visi\u00f3n IA (OpenAI)",
"type": "main",
"index": 0
}
]
]
},
"Visi\u00f3n IA (OpenAI)": {
"main": [
[
{
"node": "Parsear resultado",
"type": "main",
"index": 0
}
]
]
},
"Parsear resultado": {
"main": [
[
{
"node": "Buscar contador reciente",
"type": "main",
"index": 0
}
]
]
},
"Buscar contador reciente": {
"main": [
[
{
"node": "Evaluar coherencia",
"type": "main",
"index": 0
}
]
]
},
"Evaluar coherencia": {
"main": [
[
{
"node": "Guardar resultado",
"type": "main",
"index": 0
}
]
]
},
"Guardar resultado": {
"main": [
[
{
"node": "\u00bfRequiere atenci\u00f3n?",
"type": "main",
"index": 0
}
]
]
},
"\u00bfRequiere atenci\u00f3n?": {
"main": [
[
{
"node": "Alertar (desv\u00edo/revisi\u00f3n)",
"type": "main",
"index": 0
}
],
[
{
"node": "Fin OK",
"type": "main",
"index": 0
}
]
]
},
"Leer config": {
"main": [
[
{
"node": "Descargar imagen",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"errorWorkflow": "I59vNrbU4KKGkHXF"
}
}
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Calidad de Lata — 2) Procesamiento IA (Cola → Resultado). Uses rabbitmqTrigger, s3, httpRequest, postgres. Event-driven trigger; 14 nodes.
Source: https://github.com/aiporvos/sudamericanabebidas/blob/main/workflows/calidad-lata-2-procesamiento.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Pede Ai. Uses httpRequest, telegram, postgres, telegramTrigger. Event-driven trigger; 57 nodes.
Pede Ai. Uses httpRequest, telegram, postgres, telegramTrigger. Event-driven trigger; 53 nodes.
03 - Command Handler. Uses executeWorkflowTrigger, telegram, executeCommand, postgres. Event-driven trigger; 53 nodes.
telegram. Uses telegram, telegramTrigger, readWriteFile, httpRequest. Event-driven trigger; 29 nodes.
Telegram_n8n. Uses telegramTrigger, postgres, telegram, httpRequest. Event-driven trigger; 24 nodes.