{"updatedAt":"2026-07-06T21:20:23.054Z","createdAt":"2026-04-17T02:40:26.739Z","id":"uvnMZfrW0Giut5lM","name":"OOeirense - Telegram → Publicar Matéria no Portal","description":null,"active":true,"isArchived":false,"nodes":[{"parameters":{"jsCode":"const response = $input.first().json.body;\nconst results = response.result || [];\nif (!results.length) { return []; }\n\nconst msg = results[0];\n// SEMPRE avancar offset local, mesmo quando nao for para este workflow\n// (para nao bloquear o Instagram workflow que le o mesmo bot)\nconst staticData = $getWorkflowStaticData('global');\nstaticData.publishLastUpdateId = msg.update_id + 1;\n\nif (msg.message?.from?.is_bot) { return []; }\nconst chatId = String(msg.message?.chat?.id || '');\nif (chatId !== '7769073261') { return []; }\n\nconst text = (msg.message?.text || '').trim();\nconst match = text.match(/^(?:publicar|publica[cç][aã]o)\\s+(https?:\\/\\/\\S+)/i);\nif (!match) { return []; }\n\nconst url = match[1];\n\n// ── Trava de deduplicação por URL ──\n// Mantém lista das ultimas URLs processadas. Se URL repetida em <24h, bloqueia.\n// Persiste como array top-level (subchaves nao persistem no task runner v2.15).\nconst now = Date.now();\nconst TTL_MS = 24 * 60 * 60 * 1000; // 24h — vale para publicacao CONFIRMADA (ok:true)\nconst TRY_TTL_MS = 10 * 60 * 1000;  // 10min — tentativa em curso (anti-duplicata concorrente)\nconst prev = Array.isArray(staticData.processedUrls) ? staticData.processedUrls : [];\nconst fresh = prev.filter(e => e && e.ts && (now - e.ts) < TTL_MS);\n\n// Bloqueia se JA FOI PUBLICADA (ok:true) em <24h, ou se ha tentativa em curso (<10min).\n// Tentativa que falhou libera sozinha em 10min — antes ficava travada 24h em silencio.\nif (fresh.some(e => e.url === url && (e.ok === true || (now - e.ts) < TRY_TTL_MS))) {\n  staticData.processedUrls = fresh;\n  return [];\n}\n\n// marca a TENTATIVA (ok:false). Quem confirma com ok:true e o no 'Parsear Materia'.\nconst semEsta = fresh.filter(e => e.url !== url);\nsemEsta.push({ url, ts: now, ok: false });\nstaticData.processedUrls = semEsta.slice(-200);\n\nreturn [{ json: {\n  url,\n  messageId: msg.message.message_id,\n  chatId,\n  updateId: msg.update_id\n} }];\n"},"id":"node-extract","name":"Extrair URL (publicar)","type":"n8n-nodes-base.code","typeVersion":2,"position":[432,304]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ chat_id: \"7769073261\", text: \"⏳ Gerando matéria...\\n\\n🔗 \" + $json.url, parse_mode: \"HTML\" }) }}","options":{}},"id":"node-processing","name":"Telegram - Processando","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[624,96]},{"parameters":{"url":"={{ 'https://r.jina.ai/' + $('Extrair URL (publicar)').first().json.url }}","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Accept","value":"application/json"},{"name":"User-Agent","value":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"},{"name":"X-Timeout","value":"45"}]},"options":{"timeout":60000,"retry":{"tryTimes":2,"waitBetweenTries":3000}}},"id":"node-jina","name":"Jina Reader","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[640,352],"onError":"continueRegularOutput"},{"parameters":{"jsCode":"// Jina Reader retorna: { data: { title, description, url, content, metadata } }\nconst response = $('Jina Reader').first().json;\nconst jinaData = response.data || response;\nconst meta = jinaData.metadata || {};\nconst url = $('Extrair URL (publicar)').first().json.url;\n\n// Titulo + descricao\nlet title = (meta['og:title'] || jinaData.title || '').replace(/\\s*[-|].*$/, '').trim();\nlet description = (meta['og:description'] || meta.description || jinaData.description || '').trim();\n\n// Imagem — priorizar og:image do metadata\nlet imageUrl = (meta['og:image'] || meta['twitter:image'] || '').trim();\nlet content = jinaData.content || '';\n\n// Detectar bloqueio do Jina (403 / conteudo vazio / pagina de erro)\nconst jinaBlocked =\n  /403 forbidden|access denied|don't have permission/i.test(title) ||\n  /403 forbidden|don't have permission/i.test(content) ||\n  content.trim().length < 300 ||\n  !imageUrl;\n\nif (jinaBlocked) {\n  try {\n    const html = await this.helpers.httpRequest({\n      method: 'GET',\n      url: url,\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n        'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8',\n      },\n      returnFullResponse: false,\n    });\n    const extractMeta = (html, key) => {\n      let rx = new RegExp('<meta[^>]+(?:property|name)\\\\s*=\\\\s*[\"\\'](' + key + ')[\"\\'][^>]*content\\\\s*=\\\\s*[\"\\']([^\"\\']+)[\"\\']', 'i');\n      let m = html.match(rx);\n      if (m) return m[2];\n      rx = new RegExp('<meta[^>]+content\\\\s*=\\\\s*[\"\\']([^\"\\']+)[\"\\'][^>]*(?:property|name)\\\\s*=\\\\s*[\"\\'](' + key + ')[\"\\']', 'i');\n      m = html.match(rx);\n      return m ? m[1] : '';\n    };\n    const newTitle = (extractMeta(html, 'og:title') || extractMeta(html, 'twitter:title') || '').replace(/\\s*[-|].*$/, '').trim();\n    const newDesc = extractMeta(html, 'og:description') || extractMeta(html, 'twitter:description') || extractMeta(html, 'description') || '';\n    const newImg = extractMeta(html, 'og:image') || extractMeta(html, 'twitter:image') || '';\n    if (newTitle) title = newTitle;\n    if (newDesc) description = newDesc;\n    if (newImg) imageUrl = newImg;\n    const paras = [];\n    const pRx = /<p[^>]*>([\\s\\S]*?)<\\/p>/gi;\n    let mm;\n    while ((mm = pRx.exec(html)) !== null) {\n      const txt = mm[1].replace(/<[^>]+>/g, '').replace(/&nbsp;/g,' ').replace(/&amp;/g,'&').replace(/&quot;/g,'\"').replace(/&#39;/g,\"'\").trim();\n      if (txt.length > 40) paras.push(txt);\n    }\n    const fallbackContent = paras.join('\\n\\n');\n    if (fallbackContent.length > content.length) content = fallbackContent;\n  } catch (e) {\n    // se fallback falhar, deixa campos como estao\n  }\n}\n\n// Decodificar entidades HTML na URL (ex: &amp; -> &)\nimageUrl = imageUrl.replace(/&amp;/g, '&');\n\n// Se og:image usa CDN Cloudflare (cdn-cgi/image/...), extrair URL original\nif (imageUrl.includes('cdn-cgi/image/')) {\n  const originalMatch = imageUrl.match(/\\/https?:\\/\\/.+$/);\n  if (originalMatch) imageUrl = originalMatch[0].substring(1);\n}\n\n// ── Remover marca d'agua dos portais que servem og:image watermarked ──\n// gp1.com.br: /media/watermarks/ → /media/image_bank/\n// lupa1.com.br: /storage/XXX/conversions/*-social.jpg → /storage/XXX/*.{jpeg,jpg,png}\nconst isGp1Watermark = /gp1\\.com\\.br\\/media\\/watermarks\\//i.test(imageUrl);\nconst isLupa1Watermark = /lupa1\\.com\\.br\\/storage\\/\\d+\\/conversions\\//i.test(imageUrl);\n\nif (isGp1Watermark || isLupa1Watermark) {\n  try {\n    const html = await this.helpers.httpRequest({\n      method: 'GET',\n      url: url,\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n        'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8',\n      },\n      returnFullResponse: false,\n    });\n\n    if (isGp1Watermark) {\n      // Pega imagens em /media/image_bank/, exclui thumbnails (.90x58, .60x60 etc)\n      const rx = /https:\\/\\/[^\"'\\s)]*gp1\\.com\\.br\\/media\\/image_bank\\/[^\"'\\s)]+\\.(?:jpg|jpeg|png|webp)[^\"'\\s)]*/gi;\n      const candidates = (html.match(rx) || []).filter(u => !/\\.\\d{2,3}x\\d{2,3}_/.test(u));\n      const large = candidates.find(u => /1200x|800x|upscale/i.test(u));\n      if (large || candidates[0]) imageUrl = large || candidates[0];\n    } else if (isLupa1Watermark) {\n      const m = imageUrl.match(/\\/storage\\/(\\d+)\\/conversions\\//);\n      if (m) {\n        const storageId = m[1];\n        const rx = new RegExp('https://[^\"\\'\\\\s)]*lupa1\\\\.com\\\\.br/storage/' + storageId + '/[^\"\\'\\\\s)]+\\\\.(?:jpg|jpeg|png|webp)[^\"\\'\\\\s)]*', 'gi');\n        const candidates = (html.match(rx) || []).filter(u => !/\\/conversions\\//.test(u));\n        if (candidates[0]) imageUrl = candidates[0];\n      }\n    }\n  } catch (e) {\n    // Se fetch falhar, mantem og:image original (com marca)\n  }\n}\n\n// Fallback: primeira imagem real no markdown (pular logos, icones, social)\nif (!imageUrl) {\n  const skipPatterns = /logo|social-40|icon|favicon|brand|avatar|banner_site|sprite|sem-foto/i;\n  const imgRegex = /!\\[.*?\\]\\((https?:\\/\\/[^\\s)]+\\.(jpg|jpeg|png|webp)[^\\s)]*)\\)/gi;\n  let match;\n  while ((match = imgRegex.exec(content)) !== null) {\n    if (!skipPatterns.test(match[1])) { imageUrl = match[1]; break; }\n  }\n}\n\n// ABORTAR se nao tiver imagem\nif (!imageUrl) {\n  throw new Error('SEM_IMAGEM: nao foi possivel extrair imagem da materia. URL: ' + url);\n}\n\n// Corpo\nconst body = content\n  .replace(/!\\[.*?\\]\\(.*?\\)/g, '')\n  .replace(/\\[([^\\]]+)\\]\\([^\\)]+\\)/g, '$1')\n  .replace(/#{1,6}\\s/g, '')\n  .replace(/[*_]{1,3}/g, '')\n  .replace(/\\n{3,}/g, '\\n\\n')\n  .trim()\n  .substring(0, 3000) || description;\n\nreturn [{ json: { url, title, description, ogImage: imageUrl, body } }];\n"},"id":"node-extract-data","name":"Extrair Dados","type":"n8n-nodes-base.code","typeVersion":2,"position":[880,576],"onError":"continueErrorOutput"},{"parameters":{"method":"POST","url":"https://api.openai.com/v1/chat/completions","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"Bearer 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"},{"name":"Content-Type","value":"application/json"}]},"sendBody":true,"specifyBody":"json","jsonBody":"={{ ({\n  \"model\": \"gpt-4o\",\n  \"max_tokens\": 4500,\n  \"temperature\": 0.2,\n  \"messages\": [{\n    \"role\": \"user\",\n    \"content\": `Você é jornalista do portal O Oeirense (Oeiras, Piauí). REESCREVA a matéria-fonte abaixo com palavras próprias, FIEL AOS FATOS, sem inventar nada. Produza JSON (sem markdown, sem \\`\\`\\`).\n\nESTRUTURA:\n1. CHAPÉU: 1-2 palavras MAIÚSCULAS que rotulem o tema específico (ex: SEMANA SANTA, ELEIÇÕES 2026, SAÚDE PÚBLICA, PROTEÇÃO ANIMAL)\n2. TÍTULO: até 100 caracteres, direto, sem ponto final\n3. SUBTÍTULO: 2 frases, 200-280 caracteres, com dados concretos\n4. CORPO (bodyHtml): parágrafos em HTML usando <p> (e <em> só para citação que exista LITERALMENTE na fonte). Use a QUANTIDADE de parágrafos que a fonte sustentar — normalmente 3 a 6. NÃO force tamanho: se a fonte é curta, a matéria é curta.\n   - Comece pelo lide (quem, o quê, quando, onde) e desenvolva apenas contexto e dados QUE ESTEJAM NA FONTE.\n   - NÃO crie parágrafo de \"reações\", \"repercussão\" ou \"declarações\" se a fonte não relatar isso de fato.\n   - Impacto local: só escreva sobre efeito em Oeiras ou na região SE a fonte tratar disso. Se o fato não tem ligação com Oeiras, relate-o como ele é, sem inventar conexão local.\n5. CATEGORIA (categorySlug) — ESCOLHA UMA baseado no TEMA CENTRAL da matéria:\n   • \"cidades\" — notícias urbanas/municipais gerais, trânsito, comércio local, cotidiano, ONGs, proteção animal, acidentes que não são crime\n   • \"politica\" — Câmara, prefeitura, eleições, vereadores, deputados, governo, projetos de lei, ALEPI\n   • \"seguranca\" — Polícia Civil/Militar, prisões, apreensões, investigações criminais, violência urbana\n   • \"saude\" — SUS, UBS, hospitais, campanhas, secretaria de saúde, medicina (exceto graduação)\n   • \"educacao\" — escolas, universidades, IFPI/UFPI, alfabetização, cursos, ensino\n   • \"cultura-religiao\" — Semana Santa, procissões, festas religiosas, patrimônio histórico, arte, shows\n   • \"esportes\" — campeonatos, atletas, futebol, competições\n   • \"economia-agro\" — agricultura, pecuária, piscicultura, comércio, produtores, ranking econômico, desenvolvimento regional\n   • \"infraestrutura\" — obras, pavimentação, saneamento (Águas do Piauí), esgoto, energia, iluminação, estradas\n   • \"opiniao\" — artigos assinados, análises, reflexões, editorial, crônicas\n   REGRA: escolha pelo TEMA DOMINANTE. Se for sobre Câmara aprovando projeto de agricultura familiar → politica (ato legislativo dominante). Se for sobre agricultor recebendo alevinos → economia-agro (produtor dominante).\n6. TAGS: 4-6 palavras-chave lowercase, separadas por relevância ao tema\n\nREGRAS INVIOLÁVEIS:\n- FIDELIDADE À FONTE: a matéria deve conter APENAS fatos presentes na fonte, reescritos com palavras próprias. Não invente conteúdo novo.\n- PROIBIDO inventar falas, citações, declarações, entrevistas, reações, opiniões, números, datas, nomes, cargos ou locais.\n- Só use fala entre aspas ou <em> se a frase aparecer LITERALMENTE na fonte. Nunca transforme um fato em declaração textual de alguém.\n- Não alongue o texto com contexto, histórico ou desdobramentos ausentes na fonte. Fidelidade vale mais que tamanho.\n- Linguagem clara, objetiva, sem sensacionalismo.\n\nRESPONDA APENAS JSON:\n{\n  \"chapeu\": \"PALAVRA\",\n  \"title\": \"...\",\n  \"subtitle\": \"...\",\n  \"bodyHtml\": \"<p>...</p><p>...</p>...\",\n  \"categorySlug\": \"uma-das-10-slugs-acima\",\n  \"tags\": [\"tag1\",\"tag2\",\"tag3\",\"tag4\"]\n}\n\nFONTE:\nTítulo: ${$json.title}\nDescrição: ${$json.description}\nURL: ${$json.url}\nCorpo:\n${$json.body}`\n  }]\n}) }}","options":{}},"id":"node-openai","name":"OpenAI - Gerar Matéria","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1088,352]},{"parameters":{"jsCode":"const raw = $input.first().json?.choices?.[0]?.message?.content || '';\nconst ogImage = $('Extrair Dados').first().json.ogImage || '';\nconst sourceUrl = $('Extrair URL (publicar)').first().json.url;\n\n// Confirma o dedup: a materia foi gerada de fato, entao vale a trava de 24h (ok:true).\ntry {\n  const _sd = $getWorkflowStaticData('global');\n  const _lista = Array.isArray(_sd.processedUrls) ? _sd.processedUrls : [];\n  const _outras = _lista.filter(e => e && e.url !== sourceUrl);\n  _outras.push({ url: sourceUrl, ts: Date.now(), ok: true });\n  _sd.processedUrls = _outras.slice(-200);\n} catch (e) {}\n\nlet parsed;\ntry {\n  const jsonMatch = raw.match(/\\{[\\s\\S]*\\}/);\n  parsed = JSON.parse(jsonMatch ? jsonMatch[0] : raw);\n} catch(e) {\n  throw new Error('Falha ao parsear JSON do OpenAI: ' + raw.slice(0, 200));\n}\n\nconst categoryNames = {\n  'cidades': 'Cidades', 'politica': 'Política', 'seguranca': 'Segurança',\n  'saude': 'Saúde', 'educacao': 'Educação', 'cultura-religiao': 'Cultura & Religião',\n  'esportes': 'Esportes', 'economia-agro': 'Economia & Agro',\n  'infraestrutura': 'Infraestrutura', 'opiniao': 'Opinião'\n};\n\nconst categorySlug = parsed.categorySlug || 'cidades';\nconst categoryName = categoryNames[categorySlug] || 'Cidades';\n\nreturn [{ json: {\n  title: parsed.title,\n  subtitle: parsed.subtitle,\n  chapeu: parsed.chapeu,\n  bodyHtml: parsed.bodyHtml,\n  categorySlug,\n  categoryName,\n  tags: parsed.tags || [],\n  coverUrl: ogImage,\n  coverAlt: parsed.title,\n  sourceUrl,\n  authorName: 'Redação O Oeirense',\n  instagramDispatch: true,\n  whatsappDispatch: true\n}}];"},"id":"node-parse","name":"Parsear Matéria","type":"n8n-nodes-base.code","typeVersion":2,"position":[1312,352]},{"parameters":{"method":"POST","url":"https://ooeirense.com.br/api/articles/create","sendHeaders":true,"headerParameters":{"parameters":[{"name":"x-api-key","value":"ooeirense-n8n-2026-secret"},{"name":"Content-Type","value":"application/json"}]},"sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify($json) }}"},"id":"node-create","name":"Criar Artigo no Portal","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1520,352],"onError":"continueRegularOutput"},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify((()=>{\n  const j = $json;\n  const title = ($(\"Parsear Matéria\").first().json.title) || \"\";\n  let url = j.url || \"\";\n  let duplicate = false;\n  if (j.error && typeof j.error.message === \"string\" && j.error.message.indexOf(\"409\") !== -1) {\n    duplicate = true;\n    const m = j.error.message.match(/\"url\":\"([^\"]+)\"/);\n    if (m) url = m[1];\n  }\n  const text = duplicate\n    ? \"⚠️ Esta matéria já estava publicada\\n\\n🔗 \" + url\n    : \"✅ Matéria publicada!\\n\\n📰 <b>\" + title + \"</b>\\n\\n🔗 \" + url + \"\";\n  return { chat_id: \"7769073261\", text, parse_mode: \"HTML\" };\n})()) }}","options":{}},"id":"node-confirm","name":"Telegram - Confirmação","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1744,304]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ chat_id: \"7769073261\", text: \"❌ <b>Não foi possível publicar</b>\\n\\n\" + ($json.error && $json.error.message ? $json.error.message : \"Erro desconhecido\") + \"\\n\\n<i>A matéria não foi criada porque a imagem não pôde ser extraída.</i>\", parse_mode: \"HTML\" }) }}","options":{}},"id":"node-error","name":"Telegram - Erro","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1744,464]},{"parameters":{"path":"dispatch-publicar","httpMethod":"POST","responseMode":"onReceived","options":{}},"id":"wh-dispatch-publicar","name":"Webhook Dispatcher","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[0,304],"webhookId":"wh-dispatch-publicar"}],"connections":{"Extrair URL (publicar)":{"main":[[{"node":"Telegram - Processando","type":"main","index":0}]]},"Jina Reader":{"main":[[{"node":"Extrair Dados","type":"main","index":0}]]},"Extrair Dados":{"main":[[{"node":"OpenAI - Gerar Matéria","type":"main","index":0}],[{"node":"Telegram - Erro","type":"main","index":0}]]},"OpenAI - Gerar Matéria":{"main":[[{"node":"Parsear Matéria","type":"main","index":0}]]},"Parsear Matéria":{"main":[[{"node":"Criar Artigo no Portal","type":"main","index":0}]]},"Criar Artigo no Portal":{"main":[[{"node":"Telegram - Confirmação","type":"main","index":0}]]},"Telegram - Processando":{"main":[[{"node":"Jina Reader","type":"main","index":0}]]},"Telegram - Confirmação":{"main":[[]]},"Webhook Dispatcher":{"main":[[{"node":"Extrair URL (publicar)","type":"main","index":0}]]}},"settings":{"executionOrder":"v1","callerPolicy":"workflowsFromSameOwner","availableInMCP":false,"binaryMode":"separate"},"staticData":{"node:Poll Telegram (Matérias)":{"recurrenceRules":[]},"global":{"publishLastUpdateId":183722664,"processedUrls":[{"url":"https://www.gp1.com.br/eleicoes-2026/noticia/2026/8/20/tre-pi-define-tempo-e-ordem-de-propagandas-eleitorais-para-coligacoes-e-federacoes-confira-629590.html","ts":1787255299419,"ok":true},{"url":"https://g1.globo.com/pi/piaui/noticia/2026/08/20/grupo-clonava-whatsapp-invadia-contas-prejuizo.ghtml","ts":1787255664614,"ok":true},{"url":"https://portalintegracao.com.br/acao-leva-alimentos-da-agricultura-familiar-a-mais-de-250-familias-na-zona-rural-de-oeiras/","ts":1787325601687,"ok":false}]}},"meta":null,"pinData":{},"versionId":"0e5c6a42-ed2a-4c53-84af-143b692d4a10","activeVersionId":"0e5c6a42-ed2a-4c53-84af-143b692d4a10","versionCounter":1444,"triggerCount":1,"shared":[{"updatedAt":"2026-04-17T02:40:26.744Z","createdAt":"2026-04-17T02:40:26.744Z","role":"workflow:owner","workflowId":"uvnMZfrW0Giut5lM","projectId":"lcNbFUrydUsxxeGp","project":{"updatedAt":"2026-05-06T16:07:04.097Z","createdAt":"2026-04-10T15:23:11.499Z","id":"lcNbFUrydUsxxeGp","name":"Ultimo Campos <ultimocampos@gmail.com>","type":"personal","icon":null,"description":null,"creatorId":"7fdfc85d-b5cb-411b-92ad-50cd2ccbad11"}}],"tags":[],"activeVersion":{"updatedAt":"2026-07-03T14:27:47.283Z","createdAt":"2026-07-03T14:27:47.283Z","versionId":"0e5c6a42-ed2a-4c53-84af-143b692d4a10","workflowId":"uvnMZfrW0Giut5lM","nodes":[{"parameters":{"jsCode":"const response = $input.first().json.body;\nconst results = response.result || [];\nif (!results.length) { return []; }\n\nconst msg = results[0];\n// SEMPRE avancar offset local, mesmo quando nao for para este workflow\n// (para nao bloquear o Instagram workflow que le o mesmo bot)\nconst staticData = $getWorkflowStaticData('global');\nstaticData.publishLastUpdateId = msg.update_id + 1;\n\nif (msg.message?.from?.is_bot) { return []; }\nconst chatId = String(msg.message?.chat?.id || '');\nif (chatId !== '7769073261') { return []; }\n\nconst text = (msg.message?.text || '').trim();\nconst match = text.match(/^(?:publicar|publica[cç][aã]o)\\s+(https?:\\/\\/\\S+)/i);\nif (!match) { return []; }\n\nconst url = match[1];\n\n// ── Trava de deduplicação por URL ──\n// Mantém lista das ultimas URLs processadas. Se URL repetida em <24h, bloqueia.\n// Persiste como array top-level (subchaves nao persistem no task runner v2.15).\nconst now = Date.now();\nconst TTL_MS = 24 * 60 * 60 * 1000; // 24h — vale para publicacao CONFIRMADA (ok:true)\nconst TRY_TTL_MS = 10 * 60 * 1000;  // 10min — tentativa em curso (anti-duplicata concorrente)\nconst prev = Array.isArray(staticData.processedUrls) ? staticData.processedUrls : [];\nconst fresh = prev.filter(e => e && e.ts && (now - e.ts) < TTL_MS);\n\n// Bloqueia se JA FOI PUBLICADA (ok:true) em <24h, ou se ha tentativa em curso (<10min).\n// Tentativa que falhou libera sozinha em 10min — antes ficava travada 24h em silencio.\nif (fresh.some(e => e.url === url && (e.ok === true || (now - e.ts) < TRY_TTL_MS))) {\n  staticData.processedUrls = fresh;\n  return [];\n}\n\n// marca a TENTATIVA (ok:false). Quem confirma com ok:true e o no 'Parsear Materia'.\nconst semEsta = fresh.filter(e => e.url !== url);\nsemEsta.push({ url, ts: now, ok: false });\nstaticData.processedUrls = semEsta.slice(-200);\n\nreturn [{ json: {\n  url,\n  messageId: msg.message.message_id,\n  chatId,\n  updateId: msg.update_id\n} }];\n"},"id":"node-extract","name":"Extrair URL (publicar)","type":"n8n-nodes-base.code","typeVersion":2,"position":[432,304]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ chat_id: \"7769073261\", text: \"⏳ Gerando matéria...\\n\\n🔗 \" + $json.url, parse_mode: \"HTML\" }) }}","options":{}},"id":"node-processing","name":"Telegram - Processando","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[624,96]},{"parameters":{"url":"={{ 'https://r.jina.ai/' + $('Extrair URL (publicar)').first().json.url }}","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Accept","value":"application/json"},{"name":"User-Agent","value":"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36"},{"name":"X-Timeout","value":"45"}]},"options":{"timeout":60000,"retry":{"tryTimes":2,"waitBetweenTries":3000}}},"id":"node-jina","name":"Jina Reader","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[640,352],"onError":"continueRegularOutput"},{"parameters":{"jsCode":"// Jina Reader retorna: { data: { title, description, url, content, metadata } }\nconst response = $('Jina Reader').first().json;\nconst jinaData = response.data || response;\nconst meta = jinaData.metadata || {};\nconst url = $('Extrair URL (publicar)').first().json.url;\n\n// Titulo + descricao\nlet title = (meta['og:title'] || jinaData.title || '').replace(/\\s*[-|].*$/, '').trim();\nlet description = (meta['og:description'] || meta.description || jinaData.description || '').trim();\n\n// Imagem — priorizar og:image do metadata\nlet imageUrl = (meta['og:image'] || meta['twitter:image'] || '').trim();\nlet content = jinaData.content || '';\n\n// Detectar bloqueio do Jina (403 / conteudo vazio / pagina de erro)\nconst jinaBlocked =\n  /403 forbidden|access denied|don't have permission/i.test(title) ||\n  /403 forbidden|don't have permission/i.test(content) ||\n  content.trim().length < 300 ||\n  !imageUrl;\n\nif (jinaBlocked) {\n  try {\n    const html = await this.helpers.httpRequest({\n      method: 'GET',\n      url: url,\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n        'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8',\n      },\n      returnFullResponse: false,\n    });\n    const extractMeta = (html, key) => {\n      let rx = new RegExp('<meta[^>]+(?:property|name)\\\\s*=\\\\s*[\"\\'](' + key + ')[\"\\'][^>]*content\\\\s*=\\\\s*[\"\\']([^\"\\']+)[\"\\']', 'i');\n      let m = html.match(rx);\n      if (m) return m[2];\n      rx = new RegExp('<meta[^>]+content\\\\s*=\\\\s*[\"\\']([^\"\\']+)[\"\\'][^>]*(?:property|name)\\\\s*=\\\\s*[\"\\'](' + key + ')[\"\\']', 'i');\n      m = html.match(rx);\n      return m ? m[1] : '';\n    };\n    const newTitle = (extractMeta(html, 'og:title') || extractMeta(html, 'twitter:title') || '').replace(/\\s*[-|].*$/, '').trim();\n    const newDesc = extractMeta(html, 'og:description') || extractMeta(html, 'twitter:description') || extractMeta(html, 'description') || '';\n    const newImg = extractMeta(html, 'og:image') || extractMeta(html, 'twitter:image') || '';\n    if (newTitle) title = newTitle;\n    if (newDesc) description = newDesc;\n    if (newImg) imageUrl = newImg;\n    const paras = [];\n    const pRx = /<p[^>]*>([\\s\\S]*?)<\\/p>/gi;\n    let mm;\n    while ((mm = pRx.exec(html)) !== null) {\n      const txt = mm[1].replace(/<[^>]+>/g, '').replace(/&nbsp;/g,' ').replace(/&amp;/g,'&').replace(/&quot;/g,'\"').replace(/&#39;/g,\"'\").trim();\n      if (txt.length > 40) paras.push(txt);\n    }\n    const fallbackContent = paras.join('\\n\\n');\n    if (fallbackContent.length > content.length) content = fallbackContent;\n  } catch (e) {\n    // se fallback falhar, deixa campos como estao\n  }\n}\n\n// Decodificar entidades HTML na URL (ex: &amp; -> &)\nimageUrl = imageUrl.replace(/&amp;/g, '&');\n\n// Se og:image usa CDN Cloudflare (cdn-cgi/image/...), extrair URL original\nif (imageUrl.includes('cdn-cgi/image/')) {\n  const originalMatch = imageUrl.match(/\\/https?:\\/\\/.+$/);\n  if (originalMatch) imageUrl = originalMatch[0].substring(1);\n}\n\n// ── Remover marca d'agua dos portais que servem og:image watermarked ──\n// gp1.com.br: /media/watermarks/ → /media/image_bank/\n// lupa1.com.br: /storage/XXX/conversions/*-social.jpg → /storage/XXX/*.{jpeg,jpg,png}\nconst isGp1Watermark = /gp1\\.com\\.br\\/media\\/watermarks\\//i.test(imageUrl);\nconst isLupa1Watermark = /lupa1\\.com\\.br\\/storage\\/\\d+\\/conversions\\//i.test(imageUrl);\n\nif (isGp1Watermark || isLupa1Watermark) {\n  try {\n    const html = await this.helpers.httpRequest({\n      method: 'GET',\n      url: url,\n      headers: {\n        'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',\n        'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',\n        'Accept-Language': 'pt-BR,pt;q=0.9,en;q=0.8',\n      },\n      returnFullResponse: false,\n    });\n\n    if (isGp1Watermark) {\n      // Pega imagens em /media/image_bank/, exclui thumbnails (.90x58, .60x60 etc)\n      const rx = /https:\\/\\/[^\"'\\s)]*gp1\\.com\\.br\\/media\\/image_bank\\/[^\"'\\s)]+\\.(?:jpg|jpeg|png|webp)[^\"'\\s)]*/gi;\n      const candidates = (html.match(rx) || []).filter(u => !/\\.\\d{2,3}x\\d{2,3}_/.test(u));\n      const large = candidates.find(u => /1200x|800x|upscale/i.test(u));\n      if (large || candidates[0]) imageUrl = large || candidates[0];\n    } else if (isLupa1Watermark) {\n      const m = imageUrl.match(/\\/storage\\/(\\d+)\\/conversions\\//);\n      if (m) {\n        const storageId = m[1];\n        const rx = new RegExp('https://[^\"\\'\\\\s)]*lupa1\\\\.com\\\\.br/storage/' + storageId + '/[^\"\\'\\\\s)]+\\\\.(?:jpg|jpeg|png|webp)[^\"\\'\\\\s)]*', 'gi');\n        const candidates = (html.match(rx) || []).filter(u => !/\\/conversions\\//.test(u));\n        if (candidates[0]) imageUrl = candidates[0];\n      }\n    }\n  } catch (e) {\n    // Se fetch falhar, mantem og:image original (com marca)\n  }\n}\n\n// Fallback: primeira imagem real no markdown (pular logos, icones, social)\nif (!imageUrl) {\n  const skipPatterns = /logo|social-40|icon|favicon|brand|avatar|banner_site|sprite|sem-foto/i;\n  const imgRegex = /!\\[.*?\\]\\((https?:\\/\\/[^\\s)]+\\.(jpg|jpeg|png|webp)[^\\s)]*)\\)/gi;\n  let match;\n  while ((match = imgRegex.exec(content)) !== null) {\n    if (!skipPatterns.test(match[1])) { imageUrl = match[1]; break; }\n  }\n}\n\n// ABORTAR se nao tiver imagem\nif (!imageUrl) {\n  throw new Error('SEM_IMAGEM: nao foi possivel extrair imagem da materia. URL: ' + url);\n}\n\n// Corpo\nconst body = content\n  .replace(/!\\[.*?\\]\\(.*?\\)/g, '')\n  .replace(/\\[([^\\]]+)\\]\\([^\\)]+\\)/g, '$1')\n  .replace(/#{1,6}\\s/g, '')\n  .replace(/[*_]{1,3}/g, '')\n  .replace(/\\n{3,}/g, '\\n\\n')\n  .trim()\n  .substring(0, 3000) || description;\n\nreturn [{ json: { url, title, description, ogImage: imageUrl, body } }];\n"},"id":"node-extract-data","name":"Extrair Dados","type":"n8n-nodes-base.code","typeVersion":2,"position":[880,576],"onError":"continueErrorOutput"},{"parameters":{"method":"POST","url":"https://api.openai.com/v1/chat/completions","sendHeaders":true,"headerParameters":{"parameters":[{"name":"Authorization","value":"Bearer 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"},{"name":"Content-Type","value":"application/json"}]},"sendBody":true,"specifyBody":"json","jsonBody":"={{ ({\n  \"model\": \"gpt-4o\",\n  \"max_tokens\": 4500,\n  \"temperature\": 0.2,\n  \"messages\": [{\n    \"role\": \"user\",\n    \"content\": `Você é jornalista do portal O Oeirense (Oeiras, Piauí). REESCREVA a matéria-fonte abaixo com palavras próprias, FIEL AOS FATOS, sem inventar nada. Produza JSON (sem markdown, sem \\`\\`\\`).\n\nESTRUTURA:\n1. CHAPÉU: 1-2 palavras MAIÚSCULAS que rotulem o tema específico (ex: SEMANA SANTA, ELEIÇÕES 2026, SAÚDE PÚBLICA, PROTEÇÃO ANIMAL)\n2. TÍTULO: até 100 caracteres, direto, sem ponto final\n3. SUBTÍTULO: 2 frases, 200-280 caracteres, com dados concretos\n4. CORPO (bodyHtml): parágrafos em HTML usando <p> (e <em> só para citação que exista LITERALMENTE na fonte). Use a QUANTIDADE de parágrafos que a fonte sustentar — normalmente 3 a 6. NÃO force tamanho: se a fonte é curta, a matéria é curta.\n   - Comece pelo lide (quem, o quê, quando, onde) e desenvolva apenas contexto e dados QUE ESTEJAM NA FONTE.\n   - NÃO crie parágrafo de \"reações\", \"repercussão\" ou \"declarações\" se a fonte não relatar isso de fato.\n   - Impacto local: só escreva sobre efeito em Oeiras ou na região SE a fonte tratar disso. Se o fato não tem ligação com Oeiras, relate-o como ele é, sem inventar conexão local.\n5. CATEGORIA (categorySlug) — ESCOLHA UMA baseado no TEMA CENTRAL da matéria:\n   • \"cidades\" — notícias urbanas/municipais gerais, trânsito, comércio local, cotidiano, ONGs, proteção animal, acidentes que não são crime\n   • \"politica\" — Câmara, prefeitura, eleições, vereadores, deputados, governo, projetos de lei, ALEPI\n   • \"seguranca\" — Polícia Civil/Militar, prisões, apreensões, investigações criminais, violência urbana\n   • \"saude\" — SUS, UBS, hospitais, campanhas, secretaria de saúde, medicina (exceto graduação)\n   • \"educacao\" — escolas, universidades, IFPI/UFPI, alfabetização, cursos, ensino\n   • \"cultura-religiao\" — Semana Santa, procissões, festas religiosas, patrimônio histórico, arte, shows\n   • \"esportes\" — campeonatos, atletas, futebol, competições\n   • \"economia-agro\" — agricultura, pecuária, piscicultura, comércio, produtores, ranking econômico, desenvolvimento regional\n   • \"infraestrutura\" — obras, pavimentação, saneamento (Águas do Piauí), esgoto, energia, iluminação, estradas\n   • \"opiniao\" — artigos assinados, análises, reflexões, editorial, crônicas\n   REGRA: escolha pelo TEMA DOMINANTE. Se for sobre Câmara aprovando projeto de agricultura familiar → politica (ato legislativo dominante). Se for sobre agricultor recebendo alevinos → economia-agro (produtor dominante).\n6. TAGS: 4-6 palavras-chave lowercase, separadas por relevância ao tema\n\nREGRAS INVIOLÁVEIS:\n- FIDELIDADE À FONTE: a matéria deve conter APENAS fatos presentes na fonte, reescritos com palavras próprias. Não invente conteúdo novo.\n- PROIBIDO inventar falas, citações, declarações, entrevistas, reações, opiniões, números, datas, nomes, cargos ou locais.\n- Só use fala entre aspas ou <em> se a frase aparecer LITERALMENTE na fonte. Nunca transforme um fato em declaração textual de alguém.\n- Não alongue o texto com contexto, histórico ou desdobramentos ausentes na fonte. Fidelidade vale mais que tamanho.\n- Linguagem clara, objetiva, sem sensacionalismo.\n\nRESPONDA APENAS JSON:\n{\n  \"chapeu\": \"PALAVRA\",\n  \"title\": \"...\",\n  \"subtitle\": \"...\",\n  \"bodyHtml\": \"<p>...</p><p>...</p>...\",\n  \"categorySlug\": \"uma-das-10-slugs-acima\",\n  \"tags\": [\"tag1\",\"tag2\",\"tag3\",\"tag4\"]\n}\n\nFONTE:\nTítulo: ${$json.title}\nDescrição: ${$json.description}\nURL: ${$json.url}\nCorpo:\n${$json.body}`\n  }]\n}) }}","options":{}},"id":"node-openai","name":"OpenAI - Gerar Matéria","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1088,352]},{"parameters":{"jsCode":"const raw = $input.first().json?.choices?.[0]?.message?.content || '';\nconst ogImage = $('Extrair Dados').first().json.ogImage || '';\nconst sourceUrl = $('Extrair URL (publicar)').first().json.url;\n\n// Confirma o dedup: a materia foi gerada de fato, entao vale a trava de 24h (ok:true).\ntry {\n  const _sd = $getWorkflowStaticData('global');\n  const _lista = Array.isArray(_sd.processedUrls) ? _sd.processedUrls : [];\n  const _outras = _lista.filter(e => e && e.url !== sourceUrl);\n  _outras.push({ url: sourceUrl, ts: Date.now(), ok: true });\n  _sd.processedUrls = _outras.slice(-200);\n} catch (e) {}\n\nlet parsed;\ntry {\n  const jsonMatch = raw.match(/\\{[\\s\\S]*\\}/);\n  parsed = JSON.parse(jsonMatch ? jsonMatch[0] : raw);\n} catch(e) {\n  throw new Error('Falha ao parsear JSON do OpenAI: ' + raw.slice(0, 200));\n}\n\nconst categoryNames = {\n  'cidades': 'Cidades', 'politica': 'Política', 'seguranca': 'Segurança',\n  'saude': 'Saúde', 'educacao': 'Educação', 'cultura-religiao': 'Cultura & Religião',\n  'esportes': 'Esportes', 'economia-agro': 'Economia & Agro',\n  'infraestrutura': 'Infraestrutura', 'opiniao': 'Opinião'\n};\n\nconst categorySlug = parsed.categorySlug || 'cidades';\nconst categoryName = categoryNames[categorySlug] || 'Cidades';\n\nreturn [{ json: {\n  title: parsed.title,\n  subtitle: parsed.subtitle,\n  chapeu: parsed.chapeu,\n  bodyHtml: parsed.bodyHtml,\n  categorySlug,\n  categoryName,\n  tags: parsed.tags || [],\n  coverUrl: ogImage,\n  coverAlt: parsed.title,\n  sourceUrl,\n  authorName: 'Redação O Oeirense',\n  instagramDispatch: true,\n  whatsappDispatch: true\n}}];"},"id":"node-parse","name":"Parsear Matéria","type":"n8n-nodes-base.code","typeVersion":2,"position":[1312,352]},{"parameters":{"method":"POST","url":"https://ooeirense.com.br/api/articles/create","sendHeaders":true,"headerParameters":{"parameters":[{"name":"x-api-key","value":"ooeirense-n8n-2026-secret"},{"name":"Content-Type","value":"application/json"}]},"sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify($json) }}"},"id":"node-create","name":"Criar Artigo no Portal","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1520,352],"onError":"continueRegularOutput"},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify((()=>{\n  const j = $json;\n  const title = ($(\"Parsear Matéria\").first().json.title) || \"\";\n  let url = j.url || \"\";\n  let duplicate = false;\n  if (j.error && typeof j.error.message === \"string\" && j.error.message.indexOf(\"409\") !== -1) {\n    duplicate = true;\n    const m = j.error.message.match(/\"url\":\"([^\"]+)\"/);\n    if (m) url = m[1];\n  }\n  const text = duplicate\n    ? \"⚠️ Esta matéria já estava publicada\\n\\n🔗 \" + url\n    : \"✅ Matéria publicada!\\n\\n📰 <b>\" + title + \"</b>\\n\\n🔗 \" + url + \"\";\n  return { chat_id: \"7769073261\", text, parse_mode: \"HTML\" };\n})()) }}","options":{}},"id":"node-confirm","name":"Telegram - Confirmação","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1744,304]},{"parameters":{"method":"POST","url":"https://api.telegram.org/bot8684225036:AAHPUgR6b1y7oN7y5q1FMQ7vcoi3Io9nysE/sendMessage","sendBody":true,"specifyBody":"json","jsonBody":"={{ JSON.stringify({ chat_id: \"7769073261\", text: \"❌ <b>Não foi possível publicar</b>\\n\\n\" + ($json.error && $json.error.message ? $json.error.message : \"Erro desconhecido\") + \"\\n\\n<i>A matéria não foi criada porque a imagem não pôde ser extraída.</i>\", parse_mode: \"HTML\" }) }}","options":{}},"id":"node-error","name":"Telegram - Erro","type":"n8n-nodes-base.httpRequest","typeVersion":4.2,"position":[1744,464]},{"parameters":{"path":"dispatch-publicar","httpMethod":"POST","responseMode":"onReceived","options":{}},"id":"wh-dispatch-publicar","name":"Webhook Dispatcher","type":"n8n-nodes-base.webhook","typeVersion":2,"position":[0,304],"webhookId":"wh-dispatch-publicar"}],"connections":{"Extrair URL (publicar)":{"main":[[{"node":"Telegram - Processando","type":"main","index":0}]]},"Jina Reader":{"main":[[{"node":"Extrair Dados","type":"main","index":0}]]},"Extrair Dados":{"main":[[{"node":"OpenAI - Gerar Matéria","type":"main","index":0}],[{"node":"Telegram - Erro","type":"main","index":0}]]},"OpenAI - Gerar Matéria":{"main":[[{"node":"Parsear Matéria","type":"main","index":0}]]},"Parsear Matéria":{"main":[[{"node":"Criar Artigo no Portal","type":"main","index":0}]]},"Criar Artigo no Portal":{"main":[[{"node":"Telegram - Confirmação","type":"main","index":0}]]},"Telegram - Processando":{"main":[[{"node":"Jina Reader","type":"main","index":0}]]},"Telegram - Confirmação":{"main":[[]]},"Webhook Dispatcher":{"main":[[{"node":"Extrair URL (publicar)","type":"main","index":0}]]}},"authors":"Ultimo Campos","name":null,"description":null,"autosaved":false,"workflowPublishHistory":[{"createdAt":"2026-07-03T14:27:47.403Z","id":513,"workflowId":"uvnMZfrW0Giut5lM","versionId":"0e5c6a42-ed2a-4c53-84af-143b692d4a10","event":"activated","userId":"7fdfc85d-b5cb-411b-92ad-50cd2ccbad11"},{"createdAt":"2026-07-06T21:19:38.550Z","id":529,"workflowId":"uvnMZfrW0Giut5lM","versionId":"0e5c6a42-ed2a-4c53-84af-143b692d4a10","event":"activated","userId":"7fdfc85d-b5cb-411b-92ad-50cd2ccbad11"},{"createdAt":"2026-07-06T21:20:23.145Z","id":531,"workflowId":"uvnMZfrW0Giut5lM","versionId":"0e5c6a42-ed2a-4c53-84af-143b692d4a10","event":"activated","userId":"7fdfc85d-b5cb-411b-92ad-50cd2ccbad11"},{"createdAt":"2026-07-03T14:27:47.384Z","id":512,"workflowId":"uvnMZfrW0Giut5lM","versionId":"0e5c6a42-ed2a-4c53-84af-143b692d4a10","event":"deactivated","userId":"7fdfc85d-b5cb-411b-92ad-50cd2ccbad11"},{"createdAt":"2026-07-06T21:19:38.429Z","id":528,"workflowId":"uvnMZfrW0Giut5lM","versionId":"0e5c6a42-ed2a-4c53-84af-143b692d4a10","event":"deactivated","userId":"7fdfc85d-b5cb-411b-92ad-50cd2ccbad11"},{"createdAt":"2026-07-06T21:20:23.018Z","id":530,"workflowId":"uvnMZfrW0Giut5lM","versionId":"0e5c6a42-ed2a-4c53-84af-143b692d4a10","event":"deactivated","userId":"7fdfc85d-b5cb-411b-92ad-50cd2ccbad11"}]}}