This workflow follows the Gmail → Gmail Trigger 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 →
{
"nodes": [
{
"parameters": {
"pollTimes": {
"item": [
{
"mode": "everyMinute"
}
]
},
"simple": false,
"filters": {
"q": "subject:\"cdp-enviar-skus\""
},
"options": {}
},
"type": "n8n-nodes-base.gmailTrigger",
"typeVersion": 1.3,
"position": [
-16,
2624
],
"id": "1141e3d7-acbb-44a3-b82e-e953ac55cf8c",
"name": "Gmail Trigger",
"credentials": {
"gmailOAuth2": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"updates": [
"message"
],
"additionalFields": {}
},
"id": "f30ebe85-41c1-4c89-95f6-be31338e7ce7",
"name": "\ud83d\udcf1 Trigger Telegram",
"type": "n8n-nodes-base.telegramTrigger",
"typeVersion": 1.1,
"position": [
-240,
1920
],
"credentials": {
"telegramApi": {
"name": "<your credential>"
}
},
"notes": "v0.6.0: Recebe SKUs via Telegram (texto ou arquivo .xlsx/.csv). Necessita configurar credenciais do bot (@BotFather)."
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 1
},
"conditions": [
{
"id": "robust-check",
"leftValue": "={{ !$json.error && $json.valid_skus > 0 }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "22d612e4-b501-45b6-9ea0-67acd12c2cd4",
"name": "\ud83d\udce7 Email entrada OK?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1328,
3200
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "loose",
"version": 1
},
"conditions": [
{
"id": "robust-check",
"leftValue": "={{ !$json.error && $json.valid_skus > 0 }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "b56b187b-aedb-4316-ac84-90bce238562f",
"name": "\ud83d\udcf1 Telegram entrada OK?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
1328,
2768
]
},
{
"parameters": {
"operation": "update",
"documentId": {
"__rl": true,
"value": "1IGhsIhrwlnMaCduR-W-eIi9O4mMO2pPYjE-tefgIPII",
"mode": "list",
"cachedResultName": "cdp_skus",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IGhsIhrwlnMaCduR-W-eIi9O4mMO2pPYjE-tefgIPII/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": "SKUs",
"mode": "name",
"cachedResultName": "SKUs",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IGhsIhrwlnMaCduR-W-eIi9O4mMO2pPYjE-tefgIPII/edit#gid=0"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"row_number": "={{ $json.row_number }}",
"PROCESSADO": "={{ $json.PROCESSADO }}",
"ENCONTRADO": "={{ $json.ENCONTRADO }}",
"NOTIFICADO": "={{ $json.NOTIFICADO }}"
},
"matchingColumns": [
"row_number"
],
"schema": [
{
"id": "row_number",
"displayName": "row_number",
"required": false,
"defaultMatch": false,
"display": true,
"type": "number",
"canBeUsedToMatch": true,
"readOnly": true,
"removed": false
},
{
"id": "PROCESSADO",
"displayName": "PROCESSADO",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "ENCONTRADO",
"displayName": "ENCONTRADO",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
},
{
"id": "NOTIFICADO",
"displayName": "NOTIFICADO",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {}
},
"id": "1fdb8d2b-e29c-4bad-b483-291399bdf1d4",
"name": "\u2705 Marcar PROCESSADO \u2192 CDP_SKUs",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
2448,
2336
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
},
"notes": "Updates PROCESSADO/ENCONTRADO/NOTIFICADO=\u23f3 Processando for each dispatched sheet row, matched by row_number so duplicate CODIGO rows are initialized too."
},
{
"parameters": {
"jsCode": "// cdp_router \u2014 pair scraper HTTP responses to SKUs for sheet PROCESSADO updates.\n\nconst out = [];\nconst PROCESSING = '\u23f3 Processando';\n\nfunction responseAccepted(resp) {\n return Boolean(resp && (resp.accepted || resp.job_id || resp.body?.job_id));\n}\n\nfunction pushRowsForBatch(batch) {\n const rows = Array.isArray(batch.sheet_rows) ? batch.sheet_rows : batch.items || [];\n for (const it of rows) {\n if (!it || !it.sku) continue;\n const rowNumber = it.row_number;\n if (rowNumber === undefined || rowNumber === null || rowNumber === '') continue;\n out.push({\n json: {\n sku: String(it.sku).trim(),\n row_number: rowNumber,\n PROCESSADO: PROCESSING,\n ENCONTRADO: PROCESSING,\n NOTIFICADO: PROCESSING,\n },\n });\n }\n}\n\nconst first = $input.first().json || {};\nif (first.parallel_dispatch) {\n const responses = Array.isArray(first.scraper_responses) ? first.scraper_responses : [];\n const batches = Array.isArray(first.scraper_batches) ? first.scraper_batches : [];\n for (let i = 0; i < responses.length; i++) {\n const resp = responses[i];\n if (!responseAccepted(resp)) continue;\n const batch =\n batches.find((b) => Number(b.batch_index) === Number(resp.batch_index)) || batches[i];\n if (batch) pushRowsForBatch(batch);\n }\n return out;\n}\n\nconst httpItems = $input.all();\nconst batches = $('\u2699\ufe0f Formatar Payload Scraper').all();\n\nfor (let i = 0; i < httpItems.length; i++) {\n const resp = httpItems[i].json;\n const hasJob = Boolean(resp.job_id);\n const sc = resp.statusCode != null ? Number(resp.statusCode) : hasJob ? 200 : 0;\n if (!hasJob && (sc < 200 || sc >= 300)) continue;\n const batch = batches[i]?.json;\n if (!batch) continue;\n pushRowsForBatch(batch);\n}\nreturn out;\n",
"mode": "runOnceForAllItems"
},
"id": "c738d1b2-1afd-4163-ba21-6b9800541563",
"name": "\ud83d\udd17 Emparelhar SKUs \u2192 PROCESSADO",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2224,
2336
],
"notes": "Pairs each HTTP response with Formatar batch by index; emits one item per SKU for all channels. Skips non-2xx responses."
},
{
"parameters": {
"operation": "append",
"documentId": {
"__rl": true,
"value": "1ZBU2d3XVsngOYQH12yU7Mg9DcIzVet2dDmhMtZqHSOo",
"mode": "list",
"cachedResultName": "cdp_resultados",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ZBU2d3XVsngOYQH12yU7Mg9DcIzVet2dDmhMtZqHSOo/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": "Historico",
"mode": "name",
"cachedResultName": "Historico",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1ZBU2d3XVsngOYQH12yU7Mg9DcIzVet2dDmhMtZqHSOo/edit#gid=0"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"job_id": "={{ $json.job_id }}",
"origem": "={{ $json.origem }}",
"solicitante": "={{ $json.solicitante }}",
"disparado_em": "={{ $json.disparado_em }}",
"concluido_em": "={{ $json.concluido_em }}",
"tempo_segundos": "={{ $json.tempo_segundos }}",
"status": "={{ $json.status }}",
"skus_lidos": "={{ $json.skus_lidos }}",
"skus_validos": "={{ $json.skus_validos }}",
"skus_encontrados": "={{ $json.skus_encontrados }}",
"skus_falhos": "={{ $json.skus_falhos }}",
"taxa_sucesso_sku": "={{ $json.taxa_sucesso_sku }}",
"taxa_sucesso_sites": "={{ $json.taxa_sucesso_sites }}",
"sites_pesquisados": "={{ $json.sites_pesquisados }}",
"resumo_sites": "={{ $json.resumo_sites }}",
"lista_skus_csv": "={{ $json.lista_skus_csv }}",
"skus_repetidos": "={{ $json.skus_repetidos || '\u2014' }}",
"job_error": "={{ $json.job_error }}"
},
"matchingColumns": [],
"schema": [
{
"id": "job_id",
"displayName": "job_id",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "origem",
"displayName": "origem",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "solicitante",
"displayName": "solicitante",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "disparado_em",
"displayName": "disparado_em",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "concluido_em",
"displayName": "concluido_em",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "tempo_segundos",
"displayName": "tempo_segundos",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "status",
"displayName": "status",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "skus_lidos",
"displayName": "skus_lidos",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "skus_validos",
"displayName": "skus_validos",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "skus_encontrados",
"displayName": "skus_encontrados",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "skus_falhos",
"displayName": "skus_falhos",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "taxa_sucesso_sku",
"displayName": "taxa_sucesso_sku",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "taxa_sucesso_sites",
"displayName": "taxa_sucesso_sites",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "sites_pesquisados",
"displayName": "sites_pesquisados",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "resumo_sites",
"displayName": "resumo_sites",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "lista_skus_csv",
"displayName": "lista_skus_csv",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "skus_repetidos",
"displayName": "skus_repetidos",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
},
{
"id": "job_error",
"displayName": "job_error",
"required": false,
"defaultMatch": false,
"display": true,
"type": "string",
"canBeUsedToMatch": true,
"removed": false
}
],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {}
},
"id": "8f40d678-48f1-446b-9ef1-4199ef0a5695",
"name": "\ud83d\udcdd Erro \u2192 CDP_Resultados (Hist\u00f3rico)",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
2448,
2720
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "// cdp_router - format Scraper dispatch failures for Historico and optional email alert.\n\nfunction env(name) {\n try {\n if (typeof $env !== 'undefined' && $env && $env[name]) {\n return String($env[name]).trim();\n }\n } catch (e) {}\n try {\n if (typeof process !== 'undefined' && process.env && process.env[name]) {\n return String(process.env[name]).trim();\n }\n } catch (e) {}\n return '';\n}\n\nfunction workflowName() {\n try {\n if (typeof $workflow !== 'undefined' && $workflow && $workflow.name) {\n return String($workflow.name);\n }\n } catch (e) {}\n return '';\n}\n\nfunction isDevWorkflow() {\n return workflowName().trim().toLowerCase().startsWith('dev -') || env('CDP_ENV').toLowerCase() === 'dev';\n}\n\nfunction envFor(name) {\n if (isDevWorkflow() && name === 'NOTIFICATION_EMAIL_TO') {\n return env('CDP_DEV_NOTIFICATION_EMAIL_TO');\n }\n return env(name);\n}\n\nfunction compactError(resp) {\n const raw =\n resp.error ||\n resp.message ||\n resp.detail ||\n resp.body?.detail ||\n resp.body?.message ||\n resp.body?.error ||\n JSON.stringify(resp).substring(0, 500);\n return typeof raw === 'string' ? raw : JSON.stringify(raw);\n}\n\nfunction historicoFromError(item, resp, batch) {\n const nowIso = new Date().toISOString();\n const batchIndex = resp.batch_index || batch?.batch_index || 'unknown';\n const totalBatches = resp.total_batches || batch?.total_batches || 'unknown';\n const statusCode = resp.statusCode ?? resp.status_code ?? 'N/A';\n const errorMsg = compactError(resp);\n const items = Array.isArray(batch?.items) ? batch.items : Array.isArray(item.items) ? item.items : [];\n const batchSize = Number(resp.batch_size || batch?.batch_size || items.length || 0);\n const emailTo = String(\n item.reply_email || item.email_from || item.email || envFor('NOTIFICATION_EMAIL_TO') || ''\n ).trim();\n\n let html = '<h2>CDP Job Dispatcher - Scraper API Error</h2>';\n html += '<p><strong>Time:</strong> ' + nowIso + '</p>';\n html += '<p><strong>Batch:</strong> ' + batchIndex + ' / ' + totalBatches + '</p>';\n html += '<p><strong>HTTP Status:</strong> ' + statusCode + '</p>';\n html += '<p><strong>Error:</strong></p>';\n html += '<pre style=\"background:#fee2e2;padding:12px;border-radius:8px\">' + errorMsg + '</pre>';\n html += '<p style=\"color:#718096;font-size:12px\">Automated alert from CDP Job Dispatcher.</p>';\n\n return {\n job_id: String(resp.job_id || 'dispatch-scraper-batch-' + batchIndex),\n origem: 'dispatcher_error_scraper',\n solicitante: String(item.reply_email || item.email_from || item.chat_id || ''),\n disparado_em: String(resp.started_at || item.dispatched_at || nowIso),\n concluido_em: nowIso,\n tempo_segundos: '0',\n status: '\u274c ERRO_DISPATCH',\n skus_lidos: String(batchSize),\n skus_validos: String(batchSize),\n skus_encontrados: '0',\n skus_falhos: String(batchSize),\n taxa_sucesso_sku: '0%',\n taxa_sucesso_sites: '0%',\n sites_pesquisados: String(Array.isArray(batch?.sites) ? batch.sites.length : 0),\n resumo_sites: '{}',\n lista_skus_csv: String(items.map((i) => i.sku).filter(Boolean).join(', ')),\n skus_repetidos: '\u2014',\n job_error: '[HTTP ' + statusCode + '] ' + String(errorMsg).substring(0, 1000),\n email_from: emailTo,\n email_subject: 'Dispatcher Error - Scraper batch ' + batchIndex,\n email_html: html,\n };\n}\n\nconst item = $input.first().json || {};\n\nif (item.parallel_dispatch) {\n const responses = Array.isArray(item.scraper_responses) ? item.scraper_responses : [];\n const batches = Array.isArray(item.scraper_batches) ? item.scraper_batches : [];\n return responses\n .filter((resp) => !resp.accepted)\n .map((resp, index) => {\n const batch =\n batches.find((b) => Number(b.batch_index) === Number(resp.batch_index)) || batches[index] || {};\n return { json: historicoFromError(item, resp, batch) };\n });\n}\n\nreturn [{ json: historicoFromError(item, item, item) }];\n",
"mode": "runOnceForAllItems"
},
"id": "dd0fcac0-5213-41f7-81ec-702dc4be33b7",
"name": "\u274c Formatar Erro de Despacho",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2224,
2720
]
},
{
"parameters": {
"conditions": {
"conditions": [
{
"id": "check-job-id",
"leftValue": "={{ $json.job_id }}",
"rightValue": "",
"operator": {
"type": "string",
"operation": "notEmpty"
}
}
],
"combinator": "and",
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
}
},
"options": {}
},
"id": "a5f7cb38-e051-42aa-9030-b442c1971b42",
"name": "\u2705 API OK?",
"type": "n8n-nodes-base.if",
"typeVersion": 2,
"position": [
2000,
2720
],
"notes": "Routes to success or error path based on API response status."
},
{
"parameters": {
"method": "POST",
"url": "={{ $json.api_jobs_url }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "X-API-Key",
"value": "={{ $json.api_key }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ items: $json.items, sites: $json.sites, callback_url: $json.callback_url, priority: $json.priority || 5, force_refresh: $json.force_refresh === true, batch_group_id: $json.batch_group_id, chat_id: $json.reply_channel === 'telegram' ? $json.chat_id : undefined, command_route: $json.command_route, metadata: $json.metadata, reply_channel: $json.reply_channel, command_origin: $json.command_origin, reply_email: $json.reply_email, notify: $json.notify }) }}",
"options": {
"batching": {
"batch": {
"batchSize": 1,
"batchInterval": 3000
}
},
"response": {
"response": {
"responseFormat": "json"
}
},
"timeout": 60000
}
},
"id": "e919ee2c-1f5e-47ca-879d-ce9b23fed935",
"name": "\ud83d\ude80 POST \u2192 Scraper API (/jobs)",
"retryOnFail": true,
"maxTries": 3,
"waitBetweenTries": 5000,
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
1776,
2720
],
"notes": "Requires CDP_SCRAPER_API_BASE and CDP_API_KEY. Batches POSTs (1 per 3s) to avoid API 500 under parallel load. Retries 3\u00d7 on transient errors.",
"continueOnFail": true
},
{
"parameters": {
"jsCode": "// cdp_router \u2014 pass SKUs through; optional random sample via CDP_DISPATCH_SAMPLE_SIZE (0 = all).\n\nfunction env(name) {\n try {\n if (typeof $env !== 'undefined' && $env && $env[name]) {\n return String($env[name]).trim();\n }\n } catch (e) {}\n try {\n if (typeof process !== 'undefined' && process.env && process.env[name]) {\n return String(process.env[name]).trim();\n }\n } catch (e) {}\n return '';\n}\nfunction envInt(name, defaultVal) {\n const raw = env(name);\n if (!raw) return defaultVal;\n const n = parseInt(raw, 10);\n return Number.isFinite(n) && n >= 0 ? n : defaultVal;\n}\n\nconst data = $input.first().json;\nconst all = Array.isArray(data.skus) ? data.skus : [];\nconst allSheetRows = Array.isArray(data.sheet_rows) ? data.sheet_rows : all;\nconst maxSkus = envInt('CDP_DISPATCH_SAMPLE_SIZE', 0);\nlet skus = all;\nlet sampled = false;\nif (maxSkus > 0 && all.length > maxSkus) {\n const copy = [...all];\n for (let i = copy.length - 1; i > 0; i--) {\n const j = Math.floor(Math.random() * (i + 1));\n const tmp = copy[i];\n copy[i] = copy[j];\n copy[j] = tmp;\n }\n skus = copy.slice(0, maxSkus);\n sampled = true;\n}\nconst sampledSkuSet = new Set(\n skus.map((row) => String(row?.sku || row?.SKU || row).trim().toUpperCase()).filter(Boolean)\n);\nconst sheetRows = allSheetRows.filter((row) => {\n const sku = String(row?.sku || row?.SKU || row).trim().toUpperCase();\n return sampledSkuSet.has(sku);\n});\n\nconst batchGroupId = 'bg-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);\ntry {\n if (typeof $getWorkflowStaticData === 'function') {\n const sd = $getWorkflowStaticData('global');\n sd.cdp_last_batch_group_id = batchGroupId;\n }\n} catch (e) {}\n\nlet commandRoute = 'analisar';\ntry {\n const tg = $('\ud83d\udd00 Switch Comando (Telegram)').first().json;\n if (tg && tg.route) commandRoute = String(tg.route);\n} catch (e) {}\nif (commandRoute === 'analisar') {\n try {\n const em = $('\ud83d\udd00 Switch Comando (Email)').first().json;\n if (em && em.route) commandRoute = String(em.route);\n } catch (e) {}\n}\n\nreturn [\n {\n json: {\n ...data,\n skus,\n sheet_rows: sheetRows,\n valid_skus: skus.length,\n input_valid_skus: sheetRows.length,\n dispatch_sampled: sampled,\n dispatch_sample_limit: maxSkus,\n dispatch_total_before_sample: all.length,\n dispatch_sheet_rows_before_sample: allSheetRows.length,\n batch_group_id: batchGroupId,\n command_route: commandRoute,\n },\n },\n];\n",
"mode": "runOnceForAllItems"
},
"id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"name": "\ud83c\udfb2 Limitar SKUs",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1440,
2480
],
"notes": "All valid SKUs. Parallel router: [0] API Diversos POST [1] Scraper POST [2] confirm. batch_group_id shared."
},
{
"parameters": {
"jsCode": "// cdp_router \u2014 Scraper POST /api/v1/jobs (force_refresh=false \u2192 Redis/PostgreSQL 24h cache).\n\nconst DEFAULT_BATCH_SIZE = 100;\nconst MAX_BATCH_SIZE = 100;\nconst DEFAULT_SITES = [\n 'gm',\n 'ml',\n 'vw',\n 'eu',\n 'pecadireta',\n 'melibox',\n 'goparts',\n 'procurapecas',\n 'ebay',\n];\nconst DEFAULT_SCRAPER_API_BASE =\n 'https://cdp-scrapers-api-prod.bravecoast-b14d791e.eastus2.azurecontainerapps.io';\nconst DEFAULT_N8N_WEBHOOK_BASE = 'https://automacao.tktechnologies.com.br';\nconst data = $input.first().json;\nconst skus = Array.isArray(data.skus) ? data.skus : [];\nconst sheetRows = Array.isArray(data.sheet_rows) ? data.sheet_rows : skus;\n\nfunction env(name) {\n try {\n if (typeof $env !== 'undefined' && $env && $env[name]) {\n return String($env[name]).trim();\n }\n } catch (e) {}\n try {\n if (typeof process !== 'undefined' && process.env && process.env[name]) {\n return String(process.env[name]).trim();\n }\n } catch (e) {}\n return '';\n}\nfunction workflowName() {\n try {\n if (typeof $workflow !== 'undefined' && $workflow && $workflow.name) {\n return String($workflow.name).trim();\n }\n } catch (e) {}\n return '';\n}\nfunction isDevWorkflow() {\n return /^DEV\\s*-/i.test(workflowName()) || /^dev$/i.test(env('CDP_ENV'));\n}\nfunction devEnvName(name) {\n const map = {\n CDP_SCRAPER_API_BASE: 'CDP_DEV_SCRAPER_API_BASE',\n MUVSTOK_SCRAPER_API_BASE: 'CDP_DEV_SCRAPER_API_BASE',\n CDP_API_KEY: 'CDP_DEV_API_KEY',\n MUVSTOK_API_KEY: 'CDP_DEV_API_KEY',\n API_KEY: 'CDP_DEV_API_KEY',\n CDP_SCRAPER_BATCH_SIZE: 'CDP_DEV_SCRAPER_BATCH_SIZE',\n CDP_SCRAPER_SITES: 'CDP_DEV_SCRAPER_SITES',\n WEBHOOK_URL: 'CDP_DEV_WEBHOOK_URL',\n CDP_N8N_WEBHOOK_URL: 'CDP_DEV_N8N_WEBHOOK_URL',\n CDP_N8N_WEBHOOK_PATH: 'CDP_DEV_N8N_WEBHOOK_PATH',\n };\n return map[name] || '';\n}\nfunction envFor(name) {\n if (!isDevWorkflow()) return env(name);\n const mapped = devEnvName(name);\n const value = mapped ? env(mapped) : '';\n if (value) return value;\n if (name === 'WEBHOOK_URL') return env('WEBHOOK_URL') || DEFAULT_N8N_WEBHOOK_BASE;\n if (name === 'CDP_N8N_WEBHOOK_PATH') return 'webhook/dev-scraper-result';\n return '';\n}\nfunction envList(name) {\n const raw = envFor(name);\n return raw ? raw.split(',').map((s) => s.trim()).filter(Boolean) : [];\n}\nfunction trimTrailingSlashes(value) {\n let out = String(value || '').trim();\n while (out.endsWith('/')) out = out.slice(0, -1);\n return out;\n}\nfunction trimSlashes(value) {\n let out = String(value || '').trim();\n while (out.startsWith('/')) out = out.slice(1);\n while (out.endsWith('/')) out = out.slice(0, -1);\n return out;\n}\nfunction readStaticRequester() {\n try {\n if (typeof $getWorkflowStaticData === 'function') {\n const sd = $getWorkflowStaticData('global');\n if (sd && sd.cdp_sheet_requester) return sd.cdp_sheet_requester;\n }\n } catch (e) {}\n return null;\n}\nfunction looksLikeEmail(value) {\n return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(String(value || '').trim());\n}\nfunction skuKey(value) {\n return String(value || '')\n .trim()\n .toUpperCase()\n .replace(/[\\s\\-\\.\\\\/]/g, '');\n}\n\nconst configuredSites = envList('CDP_SCRAPER_SITES');\nconst sites = configuredSites.length ? configuredSites : DEFAULT_SITES;\nconst commandRoute = String(data.command_route || 'analisar');\nconst scraperApiBase = trimTrailingSlashes(\n envFor('CDP_SCRAPER_API_BASE') ||\n envFor('MUVSTOK_SCRAPER_API_BASE') ||\n (isDevWorkflow() ? '' : DEFAULT_SCRAPER_API_BASE)\n);\nconst apiKey = envFor('CDP_API_KEY') || envFor('MUVSTOK_API_KEY') || envFor('API_KEY');\nconst batchSizeRaw = Number(envFor('CDP_SCRAPER_BATCH_SIZE') || DEFAULT_BATCH_SIZE);\nconst BATCH_SIZE = Math.max(\n 1,\n Math.min(MAX_BATCH_SIZE, Number.isFinite(batchSizeRaw) ? batchSizeRaw : DEFAULT_BATCH_SIZE)\n);\n\nconst ctx = readStaticRequester();\nlet chatId = String(data.telegram_chat_id || data.chat_id || '').trim();\nlet emailFrom = String(data.email_from || '').trim();\nconst notifyRaw = String(data.notify || '').trim();\nlet commandOrigin = String(data.command_origin || data.origem || '').trim().toLowerCase();\nlet replyChannel = String(data.reply_channel || '').trim().toLowerCase();\nif (!emailFrom && looksLikeEmail(notifyRaw)) emailFrom = notifyRaw;\nconst dataHasChannel = Boolean(\n replyChannel || commandOrigin === 'email' || commandOrigin === 'telegram' || emailFrom || chatId\n);\nif (ctx) {\n if (!dataHasChannel && !replyChannel && ctx.reply_channel) {\n replyChannel = String(ctx.reply_channel).trim().toLowerCase();\n }\n if (!dataHasChannel && !commandOrigin && ctx.command_origin) {\n commandOrigin = String(ctx.command_origin).trim().toLowerCase();\n }\n if (!dataHasChannel && !emailFrom && ctx.email_from) emailFrom = String(ctx.email_from).trim();\n const inputIsEmail = commandOrigin === 'email' || replyChannel === 'email' || emailFrom;\n if (!inputIsEmail && !chatId && ctx.chat_id) chatId = String(ctx.chat_id).trim();\n}\nlet notify = 'none';\nif (!replyChannel) {\n if (commandOrigin === 'email' || emailFrom) replyChannel = 'email';\n else if (commandOrigin === 'telegram' || chatId) replyChannel = 'telegram';\n}\nif (!commandOrigin && replyChannel) commandOrigin = replyChannel;\nif (replyChannel === 'email') {\n notify = 'email';\n commandOrigin = 'email';\n chatId = '';\n} else if (replyChannel === 'telegram') {\n notify = 'telegram';\n commandOrigin = 'telegram';\n emailFrom = '';\n} else if (chatId) {\n notify = 'telegram';\n replyChannel = 'telegram';\n commandOrigin = commandOrigin || 'telegram';\n} else if (emailFrom) {\n notify = 'email';\n replyChannel = 'email';\n commandOrigin = commandOrigin || 'email';\n}\n\nconst batchGroupId =\n String(data.batch_group_id || '').trim() ||\n 'bg-' + Date.now().toString(36) + '-' + Math.random().toString(36).slice(2, 8);\nconst adHoc = String(commandRoute || '').startsWith('sku');\n\nlet callbackUrl = envFor('CDP_N8N_WEBHOOK_URL');\nif (!callbackUrl) {\n const base = trimTrailingSlashes(envFor('WEBHOOK_URL'));\n const rel = trimSlashes(envFor('CDP_N8N_WEBHOOK_PATH') || 'webhook/scraper-result');\n if (base) callbackUrl = base + '/' + rel;\n}\nif (!callbackUrl && !isDevWorkflow()) {\n callbackUrl = 'https://automacao.tktechnologies.com.br/webhook/scraper-result';\n}\n\nfunction encodeQueryParam(key, value) {\n return encodeURIComponent(key) + '=' + encodeURIComponent(String(value));\n}\nfunction buildQueryString(parts) {\n return parts.map(([k, v]) => encodeQueryParam(k, v)).join('&');\n}\nconst deliveryMode = notify === 'none' ? 'legacy' : 'aggregate';\nconst queryParts = [\n ['notify', notify],\n ['reply_channel', replyChannel || notify],\n ['command_origin', commandOrigin || replyChannel || notify],\n ['batch_group_id', batchGroupId],\n ['dual_run', 'scraper'],\n ['command_route', commandRoute],\n ['delivery_mode', deliveryMode],\n];\nif (notify === 'telegram' && chatId) queryParts.push(['chat_id', chatId]);\nif (notify === 'email' && emailFrom) queryParts.push(['reply_email', emailFrom]);\nif (adHoc) queryParts.push(['ad_hoc', 'true']);\ncallbackUrl += (callbackUrl.includes('?') ? '&' : '?') + buildQueryString(queryParts);\n\nconst batches = [];\nfor (let i = 0; i < skus.length; i += BATCH_SIZE) {\n batches.push(skus.slice(i, i + BATCH_SIZE));\n}\nconst totalBatches = batches.length;\n\nreturn batches.map((batch, index) => {\n const batchSkuKeys = new Set(batch.map((it) => skuKey(it.sku)));\n const batchSheetRows = sheetRows.filter((it) => batchSkuKeys.has(skuKey(it.sku || it.SKU || it)));\n return {\n json: {\n callback_url: callbackUrl,\n items: batch.map((it) => ({\n sku: it.sku,\n brand: it.brand || '',\n description: it.description || '',\n })),\n sheet_rows: batchSheetRows.map((it) => ({\n sku: it.sku,\n row_number: it.row_number ?? null,\n })),\n sites,\n priority: 5,\n force_refresh: false,\n api_jobs_url: scraperApiBase + '/api/v1/jobs',\n api_key: apiKey,\n batch_group_id: batchGroupId,\n batch_index: index + 1,\n total_batches: totalBatches,\n batch_size: batch.length,\n sheet_row_count: batchSheetRows.length,\n ad_hoc: adHoc,\n notify,\n reply_channel: replyChannel || notify,\n command_origin: commandOrigin || replyChannel || notify,\n chat_id: notify === 'telegram' ? chatId : undefined,\n reply_email: notify === 'email' ? emailFrom : '',\n command_route: commandRoute,\n metadata: {\n source: 'cdp_router',\n pipeline: 'scraper',\n command_route: commandRoute,\n command_origin: commandOrigin || replyChannel || notify,\n reply_channel: replyChannel || notify,\n notify,\n delivery_mode: deliveryMode,\n chat_id: notify === 'telegram' ? chatId : '',\n reply_email: notify === 'email' ? emailFrom : '',\n batch_group_id: batchGroupId,\n batch_index: index + 1,\n total_batches: totalBatches,\n cache_policy: 'redis_24h',\n unique_skus: batch.length,\n sheet_rows: batchSheetRows.length,\n },\n },\n };\n});\n",
"mode": "runOnceForAllItems"
},
"id": "99162794-4160-4334-bdd3-885473fe4524",
"name": "\u2699\ufe0f Formatar Payload Scraper",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1552,
2720
],
"notes": "CDP v1.0: active default sites gm,ml,vw,eu,pecadireta; optional CDP_SCRAPER_SITES env can include melibox. Sends only supported API body fields and carries requester context in callback_url query params."
},
{
"parameters": {
"jsCode": "// cdp_router \u2014 validate sheet rows, dedupe dispatch SKUs, preserve sheet rows.\n\nconst raw = $input.all();\nconst seen = new Set();\nconst uniqueBySku = new Map();\nconst sheetRows = [];\nconst dqIssues = [];\nlet skippedProcessado = 0;\n\nfunction normalizeStatus(value) {\n return String(value || '')\n .trim()\n .toLowerCase()\n .normalize('NFD')\n .replace(/[\\u0300-\\u036f]/g, '')\n .replace(/[^a-z0-9]+/g, ' ')\n .trim();\n}\n\nfunction normalizeSku(value) {\n return String(value || '')\n .trim()\n .toUpperCase()\n .replace(/[\\s\\-\\.\\\\/]/g, '');\n}\n\nfor (const item of raw) {\n const row = item.json;\n const rawSku = row.CODIGO ?? row.SKU ?? row.sku ?? row.codigo;\n const sku = normalizeSku(rawSku);\n\n const processado = normalizeStatus(row.PROCESSADO);\n if (processado === 'processado' || processado === 'sim' || processado === 'true') {\n skippedProcessado++;\n continue;\n }\n\n if (!sku) {\n dqIssues.push({ issue: 'EMPTY_SKU', row: JSON.stringify(row) });\n continue;\n }\n if (sku.length < 3) {\n dqIssues.push({ issue: 'SHORT_SKU', sku });\n continue;\n }\n if (seen.has(sku)) {\n dqIssues.push({\n issue: 'DUPLICATE_SKU',\n sku,\n row_number: row.row_number ?? null,\n action: 'deduped_dispatch_preserved_sheet_row',\n });\n }\n seen.add(sku);\n\n const rowData = {\n sku,\n sku_original: rawSku ? String(rawSku).trim() : '',\n brand: row.UNIDADE ? String(row.UNIDADE).trim() : '',\n description: row.ITEM ? String(row.ITEM).trim() : '',\n row_number: row.row_number ?? null,\n notify_email: row['E-MAIL'] ? String(row['E-MAIL']).trim() : '',\n notify_phone: row.CONTATO ? String(row.CONTATO).trim() : '',\n };\n sheetRows.push(rowData);\n\n if (!uniqueBySku.has(sku)) {\n uniqueBySku.set(sku, { ...rowData });\n } else {\n const existing = uniqueBySku.get(sku);\n if (existing) {\n if (!existing.brand && rowData.brand) existing.brand = rowData.brand;\n if (!existing.description && rowData.description) existing.description = rowData.description;\n if (!existing.notify_email && rowData.notify_email) existing.notify_email = rowData.notify_email;\n if (!existing.notify_phone && rowData.notify_phone) existing.notify_phone = rowData.notify_phone;\n }\n }\n}\n\nconst duplicateIssues = dqIssues.filter((i) => i.issue === 'DUPLICATE_SKU');\nconst duplicateSkus = [...new Set(duplicateIssues.map((i) => i.sku).filter(Boolean))];\nconst skus = [...uniqueBySku.values()];\n\nreturn [\n {\n json: {\n total_read: raw.length,\n input_valid_skus: sheetRows.length,\n valid_skus: skus.length,\n unique_skus: seen.size,\n skipped_processado: skippedProcessado,\n duplicates: duplicateIssues.length,\n duplicate_skus: duplicateSkus,\n empty_skus: dqIssues.filter((i) => i.issue === 'EMPTY_SKU').length,\n short_skus: dqIssues.filter((i) => i.issue === 'SHORT_SKU').length,\n dq_issues: dqIssues,\n skus,\n sheet_rows: sheetRows,\n dispatched_at: new Date().toISOString(),\n },\n },\n];\n",
"mode": "runOnceForAllItems"
},
"id": "d5e14e42-868a-4b20-8743-6de4fd18ee8c",
"name": "\ud83d\udd0d DQ: Validar & Deduplicar",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1328,
2240
],
"notes": "v0.7.0: ORIGEM removed from per-SKU. notify_email/notify_phone read from E-MAIL/CONTATO columns. Routing via ad_hoc presence of chat_id/email_from only."
},
{
"parameters": {
"documentId": {
"__rl": true,
"value": "1IGhsIhrwlnMaCduR-W-eIi9O4mMO2pPYjE-tefgIPII",
"mode": "list",
"cachedResultName": "cdp_skus",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IGhsIhrwlnMaCduR-W-eIi9O4mMO2pPYjE-tefgIPII/edit?usp=drivesdk"
},
"sheetName": {
"__rl": true,
"value": 843035952,
"mode": "list",
"cachedResultName": "SKUs",
"cachedResultUrl": "https://docs.google.com/spreadsheets/d/1IGhsIhrwlnMaCduR-W-eIi9O4mMO2pPYjE-tefgIPII/edit#gid=843035952"
},
"options": {}
},
"id": "51a927fa-a416-4e47-9962-5ad6da0a520d",
"name": "\ud83d\udcca Ler CDP_SKUs",
"type": "n8n-nodes-base.googleSheets",
"typeVersion": 4.5,
"position": [
1104,
2240
],
"credentials": {
"googleSheetsOAuth2Api": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 8 * * 1"
}
]
}
},
"id": "2d59bd66-7a09-4741-8416-97842e8854e4",
"name": "\u23f0 Trigger Agendado (Seg 8h)",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
880,
3216
],
"notes": "Runs every Monday at 8am. Adjust cron as needed."
},
{
"parameters": {},
"id": "a0cbb6d2-629d-4043-bdc1-a8fca392c3e2",
"name": "\u25b6\ufe0f Trigger Manual",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
880,
3024
]
},
{
"parameters": {
"jsCode": "// \u2500\u2500\u2500 Telegram Command Router v4 \u2014 adds .status / .andamento \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\nconst DEFAULT_SCRAPER_API_BASE =\n 'https://cdp-scrapers-api-prod.bravecoast-b14d791e.eastus2.azurecontainerapps.io';\n\nfunction envValue(name) {\n try {\n if (typeof $env !== 'undefined' && $env && $env[name]) {\n return String($env[name]).trim();\n }\n } catch (e) {\n return '';\n }\n try {\n if (typeof process !== 'undefined' && process.env && process.env[name]) {\n return String(process.env[name]).trim();\n }\n } catch (e) {\n return '';\n }\n return '';\n}\nfunction workflowName() {\n try {\n if (typeof $workflow !== 'undefined' && $workflow && $workflow.name) {\n return String($workflow.name).trim();\n }\n } catch (e) {}\n return '';\n}\nfunction isDevWorkflow() {\n return /^DEV\\s*-/i.test(workflowName()) || /^dev$/i.test(envValue('CDP_ENV'));\n}\nfunction devEnvName(name) {\n const map = {\n CDP_SCRAPER_API_BASE: 'CDP_DEV_SCRAPER_API_BASE',\n MUVSTOK_SCRAPER_API_BASE: 'CDP_DEV_SCRAPER_API_BASE',\n CDP_API_KEY: 'CDP_DEV_API_KEY',\n MUVSTOK_API_KEY: 'CDP_DEV_API_KEY',\n API_KEY: 'CDP_DEV_API_KEY',\n TELEGRAM_ALLOWED_CHAT_IDS: 'TELEGRAM_DEV_ALLOWED_CHAT_IDS',\n TELEGRAM_BOT_TOKEN: 'TELEGRAM_DEV_BOT_TOKEN',\n TELEGRAM_TOKEN: 'TELEGRAM_DEV_BOT_TOKEN',\n TELEGRAM_API_TOKEN: 'TELEGRAM_DEV_BOT_TOKEN',\n CDP_STATUS_COMMANDS: 'CDP_DEV_STATUS_COMMANDS',\n };\n return map[name] || '';\n}\nfunction envFor(name) {\n if (!isDevWorkflow()) return envValue(name);\n const mapped = devEnvName(name);\n const value = mapped ? envValue(mapped) : '';\n if (value) return value;\n if (name === 'CDP_STATUS_COMMANDS') return envValue(name);\n return '';\n}\n\nfunction envList(name) {\n const raw = envFor(name);\n return raw ? raw.split(',').map((s) => s.trim()).filter(Boolean) : [];\n}\n\nfunction statusCommands() {\n const raw = envList('CDP_STATUS_COMMANDS');\n if (raw.length) return raw;\n return ['.status', '.andamento', '.progresso'];\n}\n\nfunction trimTrailingSlashes(value) {\n let out = String(value || '').trim();\n while (out.endsWith('/')) out = out.slice(0, -1);\n return out;\n}\n\nfunction scraperApiBase() {\n return trimTrailingSlashes(\n envFor('CDP_SCRAPER_API_BASE') ||\n envFor('MUVSTOK_SCRAPER_API_BASE') ||\n (isDevWorkflow() ? '' : DEFAULT_SCRAPER_API_BASE)\n );\n}\n\nfunction cdpApiKey() {\n return envFor('CDP_API_KEY') || envFor('MUVSTOK_API_KEY') || envFor('API_KEY');\n}\n\nfunction telegramBotToken() {\n return (\n envFor('TELEGRAM_BOT_TOKEN') ||\n envFor('TELEGRAM_TOKEN') ||\n envFor('TELEGRAM_API_TOKEN')\n );\n}\n\nconst msg = $input.first().json;\nconst chatId = String(msg.message?.chat?.id || '');\nconst username = String(msg.message?.from?.username || msg.message?.from?.first_name || '');\nconst text = String(msg.message?.text || msg.message?.caption || '').trim();\nconst doc = msg.message?.document || null;\nconst hasFile = !!doc;\n\nconst allowed = envList('TELEGRAM_ALLOWED_CHAT_IDS');\nif (allowed.length && !allowed.includes(chatId)) {\n return [{ json: { route: 'unauthorized', chat_id: chatId } }];\n}\n\nconst lower = text.toLowerCase();\nconst isAnalisar = lower.startsWith('.analisar');\nconst isSku = lower.startsWith('.sku');\nconst isStatus = statusCommands().some((cmd) => lower.startsWith(cmd.toLowerCase()));\n\nif (isStatus) {\n const base = scraperApiBase();\n return [\n {\n json: {\n route: 'status',\n chat_id: chatId,\n username,\n origem: 'telegram',\n notify: chatId,\n dispatch_runs_lookup_url:\n base + '/api/v1/dispatch-runs/active/for-chat/' + encodeURIComponent(chatId),\n dispatch_runs_api_key: cdpApiKey(),\n },\n },\n ];\n}\n\nif (isSku || hasFile) {\n const afterCmd = isSku ? text.slice(4).trim() : '';\n const textSkus = afterCmd\n .split(/[\\n,;\\s]+/)\n .map((s) => s.trim().toUpperCase())\n .filter((s) => /^[A-Z0-9]{5,}$/.test(s))\n .slice(0, 200);\n\n if (hasFile && textSkus.length > 0) {\n return [\n {\n json: {\n route: 'sku_both',\n chat_id: chatId,\n username,\n text_skus: textSkus,\n file_id: doc.file_id,\n file_name: doc.file_name || 'attachment',\n telegram_bot_token: telegramBotToken(),\n origem: 'telegram',\n notify: chatId,\n },\n },\n ];\n }\n if (hasFile && textSkus.length === 0) {\n return [\n {\n json: {\n route: 'sku_file',\n chat_id: chatId,\n username,\n text_skus: [],\n file_id: doc.file_id,\n file_name: doc.file_name || 'attachment',\n telegram_bot_token: telegramBotToken(),\n origem: 'telegram',\n notify: chatId,\n },\n },\n ];\n }\n if (!hasFile && textSkus.length > 0) {\n return [\n {\n json: {\n route: 'sku_text',\n chat_id: chatId,\n username,\n text_skus: textSkus,\n origem: 'telegram',\n notify: chatId,\n },\n },\n ];\n }\n return [\n {\n json: {\n route: 'sku_empty',\n chat_id: chatId,\n username,\n origem: 'telegram',\n notify: chatId,\n },\n },\n ];\n}\n\nif (isAnalisar) {\n return [\n {\n json: {\n route: 'analisar',\n chat_id: chatId,\n username,\n is_full_sheet_trigger: true,\n origem: 'telegram',\n notify: chatId,\n },\n },\n ];\n}\n\nreturn [{ json: { route: 'ignore', chat_id: chatId } }];\n",
"mode": "runOnceForAllItems"
},
"id": "2e1cfa31-4577-41a0-b942-ff2760cfb611",
"name": "\ud83d\udcf1 Roteador de Comando",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-16,
1920
],
"notes": "v4: routes .analisar, .sku text, .sku file, .sku both, .sku empty, .status/.andamento, ignore"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "5d6e44e4-3ab7-42b8-874b-54b0c5ddf06b",
"leftValue": "={{ $json.route }}",
"rightValue": "analisar",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "analisar"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "b51ccc60-5c17-41e8-a06e-170e7f879fe9",
"leftValue": "={{ $json.route }}",
"rightValue": "sku_text",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "sku_text"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "3a84bad3-4c62-4abf-aa5f-5d219781e651",
"leftValue": "={{ $json.route }}",
"rightValue": "sku_file",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "sku_file"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "8d7a625e-8215-436c-99e2-360b5b9827fb",
"leftValue": "={{ $json.route }}",
"rightValue": "sku_both",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "sku_both"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "f26fec1f-3e90-477b-8a1e-c7777c1b0a49",
"leftValue": "={{ $json.route }}",
"rightValue": "sku_empty",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "sku_empty"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "775da2ca-b72f-4f4f-8ff3-f02ecdb7269c",
"leftValue": "={{ $json.route }}",
"rightValue": "ignore",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "ignore"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "130b9b9e-f4cd-4dc6-9954-ed20ef1d6d6f",
"leftValue": "={{ $json.route }}",
"rightValue": "unauthorized",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "unauthorized"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "7cdc65c2-5860-44e1-a03e-4e1553168a61",
"leftValue": "={{ $json.route }}",
"rightValue": "status",
"operator": {
"type": "string",
"operation": "equals",
"name": "filter.operator.equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "status"
}
]
},
"options": {
"fallbackOutput": "none"
}
},
"id": "76aec9fb-5192-40e1-9229-62aa7f3874b8",
"name": "\ud83d\udd00 Switch Comando (Telegram)",
"type": "n8n-nodes-base.switch",
"typeVersion": 3,
"position": [
208,
1840
],
"notes": "Routes: [0]analisar [1]sku_text [2]sku_file [3]sku_both [4]sku_empty [5]ignore [6]unauthorized [7]status"
},
{
"parameters": {
"jsCode": "\n// \u2500\u2500\u2500 Email Command Router v4 \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n// v4: Detects attachments in both simple mode (msg.attachments) and raw mode\n// (msg.payload.parts[]) from Gmail Trigger with simple=false.\n// Passes messageId for downstream attachment download.\nfunction envList(name) {\n try {\n const raw = (typeof process !== 'undefined' && process.env && process.env[name]) || '';\n return raw.split(',').map(s => s.trim()).filter(Boolean);\n } catch(e) { return []; }\n}\n\nconst msg = $input.first().json;\nlet fromEmail = '';\nif (msg.from?.value?.[0]?.address) fromEmail = String(msg.from.value[0].address).trim().toLowerCase();\nelse if (typeof msg.from === 'string') fromEmail = msg.from.trim().toLowerCase();\n\n// \u2500\u2500 Auth check \u2500\u2500\nconst allowed = envList('EMAIL_ALLOWED_SENDERS').map(s => s.toLowerCase());\nif (allowed.length && !allowed.some(a => fromEmail.includes(a))) {\n return [{ json: { route: 'unauthorized', email_from: fromEmail } }];\n}\n\n// \u2500\u2500 Read subject + body \u2500\u2500\nconst subject = String(msg.subject || '').trim();\nconst bodyRaw = String(msg.textPlain || msg.text || msg.html || '').replace(/<[^>]+>/g, ' ').replace(/\\s+/g, ' ').trim();\n\nconst subjectL = subject.toLowerCase();\nconst bodyLines = bodyRaw.split(/\\r?\\n/).map(l => l.trim()).filter(Boolean);\nconst bodyFirstL = (bodyLines[0] || '').toLowerCase();\nlet cmdSource;\nif (subjectL.startsWith('.analisar') || subjectL.startsWith('.sku')) {\n cmdSource = subject;\n} else if (bodyFirstL.startsWith('.analisar') || bodyFirstL.startsWith('.sku')) {\n cmdSource = bodyRaw;\n} else {\n cmdSource = subject || bodyRaw;\n}\nconst lower = cmdSource.toLowerCase();\n\n// \u2500\u2500 Attachment check: simple mode (msg.attachments) \u2500\u2500\nconst attachments = msg.attachments || [];\nlet xlsAttachment = attachments.find(a =>\n /\\.(xlsx?|csv)$/i.test(a.name || a.filename || '')\n);\nlet messageId = String(msg.id || msg.messageId || '').trim();\n\n// \u2500\u2500 Attachment check: raw mode (msg.payload.parts[]) \u2500\u2500\n// Gmail Trigger with simple=false returns raw Gmail API structure\nif (!xlsAttachment && msg.payload && msg.payload.parts) {\n const parts = msg.payload.parts;\n // Flatten: some emails nest parts inside parts (multipart/mixed > multipart/alternative + attachment)\n const allParts = [];\n function collectParts(partsList) {\n for (const p of partsList) {\n allParts.push(p);\n if (p.parts) collectParts(p.parts);\n }\n }\n collectParts(parts);\n\n for (const part of allParts) {\n const fn = String(part.filename || '').trim();\n if (/\\.(xlsx?|csv)$/i.test(fn) && part.body && part.body.attachmentId) {\n xlsAttachment = {\n id: part.body.attachmentId,\n name: fn,\n mimeType: part.mimeType || '',\n size: part.body.size || 0\n };\n break;\n }\n }\n // Also try to get messageId from raw payload\n if (!messageId && msg.payload.headers) {\n const msgIdHeader = msg.payload.headers.find(h => h.name?.toLowerCase() === 'message-id');\n if (msgIdHeader) messageId = String(msgIdHeader.value || '').trim();\n }\n}\n\n// Fallback messageId from threadId or any id on the message root\nif (!messageId) messageId = String(msg.threadId || '').trim();\n\nconst hasFile = !!xlsAttachment;\n\nconst isAnalisar = lower.startsWith('.analisar');\nconst isSku = lower.startsWith('.sku');\n\nif (isSku || hasFile) {\n const afterCmd = isSku ? cmdSource.slice(4).trim() : '';\n const textSkus = afterCmd\n .split(/[\\n,;\\s]+/)\n .map(s => s.trim().toUpperCase())\n .filter(s => /^[A-Z0-9]{5,}$/.test(s))\n .slice(0, 200);\n\n const baseOut = {\n email_from: fromEmail,\n notify: fromEmail,\n origem: 'email',\n message_id: messageId\n };\n\n if (hasFile && textSkus.length > 0) {\n return [{ json: { route: 'sku_both', ...baseOut, text_skus: textSkus,\n attachment_id: xlsAttachment.id || '', attachment_name: xlsAttachment.name || xlsAttachment.filename || '' }}];\n }\n if (hasFile && textSkus.length === 0) {\n return [{ json: { route: 'sku_file', ...baseOut,\n attachment_id: xlsAttachment.id || '', attachment_name: xlsAttachment.name || xlsAttachment.filename || '' }}];\n }\n if (!hasFile && textSkus.length > 0) {\n return [{ json: { route: 'sku_text', ...baseOut, text_skus: textSkus }}];\n }\n return [{ json: { route: 'sku_empty', ...baseOut }}];\n}\n\nif (isAnalisar) {\n return [{ json: { route: 'analisar', email_from: fromEmail, notify: fromEmail,\n is_full_sheet_trigger: true, origem: 'email', message_id: messageId }}];\n}\n\nreturn [{ json: { route: 'ignore', email_from: fromEmail } }];\n
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
gmailOAuth2googleSheetsOAuth2ApitelegramApi
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
cdp_router. Uses gmailTrigger, telegramTrigger, googleSheets, httpRequest. Event-driven trigger; 53 nodes.
Source: https://github.com/tktechnologies/cdp-hub/blob/f2ea9160f764c777c222b2a6b7d1fcc0925ee17f/n8n/workflows/cdp_router.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.
cdp_router. Uses gmailTrigger, telegramTrigger, googleSheets, httpRequest. Event-driven trigger; 53 nodes.
This workflow turns scattered user feedback into a structured product backlog pipeline. It collects feedback from three channels (Telegram bot, Google Form/Sheets, and Gmail), normalizes it, and sends
Automatically process invoices and receipts using Gemini OCR, extracting data directly into Google Sheets from multiple sources including Google Drive, Gmail, and Telegram. This powerful workflow ensu
Turn job searching into a conversational experience! This intelligent Telegram bot automatically scrapes job postings from LinkedIn, Indeed, and Monster, filters for sales & marketing positions, and d
This n8n template demonstrates how to use AI to score the all Resumes by matching it with Job profile