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 1) Ingesta (Telegram \u2192 Cola)",
"nodes": [
{
"id": "tg-trg",
"name": "Recibir foto (Telegram)",
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1.2,
"position": [
0,
0
],
"parameters": {
"updates": [
"message"
],
"additionalFields": {
"download": true
}
}
},
{
"id": "code-norm",
"name": "Normalizar evidencia",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
220,
0
],
"parameters": {
"jsCode": "const item = $input.first();\nconst m = item.json.message ?? item.json;\nconst chatId = m.chat?.id;\nconst chatTitle = m.chat?.title || '';\nconst messageId = m.message_id;\nconst fromId = m.from?.id;\nconst cap = m.caption || '';\nconst hasPhoto = Array.isArray(m.photo) && m.photo.length > 0;\nconst capturadoEn = new Date((m.date || 0) * 1000).toISOString();\nconst proceso = 'calidad-lata';\nconst evidenceId = proceso + ':' + chatId + ':' + messageId;\nconst imagenRef = proceso + '/' + chatId + '/' + messageId + '.jpg';\nreturn [{ json: { evidence_id: evidenceId, proceso, linea_fallback: chatTitle || 'desconocida', equipo: '', capturado_en: capturadoEn, imagen_ref: imagenRef, origen: { chat_id: chatId, chat_title: chatTitle, from_id: fromId, message_id: messageId }, caption: cap, has_photo: hasPhoto }, binary: item.binary }];"
}
},
{
"id": "if-foto",
"name": "\u00bfEs foto?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
440,
0
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"combinator": "and",
"conditions": [
{
"id": "c1",
"leftValue": "={{ $json.has_photo }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
]
}
}
},
{
"id": "crypto-hash",
"name": "Hash imagen",
"type": "n8n-nodes-base.crypto",
"typeVersion": 2,
"position": [
660,
-80
],
"parameters": {
"action": "hash",
"type": "SHA256",
"binaryData": true,
"binaryPropertyName": "data",
"dataPropertyName": "imagen_hash",
"encoding": "hex"
}
},
{
"id": "code-reattach-bin",
"name": "Reasignar binario",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
770,
-80
],
"parameters": {
"jsCode": "const item = $input.first();\nreturn [{ json: item.json, binary: $('Normalizar evidencia').first().binary }];"
}
},
{
"id": "s3-up",
"name": "Guardar imagen original",
"type": "n8n-nodes-base.s3",
"typeVersion": 1,
"position": [
880,
-80
],
"retryOnFail": true,
"maxTries": 3,
"parameters": {
"resource": "file",
"operation": "upload",
"bucketName": "calidad-lata",
"fileName": "={{ $('Normalizar evidencia').first().json.imagen_ref }}",
"binaryData": true,
"binaryPropertyName": "data",
"additionalFields": {}
}
},
{
"id": "pg-linea",
"name": "Buscar l\u00ednea",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
1100,
-80
],
"alwaysOutputData": true,
"retryOnFail": true,
"maxTries": 2,
"parameters": {
"resource": "database",
"operation": "executeQuery",
"query": "=SELECT linea, equipo, proceso FROM lineas_grupos WHERE chat_id = '{{ $('Normalizar evidencia').first().json.origen.chat_id }}' LIMIT 1;",
"options": {}
}
},
{
"id": "pg-dup",
"name": "\u00bfDuplicada en otro grupo?",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
1320,
-80
],
"alwaysOutputData": true,
"retryOnFail": true,
"maxTries": 2,
"parameters": {
"resource": "database",
"operation": "executeQuery",
"query": "=SELECT evidence_id AS dup_evidence_id FROM evidencias WHERE imagen_hash = '{{ $('Hash imagen').first().json.imagen_hash }}' AND capturado_en > now() - (interval '1 hour' * COALESCE((SELECT valor::int FROM config WHERE clave='dedup_ventana_horas'), 24)) LIMIT 1;",
"options": {}
}
},
{
"id": "code-prep-reg",
"name": "Preparar registro",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1540,
-80
],
"parameters": {
"jsCode": "const n = $('Normalizar evidencia').first().json;\nconst hash = $('Hash imagen').first().json.imagen_hash || null;\nconst lineaRows = $('Buscar l\u00ednea').all().map(i => i.json).filter(j => j && j.linea);\nconst linea = lineaRows.length ? lineaRows[0].linea : n.linea_fallback;\nconst equipo = lineaRows.length ? (lineaRows[0].equipo || '') : '';\nconst proceso = lineaRows.length ? (lineaRows[0].proceso || n.proceso) : n.proceso;\nconst dupRows = $input.all().map(i => i.json).filter(j => j && j.dup_evidence_id);\nconst estado = dupRows.length ? 'duplicada' : 'recibido';\nconst duplicada_de = dupRows.length ? dupRows[0].dup_evidence_id : null;\nreturn [{ json: { ...n, linea, equipo, proceso, imagen_hash: hash, estado, duplicada_de } }];"
}
},
{
"id": "pg-ins",
"name": "Registrar evidencia",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.6,
"position": [
1760,
-80
],
"onError": "continueErrorOutput",
"parameters": {
"resource": "database",
"operation": "insert",
"schema": {
"__rl": true,
"mode": "list",
"value": "public",
"cachedResultName": "public"
},
"table": {
"__rl": true,
"mode": "list",
"value": "evidencias",
"cachedResultName": "evidencias"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"evidence_id": "={{ $('Preparar registro').first().json.evidence_id }}",
"proceso": "={{ $('Preparar registro').first().json.proceso }}",
"linea": "={{ $('Preparar registro').first().json.linea }}",
"equipo": "={{ $('Preparar registro').first().json.equipo }}",
"capturado_en": "={{ $('Preparar registro').first().json.capturado_en }}",
"imagen_ref": "={{ $('Preparar registro').first().json.imagen_ref }}",
"origen": "={{ JSON.stringify($('Preparar registro').first().json.origen) }}",
"estado": "={{ $('Preparar registro').first().json.estado }}",
"imagen_hash": "={{ $('Preparar registro').first().json.imagen_hash }}"
},
"matchingColumns": [],
"schema": []
},
"options": {}
}
},
{
"id": "if-encolar",
"name": "\u00bfEncolar?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1980,
-80
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 2
},
"combinator": "and",
"conditions": [
{
"id": "q1",
"leftValue": "={{ $('Preparar registro').first().json.estado }}",
"rightValue": "recibido",
"operator": {
"type": "string",
"operation": "equals"
}
}
]
}
}
},
{
"id": "rmq-send",
"name": "Publicar a cola",
"type": "n8n-nodes-base.rabbitmq",
"typeVersion": 1.1,
"position": [
2200,
-160
],
"retryOnFail": true,
"maxTries": 3,
"parameters": {
"operation": "sendMessage",
"mode": "queue",
"queue": "calidad.lata.procesar",
"sendInputData": false,
"message": "={{ JSON.stringify({ evidence_id: $('Preparar registro').first().json.evidence_id, proceso: $('Preparar registro').first().json.proceso, linea: $('Preparar registro').first().json.linea, equipo: $('Preparar registro').first().json.equipo, capturado_en: $('Preparar registro').first().json.capturado_en, imagen_ref: $('Preparar registro').first().json.imagen_ref, origen: $('Preparar registro').first().json.origen }) }}",
"options": {
"durable": true
}
}
},
{
"id": "redis-metric",
"name": "Contador recibidas (m\u00e9trica)",
"type": "n8n-nodes-base.redis",
"typeVersion": 1,
"position": [
2420,
-160
],
"onError": "continueRegularOutput",
"parameters": {
"operation": "incr",
"key": "={{ 'metrics:recibidas:' + $('Preparar registro').first().json.linea }}",
"expire": false
}
},
{
"id": "noop-dup-grupos",
"name": "Duplicada entre grupos",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
2200,
60
],
"parameters": {}
},
{
"id": "noop-nofoto",
"name": "Descartar (sin foto)",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
660,
240
],
"parameters": {}
},
{
"id": "noop-dup",
"name": "Duplicado (ignorar)",
"type": "n8n-nodes-base.noOp",
"typeVersion": 1,
"position": [
1980,
120
],
"parameters": {}
},
{
"id": "sticky-1",
"name": "Nota-ingesta",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-40,
-260
],
"parameters": {
"content": "### 1) Ingesta v3\nTelegram \u2192 normaliza (ids por chat_id) \u2192 hash SHA-256 de la imagen \u2192 guarda original en MinIO \u2192 busca la L\u00cdNEA en la tabla lineas_grupos (config-driven; fallback = t\u00edtulo del grupo) \u2192 detecta si la MISMA foto ya lleg\u00f3 por otro grupo (ventana config dedup_ventana_horas) \u2192 registra evidencia (con hash y estado) \u2192 si estado='recibido' publica a RabbitMQ + m\u00e9trica Redis; si 'duplicada' NO se encola (minuta: eliminar informaci\u00f3n duplicada).\n\nDedup por mensaje: PK evidence_id (salida de error del INSERT \u2192 ignorar).\n\n\u26a0\ufe0f 13/07: el nodo Crypto ('Hash imagen') NO propaga el binario en su salida (solo json) \u2014 se agreg\u00f3 'Reasignar binario' (Code) para reinyectar la foto desde 'Normalizar evidencia' antes de subir a S3. Sin esto, TODA foto fallaba en 'Guardar imagen original' (outage 11/07 22:11 \u2192 13/07 14:32).",
"color": 4,
"height": 300,
"width": 640
}
}
],
"connections": {
"Recibir foto (Telegram)": {
"main": [
[
{
"node": "Normalizar evidencia",
"type": "main",
"index": 0
}
]
]
},
"Normalizar evidencia": {
"main": [
[
{
"node": "\u00bfEs foto?",
"type": "main",
"index": 0
}
]
]
},
"\u00bfEs foto?": {
"main": [
[
{
"node": "Hash imagen",
"type": "main",
"index": 0
}
],
[
{
"node": "Descartar (sin foto)",
"type": "main",
"index": 0
}
]
]
},
"Hash imagen": {
"main": [
[
{
"node": "Reasignar binario",
"type": "main",
"index": 0
}
]
]
},
"Reasignar binario": {
"main": [
[
{
"node": "Guardar imagen original",
"type": "main",
"index": 0
}
]
]
},
"Guardar imagen original": {
"main": [
[
{
"node": "Buscar l\u00ednea",
"type": "main",
"index": 0
}
]
]
},
"Buscar l\u00ednea": {
"main": [
[
{
"node": "\u00bfDuplicada en otro grupo?",
"type": "main",
"index": 0
}
]
]
},
"\u00bfDuplicada en otro grupo?": {
"main": [
[
{
"node": "Preparar registro",
"type": "main",
"index": 0
}
]
]
},
"Preparar registro": {
"main": [
[
{
"node": "Registrar evidencia",
"type": "main",
"index": 0
}
]
]
},
"Registrar evidencia": {
"main": [
[
{
"node": "\u00bfEncolar?",
"type": "main",
"index": 0
}
],
[
{
"node": "Duplicado (ignorar)",
"type": "main",
"index": 0
}
]
]
},
"\u00bfEncolar?": {
"main": [
[
{
"node": "Publicar a cola",
"type": "main",
"index": 0
}
],
[
{
"node": "Duplicada entre grupos",
"type": "main",
"index": 0
}
]
]
},
"Publicar a cola": {
"main": [
[
{
"node": "Contador recibidas (m\u00e9trica)",
"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 — 1) Ingesta (Telegram → Cola). Uses telegramTrigger, crypto, s3, postgres. Event-driven trigger; 17 nodes.
Source: https://github.com/aiporvos/sudamericanabebidas/blob/main/workflows/calidad-lata-1-ingesta.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.
News Digest Bot - Multi-User (Postgres). Uses telegramTrigger, postgres, telegram, rssFeedRead. Event-driven trigger; 45 nodes.
Telegram Wait. Uses stickyNote, httpRequest, redis, noOp. Event-driven trigger; 36 nodes.
telegram. Uses telegram, telegramTrigger, readWriteFile, httpRequest. Event-driven trigger; 29 nodes.