This workflow follows the Datatable → HTTP Request recipe pattern — see all workflows that pair these two integrations.
The workflow JSON
Copy or download the full n8n JSON below. Paste it into a new n8n workflow, add your credentials, activate. Full import guide →
{
"name": "Postagem Redes \u2014 Portal: A\u00e7\u00f5es",
"description": null,
"isArchived": false,
"nodes": [
{
"parameters": {
"content": "## Produ\u00e7\u00e3o controlada\n\n**Uma fila, quatro redes, tr\u00eas provedores de IA.**\n\n- A\u00e7\u00f5es do portal sempre exigem revis\u00e3o humana.\n- A fila s\u00f3 dispara quando SOCIAL_PUBLISH_ENABLED=true.\n- Cada entrega \u00e9 reservada antes da chamada externa; falhas recebem at\u00e9 3 tentativas com espera exponencial.\n- Credenciais ficam somente no cofre do n8n, nunca neste workflow.",
"height": 270,
"width": 550,
"color": 6
},
"name": "Orquestrador de publica\u00e7\u00e3o",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-656,
-464
]
},
{
"parameters": {
"content": "## 01 \u00b7 Portal e IA\n\nA sugest\u00e3o nunca publica conte\u00fado.\n\nOpenAI \u00e9 o provedor prim\u00e1rio; Gemini e Ollama s\u00f3 entram quando a vari\u00e1vel de fallback correspondente estiver habilitada.",
"height": 180,
"width": 510,
"color": 4
},
"name": "01 \u00b7 Portal e IA",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
192,
-464
]
},
{
"parameters": {
"content": "## 02 \u00b7 Fila, reserva e valida\u00e7\u00e3o\n\nCada destino recebe um `dispatchId` antes da chamada externa. A trava global e as vari\u00e1veis por rede s\u00e3o verificadas antes de publicar.",
"height": 185,
"width": 560,
"color": 5
},
"name": "02 \u00b7 Fila protegida",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
-656,
352
]
},
{
"parameters": {
"content": "## 03 \u00b7 APIs oficiais\n\nCada faixa segue a regra pr\u00f3pria da plataforma. HTTP Request fica apenas onde n\u00e3o h\u00e1 opera\u00e7\u00e3o nativa completa no n8n.",
"height": 165,
"width": 500,
"color": 2
},
"name": "03 \u00b7 Publicadores por rede",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
1552,
128
]
},
{
"parameters": {
"content": "## 04 \u00b7 Resultado por entrega\n\nSucesso e falha s\u00e3o gravados no estado e no Ledger nativo. Falhas t\u00eam at\u00e9 tr\u00eas tentativas com espera exponencial.",
"height": 170,
"width": 520,
"color": 3
},
"name": "04 \u00b7 Resultado e recupera\u00e7\u00e3o",
"type": "n8n-nodes-base.stickyNote",
"typeVersion": 1,
"position": [
2704,
512
]
},
{
"parameters": {
"httpMethod": "POST",
"path": "postagem-redes-api",
"responseMode": "responseNode",
"options": {}
},
"name": "Webhook do portal",
"type": "n8n-nodes-base.webhook",
"typeVersion": 2.1,
"position": [
-624,
0
]
},
{
"parameters": {
"jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction scan(){\n fs.mkdirSync(INPUT,{recursive:true});\n const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n const items=[];\n for(const folder of folders){\n const dir=path.join(INPUT,folder);\n const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n const id=slug(folder); const old=state.records[id]||{};\n const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n }\n return {state,items};\n}\n\nconst item=$input.first(); const body=item.json.body||item.json||{}; const action=safeText(body.action,30); const operator=safeText(body.operator,80); if(!operator) throw new Error('Informe o nome do respons\u00e1vel.');\nconst now=new Date().toISOString(); const {state,items}=scan();\nif(action==='generate'){\n const id=safeText(body.contentId,130); const content=items.find(x=>x.id===id); if(!content) throw new Error('Conte\u00fado n\u00e3o encontrado. Atualize a p\u00e1gina.');\n if(String($env.SOCIAL_AI_ENABLED||'').toLowerCase()!=='true') return [{json:{ok:false,route:'respond',message:'A IA est\u00e1 desativada. Configure SOCIAL_AI_ENABLED=true e a credencial OpenAI no workflow Portal: A\u00e7\u00f5es.'}}];\n const brief=safeText(body.brief??content.brief,1600); const prompt=['Voc\u00ea \u00e9 redator de marketing B2B industrial em portugu\u00eas do Brasil.','Crie apenas uma sugest\u00e3o para revis\u00e3o humana; n\u00e3o invente especifica\u00e7\u00f5es, certifica\u00e7\u00f5es, clientes, pre\u00e7os ou resultados.','Retorne JSON v\u00e1lido sem markdown com as chaves: baseCaption (string), variants (objeto com instagram, facebook, linkedin e xThread; xThread \u00e9 array de 1 a 4 strings), hashtags (array at\u00e9 12), reviewNotes (array at\u00e9 5).','Adapte tom e tamanho \u00e0 rede. Inclua CTA discreto quando fizer sentido.','T\u00edtulo interno: '+content.title,'Brief: '+brief,'Legenda atual: '+content.caption,'Quantidade de slides: '+content.slides.length].join('\\n');\n return [{json:{route:'ai',contentId:id,operator,brief,aiPrompt:prompt}}];\n}\nif(action==='upload'){\n const title=safeText(body.title,120); const caption=safeText(body.caption,5000); if(!title||!caption) throw new Error('T\u00edtulo e legenda s\u00e3o obrigat\u00f3rios.');\n let order=[]; try{order=JSON.parse(body.order||'[]')}catch{} const rank=new Map(order.map((key,index)=>[String(key),index]));\n const entries=Object.entries(item.binary||{}).filter(([,file])=>['image/png','image/jpeg','image/webp'].includes(String(file.mimeType||'').toLowerCase())); if(entries.length<1||entries.length>10) throw new Error('Envie de 1 a 10 imagens PNG, JPG ou WEBP.');\n entries.sort(([a],[b])=>(rank.get(a)??999)-(rank.get(b)??999)||a.localeCompare(b));\n const base='rapido-'+Date.now()+'-'+slug(title).slice(0,24); const dir=path.join(INPUT,base); fs.mkdirSync(dir,{recursive:false}); try{for(let index=0;index<entries.length;index++){const [binaryName,file]=entries[index];const ext=file.mimeType==='image/png'?'.png':file.mimeType==='image/webp'?'.webp':'.jpg';const buffer=await this.helpers.getBinaryDataBuffer(0,binaryName);fs.writeFileSync(path.join(dir,String(index+1).padStart(2,'0')+ext),buffer)}}catch(error){try{fs.rmSync(dir,{recursive:true,force:true})}catch{}throw new Error('N\u00e3o foi poss\u00edvel gravar as imagens enviadas. Tente novamente.')} fs.writeFileSync(path.join(dir,'Texto.txt'),caption,'utf8'); const id=slug(base); state.records[id]={title,caption,status:'pendente',networks:[],deliveries:{},updatedAt:now,audit:[{at:now,operator,action:'criou conte\u00fado r\u00e1pido',comment:entries.length+' slide(s) organizado(s) antes do envio.'}]}; saveState(state); return [{json:{ok:true,route:'respond',message:'Publica\u00e7\u00e3o criada e enviada para aprova\u00e7\u00e3o.',id}}];\n}\nconst id=safeText(body.contentId,130); const content=items.find(x=>x.id===id); if(!content) throw new Error('Conte\u00fado n\u00e3o encontrado. Atualize a p\u00e1gina.'); const previous=state.records[id]||{};\nif(action==='reorder'){\n let requested=[]; try{requested=JSON.parse(body.slides||'[]')}catch{} if(!Array.isArray(requested)||requested.length!==content.slides.length||new Set(requested).size!==content.slides.length||requested.some(file=>!content.slides.includes(file))) throw new Error('A nova ordem de slides \u00e9 inv\u00e1lida. Atualize a p\u00e1gina e tente novamente.');\n const dir=path.join(INPUT,content.folder); const token=Date.now()+'-'+Math.random().toString(36).slice(2,8); const moves=requested.map((file,index)=>({from:path.join(dir,file),temp:path.join(dir,'.reordenar-'+token+'-'+index+path.extname(file)),to:path.join(dir,String(index+1).padStart(2,'0')+path.extname(file))}));\n try{moves.forEach(move=>fs.renameSync(move.from,move.temp));moves.forEach(move=>fs.renameSync(move.temp,move.to))}catch(error){try{moves.forEach(move=>{if(fs.existsSync(move.temp))fs.renameSync(move.temp,move.from)})}catch{}throw new Error('N\u00e3o foi poss\u00edvel reorganizar os slides. Nenhuma decis\u00e3o foi salva.');}\n state.records[id]={...previous,title:content.title,caption:content.caption,brief:content.brief||'',status:content.status,networks:content.networks||[],scheduleAt:content.scheduleAt||'',updatedAt:now,audit:[...(previous.audit||[]),{at:now,operator,action:'reorganizou slides',comment:'Ordem do carrossel atualizada no portal.'}]}; saveState(state); return [{json:{ok:true,route:'respond',message:'Ordem do carrossel atualizada.'}}];\n}\nif(action!=='save') throw new Error('A\u00e7\u00e3o n\u00e3o reconhecida.'); const title=safeText(body.title,120); const caption=safeText(body.caption,5000); const brief=safeText(body.brief,1600); const status=safeText(body.status,30); if(!title) throw new Error('Informe o t\u00edtulo interno da publica\u00e7\u00e3o.'); if(!allowedStatuses.has(status)||['publicado','parcial','falhou'].includes(status)) throw new Error('Status inv\u00e1lido.'); if(!caption) throw new Error('A legenda n\u00e3o pode ficar vazia.'); if(status==='agendado'&&!safeText(body.scheduleAt,40)) throw new Error('Informe data e hora para o agendamento.'); const rawNetworks=body.networks??[]; let networks=[]; if(Array.isArray(rawNetworks)) networks=rawNetworks; else if(typeof rawNetworks==='string'){const normalized=rawNetworks.trim(); try{networks=JSON.parse(normalized)}catch{networks=normalized.split(',')}} else networks=[rawNetworks]; if(!Array.isArray(networks)) networks=[networks]; networks=[...new Set(networks.map(x=>safeText(x,20)).filter(x=>['instagram','facebook','linkedin','x'].includes(x)))]; if((status==='aprovado'||status==='agendado')&&!networks.length) throw new Error('Selecione pelo menos uma rede.'); const comment=safeText(body.comment,1000); const deliveries={}; for(const network of networks){const prior=previous.deliveries?.[network]||{}; deliveries[network]={status:(status==='aprovado'||status==='agendado')?'queued':'draft',attempts:0,queuedAt:(status==='aprovado'||status==='agendado')?now:'',lastRemoteId:'',lastPermalink:'',lastError:'',...((prior.status==='published'&&previous.caption===caption&&previous.title===title)?prior:{})};} state.records[id]={...previous,title,caption,brief,status,networks,deliveries,scheduleAt:status==='agendado'?safeText(body.scheduleAt,40):'',updatedAt:now,audit:[...(previous.audit||[]),{at:now,operator,action:status,comment,networks}]}; saveState(state); return [{json:{ok:true,route:'respond',message:status==='aprovado'?'Conte\u00fado aprovado e adicionado \u00e0 fila de homologa\u00e7\u00e3o.':status==='agendado'?'Conte\u00fado aprovado e agendado para a fila.':'Atualiza\u00e7\u00e3o salva com sucesso.'}}];"
},
"name": "Processar a\u00e7\u00e3o do portal",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-384,
0
]
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ $json.route }}",
"rightValue": "ai",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Gerar IA"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ $json.route }}",
"rightValue": "respond",
"operator": {
"type": "string",
"operation": "equals"
}
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Responder"
}
]
},
"options": {
"fallbackOutput": "none",
"ignoreCase": false
}
},
"name": "Roteador da a\u00e7\u00e3o",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.4,
"position": [
-128,
0
]
},
{
"parameters": {
"modelId": {
"mode": "id",
"value": "={{ $env.SOCIAL_AI_MODEL || \"gpt-5-mini\" }}"
},
"responses": {
"values": [
{
"content": "={{ $json.aiPrompt }}"
}
]
},
"builtInTools": {},
"options": {
"instructions": "Retorne somente JSON v\u00e1lido conforme o pedido; n\u00e3o use markdown.",
"maxTokens": 1800
}
},
"name": "OpenAI \u00b7 sugest\u00e3o prim\u00e1ria",
"type": "@n8n/n8n-nodes-langchain.openAi",
"typeVersion": 2.3,
"position": [
144,
-144
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ String($env.SOCIAL_AI_GEMINI_FALLBACK_ENABLED || \"\").toLowerCase() === \"true\" }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"name": "Fallback Gemini habilitado?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
400,
-80
]
},
{
"parameters": {
"modelId": {
"mode": "id",
"value": "={{ $env.SOCIAL_GEMINI_MODEL || \"gemini-3.5-flash\" }}"
},
"messages": {
"values": [
{
"content": "={{ $(\"Processar a\u00e7\u00e3o do portal\").item.json.aiPrompt }}"
}
]
},
"jsonOutput": true,
"builtInTools": {},
"options": {
"systemMessage": "Retorne somente JSON v\u00e1lido conforme o pedido; n\u00e3o use markdown.",
"maxOutputTokens": 1800
}
},
"name": "Gemini \u00b7 fallback",
"type": "@n8n/n8n-nodes-langchain.googleGemini",
"typeVersion": 1.2,
"position": [
640,
-160
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ String($env.SOCIAL_AI_OLLAMA_FALLBACK_ENABLED || \"\").toLowerCase() === \"true\" }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"name": "Fallback Ollama habilitado?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
912,
-64
]
},
{
"parameters": {
"modelId": {
"mode": "id",
"value": "={{ $env.SOCIAL_OLLAMA_MODEL || \"llama3.2\" }}"
},
"messages": {
"values": [
{
"content": "={{ $(\"Processar a\u00e7\u00e3o do portal\").item.json.aiPrompt }}"
}
]
},
"options": {
"system": "Retorne somente JSON v\u00e1lido conforme o pedido; n\u00e3o use markdown.",
"temperature": 0.3,
"num_predict": 1800
}
},
"name": "Ollama \u00b7 fallback local",
"type": "@n8n/n8n-nodes-langchain.ollama",
"typeVersion": 1,
"position": [
1152,
-160
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction scan(){\n fs.mkdirSync(INPUT,{recursive:true});\n const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n const items=[];\n for(const folder of folders){\n const dir=path.join(INPUT,folder);\n const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n const id=slug(folder); const old=state.records[id]||{};\n const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n }\n return {state,items};\n}\n\nconst request=$items('Processar')[0]?.json||{}; const id=safeText(request.contentId,130); const operator=safeText(request.operator,80)||'Sistema'; const {state,items}=scan(); const content=items.find(x=>x.id===id); if(!content) throw new Error('Conte\u00fado n\u00e3o encontrado ao salvar a sugest\u00e3o.');\n const candidate=$json.output_text||$json.text||$json.content||$json.output?.flatMap(x=>x.content||[]).map(x=>x.text||'').join('')||$json.candidates?.[0]?.content?.parts?.map(x=>x.text||'').join('')||''; let draft; try{draft=JSON.parse(candidate)}catch{throw new Error('A IA n\u00e3o retornou um JSON v\u00e1lido. Nenhuma legenda foi alterada.')}\nconst text=v=>safeText(v,5000); const variants=draft&&typeof draft.variants==='object'?draft.variants:{}; const normalized={baseCaption:text(draft.baseCaption),variants:{instagram:text(variants.instagram),facebook:text(variants.facebook),linkedin:text(variants.linkedin),xThread:Array.isArray(variants.xThread)?variants.xThread.map(v=>text(v,280)).filter(Boolean).slice(0,4):[]},hashtags:Array.isArray(draft.hashtags)?draft.hashtags.map(v=>text(v,80)).filter(Boolean).slice(0,12):[],reviewNotes:Array.isArray(draft.reviewNotes)?draft.reviewNotes.map(v=>text(v,240)).filter(Boolean).slice(0,5):[],generatedAt:new Date().toISOString(),model:safeText($json.model||$json.model_id||$env.SOCIAL_AI_MODEL||'OpenAI',80)}; if(!normalized.baseCaption) throw new Error('A IA n\u00e3o retornou uma legenda-base v\u00e1lida. Nenhuma altera\u00e7\u00e3o foi salva.');\nconst previous=state.records[id]||{}; const now=new Date().toISOString(); state.records[id]={...previous,brief:safeText(request.brief,1600),aiDraft:normalized,updatedAt:now,audit:[...(previous.audit||[]),{at:now,operator,action:'gerou sugest\u00e3o IA',comment:'Rascunho salvo para revis\u00e3o humana; a legenda atual n\u00e3o foi substitu\u00edda.'}]}; saveState(state); return [{json:{ok:true,route:'respond',message:'Sugest\u00e3o da IA salva para revis\u00e3o. A legenda atual n\u00e3o foi alterada.',draft:normalized}}];"
},
"name": "Validar e salvar sugest\u00e3o IA",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1440,
-144
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "return [{json:{ok:false,route:'respond',message:'N\u00e3o foi poss\u00edvel gerar a sugest\u00e3o. Revise as credenciais OpenAI/Gemini/Ollama ou tente novamente.'}}];"
},
"name": "Registrar indisponibilidade da IA",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1424,
16
]
},
{
"parameters": {
"options": {
"responseHeaders": {
"entries": [
{
"name": "Cache-Control",
"value": "no-store"
}
]
}
}
},
"name": "Responder ao portal",
"type": "n8n-nodes-base.respondToWebhook",
"typeVersion": 1.5,
"position": [
1696,
0
]
},
{
"parameters": {
"rule": {
"interval": [
{
"field": "minutes"
}
]
}
},
"name": "Schedule Trigger \u00b7 a cada 5 minutos",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.3,
"position": [
-720,
624
]
},
{
"parameters": {
"jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction scan(){\n fs.mkdirSync(INPUT,{recursive:true});\n const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n const items=[];\n for(const folder of folders){\n const dir=path.join(INPUT,folder);\n const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n const id=slug(folder); const old=state.records[id]||{};\n const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n }\n return {state,items};\n}\n\nconst enabled=String($env.SOCIAL_PUBLISH_ENABLED||'').toLowerCase()==='true'; if(!enabled) return [];\nconst now=new Date(); const nowIso=now.toISOString(); const {state,items}=scan(); const outgoing=[]; const publicMediaBase=String($env.SOCIAL_PUBLIC_MEDIA_BASE_URL||'').replace(/\\/+$/,''); const requireSignature=String($env.SOCIAL_MEDIA_REQUIRE_SIGNED_URLS||'').toLowerCase()==='true'; const signingSecret=String($env.SOCIAL_MEDIA_SIGNING_SECRET||'');\nfunction signedAssetUrl(contentId,file){if(!publicMediaBase) return ''; const base=publicMediaBase+'?id='+encodeURIComponent(contentId)+'&file='+encodeURIComponent(file); if(!requireSignature) return base; if(!signingSecret) throw new Error('SOCIAL_MEDIA_SIGNING_SECRET \u00e9 obrigat\u00f3rio quando SOCIAL_MEDIA_REQUIRE_SIGNED_URLS=true.'); const exp=Math.floor(Date.now()/1000)+7200; const sig=crypto.createHmac('sha256',signingSecret).update(contentId+':'+file+':'+exp).digest('base64url'); return base+'&exp='+exp+'&sig='+encodeURIComponent(sig)}\nfor(const content of items){\n const due=content.status==='aprovado'||(content.status==='agendado'&&content.scheduleAt&&new Date(content.scheduleAt)<=now); if(!due) continue;\n for(const network of content.networks||[]){ const delivery=state.records[content.id]?.deliveries?.[network]; if(!delivery) continue;\n const retryAt=delivery.retryAt?new Date(delivery.retryAt):null; const eligible=['queued','retry'].includes(delivery.status)&&(!retryAt||retryAt<=now); if(!eligible) continue;\n delivery.status='dispatching'; delivery.attempts=Number(delivery.attempts||0)+1; delivery.dispatchId=crypto.randomUUID(); delivery.lastAttemptAt=nowIso; delivery.lastError='';\n const variant=network==='x'?(content.aiDraft?.variants?.xThread||[]):safeText(content.aiDraft?.variants?.[network]||content.caption,5000);\n const assetUrls=Object.fromEntries((content.slides||[]).map(file=>[file,signedAssetUrl(content.id,file)])); outgoing.push({json:{contentId:content.id,folder:content.folder,title:content.title,caption:content.caption,brief:content.brief||'',network,slides:content.slides,assetVersion:content.assetVersion,variant,dispatchId:delivery.dispatchId,attempt:delivery.attempts,publicMediaBase,assetUrls}});\n }\n if(outgoing.length) break;\n}\nif(outgoing.length){for(const payload of outgoing){const record=state.records[payload.json.contentId]; record.updatedAt=nowIso; record.audit=[...(record.audit||[]),{at:nowIso,operator:'Sistema',action:'reservou publica\u00e7\u00e3o',comment:'Entrega '+payload.json.network+' reservada com idempot\u00eancia.',network:payload.json.network,dispatchId:payload.json.dispatchId}]} saveState(state)}\nreturn outgoing;"
},
"name": "Reservar entregas eleg\u00edveis",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
-384,
624
]
},
{
"parameters": {
"jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');\nconst STATE = path.join(ROOT, 'state.json');\nconst allowedStatuses = new Set(['pendente','aprovado','agendado','rejeitado','incompleto','publicado','parcial','falhou']);\nfunction slug(value) { const base=String(value).normalize('NFD').replace(/[\\u0300-\\u036f]/g,'').toLowerCase().replace(/[^a-z0-9]+/g,'-').replace(/(^-|-$)/g,'').slice(0,70)||'conteudo'; let h=0; for(const c of String(value)) h=((h<<5)-h+c.charCodeAt(0))|0; return base+'-'+(h>>>0).toString(36); }\nfunction safeText(value,max=5000){return String(value??'').replace(/\\0/g,'').trim().slice(0,max)}\nfunction readJson(file,fallback){try{return JSON.parse(fs.readFileSync(file,'utf8'))}catch{return fallback}}\nfunction saveState(state){const lock=STATE+'.lock'; let fd; try{fd=fs.openSync(lock,'wx')}catch{throw new Error('Outra altera\u00e7\u00e3o est\u00e1 em andamento. Atualize a p\u00e1gina e tente novamente.')} try{const temp=STATE+'.tmp';fs.writeFileSync(temp,JSON.stringify(state,null,2),'utf8');fs.renameSync(temp,STATE)}finally{if(fd!==undefined)fs.closeSync(fd);try{fs.unlinkSync(lock)}catch{}}}\nfunction reconcileRecordStatus(record){\n const statuses=(record?.networks||[]).map(network=>String(record?.deliveries?.[network]?.status||'queued'));\n if(!statuses.length) return record?.status||'pendente';\n if(!statuses.every(status=>['published','failed','blocked'].includes(status))) return record.status;\n const published=statuses.filter(status=>status==='published').length;\n record.status=published===statuses.length?'publicado':published>0?'parcial':'falhou';\n return record.status;\n}\nfunction scan(){\n fs.mkdirSync(INPUT,{recursive:true});\n const state=readJson(STATE,{version:1,records:{}}); state.records??={};\n const folders=fs.readdirSync(INPUT,{withFileTypes:true}).filter(x=>x.isDirectory()).map(x=>x.name);\n const items=[];\n for(const folder of folders){\n const dir=path.join(INPUT,folder);\n const files=fs.readdirSync(dir,{withFileTypes:true}).filter(x=>x.isFile()).map(x=>x.name);\n const slides=files.filter(x=>/\\.(png|jpe?g|webp)$/i.test(x)).sort((a,b)=>a.localeCompare(b,'pt-BR',{numeric:true}));\n const assetVersion=Math.max(0,...slides.map(file=>Math.floor(fs.statSync(path.join(dir,file)).mtimeMs)));\n const captionFile=files.find(x=>x.toLowerCase()==='texto.txt');\n const originalCaption=captionFile?safeText(fs.readFileSync(path.join(dir,captionFile),'utf8')):'';\n const id=slug(folder); const old=state.records[id]||{};\n const status=slides.length<1||!originalCaption&&!old.caption?'incompleto':allowedStatuses.has(old.status)?old.status:'pendente';\n items.push({id,folder,title:old.title||folder,slides,assetVersion,caption:old.caption??originalCaption,brief:old.brief||'',status,networks:Array.isArray(old.networks)?old.networks:[],scheduleAt:old.scheduleAt||'',updatedAt:old.updatedAt||'',aiDraft:old.aiDraft&&typeof old.aiDraft==='object'?old.aiDraft:null,deliveries:old.deliveries&&typeof old.deliveries==='object'?old.deliveries:{},audit:Array.isArray(old.audit)?old.audit:[]});\n }\n return {state,items};\n}\n\nconst config=$env; const now=new Date().toISOString(); const outputs=[]; const blocked=[];\nfor(const [inputIndex,inputItem] of $input.all().entries()){\n const payload=inputItem.json; const network=safeText(payload.network,30);\n const requirements={instagram:['SOCIAL_META_ENABLED','SOCIAL_META_INSTAGRAM_ACCOUNT_ID','SOCIAL_PUBLIC_MEDIA_BASE_URL'],facebook:['SOCIAL_META_ENABLED','SOCIAL_META_PAGE_ID','SOCIAL_PUBLIC_MEDIA_BASE_URL'],linkedin:['SOCIAL_LINKEDIN_ENABLED','SOCIAL_LINKEDIN_ORGANIZATION_URN'],x:['SOCIAL_X_ENABLED']}[network]||[];\n const missing=requirements.filter(key=>!safeText(config[key],400));\n const featureFlag={instagram:'SOCIAL_META_ENABLED',facebook:'SOCIAL_META_ENABLED',linkedin:'SOCIAL_LINKEDIN_ENABLED',x:'SOCIAL_X_ENABLED'}[network];\n if(featureFlag&&String(config[featureFlag]||'').trim().toLowerCase()!=='true'&&!missing.includes(featureFlag)) missing.unshift(featureFlag);\n if(!missing.length){outputs.push({json:payload,pairedItem:{item:inputIndex}});continue;}\n blocked.push({payload,network,missing});\n}\nif(blocked.length){\n const {state}=scan(); let changed=false;\n for(const entry of blocked){\n const delivery=state.records?.[entry.payload.contentId]?.deliveries?.[entry.network];\n if(delivery&&delivery.dispatchId===entry.payload.dispatchId){\n delivery.status='blocked'; delivery.retryAt=''; delivery.lastError='Configura\u00e7\u00e3o pendente: '+entry.missing.join(', ');\n const record=state.records[entry.payload.contentId]; reconcileRecordStatus(record); record.updatedAt=now;\n record.audit=[...(record.audit||[]),{at:now,operator:'Sistema',action:'bloqueou publica\u00e7\u00e3o',comment:delivery.lastError,network:entry.network,dispatchId:entry.payload.dispatchId}];\n changed=true;\n }\n }\n if(changed) saveState(state);\n}\nreturn outputs;"
},
"name": "Pr\u00e9-validar publica\u00e7\u00e3o",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
96,
624
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"leftValue": "={{ $json.network }}",
"rightValue": "instagram",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "1df2c2e8-7ebf-4630-a643-ac813db4271e"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Instagram"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"leftValue": "={{ $json.network }}",
"rightValue": "facebook",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "2453b5e4-a6ba-4f7a-9619-d940c84383ea"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "Facebook"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"leftValue": "={{ $json.network }}",
"rightValue": "linkedin",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "07b2e0a8-8ff8-4ca0-b10d-487ffe2e51b6"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "LinkedIn"
},
{
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 3
},
"conditions": [
{
"leftValue": "={{ $json.network }}",
"rightValue": "x",
"operator": {
"type": "string",
"operation": "equals"
},
"id": "107cc222-a1e4-44bf-b91e-5d132d3859f0"
}
],
"combinator": "and"
},
"renameOutput": true,
"outputKey": "X / thread"
}
]
},
"options": {
"fallbackOutput": "none",
"ignoreCase": false
}
},
"name": "Roteador por rede",
"type": "n8n-nodes-base.switch",
"typeVersion": 3.4,
"position": [
352,
624
]
},
{
"parameters": {
"jsCode": "const parent = $('Roteador por rede').item.json;\n\nif (!Array.isArray(parent.slides) || parent.slides.length < 2 || parent.slides.length > 10) {\n throw new Error('O carrossel do Instagram deve ter entre 2 e 10 imagens.');\n}\n\nif (!parent.publicMediaBase || !String(parent.publicMediaBase).startsWith('https://')) {\n throw new Error('Instagram requer uma URL p\u00fablica HTTPS para as imagens.');\n}\n\nreturn parent.slides.map((file, index) => {\n const url = parent.assetUrls?.[file];\n\n if (!url || !String(url).startsWith('https://')) {\n throw new Error(`N\u00e3o foi poss\u00edvel gerar a URL segura da imagem ${file}.`);\n }\n\n return {\n json: {\n ...parent,\n file,\n url,\n slideIndex: index,\n },\n pairedItem: { item: 0 },\n };\n});"
},
"name": "Instagram \u00b7 preparar slides",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
624,
352
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"method": "POST",
"url": "={{ \"https://graph.facebook.com/\"+($env.SOCIAL_META_GRAPH_VERSION || \"v25.0\")+\"/\"+$env.SOCIAL_META_INSTAGRAM_ACCOUNT_ID+\"/media\" }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ \"Bearer \"+$(\"Instagram \u00b7 token da P\u00e1gina\").first().json.access_token }}"
}
]
},
"sendBody": true,
"contentType": "form-urlencoded",
"bodyParameters": {
"parameters": [
{
"name": "image_url",
"value": "={{ $json.url }}"
},
{
"name": "is_carousel_item",
"value": "true"
}
]
},
"options": {
"timeout": 60000
}
},
"name": "Instagram \u00b7 criar containers",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
864,
352
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "const parent=$('Pr\u00e9-validar publica\u00e7\u00e3o').first().json; const children=$input.all().map(item=>item.json.id).filter(Boolean); if(children.length!==parent.slides.length) throw new Error('A Meta n\u00e3o retornou todos os containers do carrossel.'); return [{json:{...parent,children},pairedItem:{item:0}}];"
},
"name": "Instagram \u00b7 reunir containers",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1104,
352
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"method": "POST",
"url": "={{ \"https://graph.facebook.com/\"+($env.SOCIAL_META_GRAPH_VERSION || \"v25.0\")+\"/\"+$env.SOCIAL_META_INSTAGRAM_ACCOUNT_ID+\"/media\" }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ \"Bearer \"+$(\"Instagram \u00b7 token da P\u00e1gina\").first().json.access_token }}"
}
]
},
"sendBody": true,
"contentType": "form-urlencoded",
"bodyParameters": {
"parameters": [
{
"name": "media_type",
"value": "CAROUSEL"
},
{
"name": "children",
"value": "={{ $json.children.join(\",\") }}"
},
{
"name": "caption",
"value": "={{ $json.variant || $json.caption }}"
}
]
},
"options": {
"timeout": 60000
}
},
"name": "Instagram \u00b7 criar carrossel",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1344,
352
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"amount": 30
},
"name": "Instagram \u00b7 aguardar processamento",
"type": "n8n-nodes-base.wait",
"typeVersion": 1.1,
"position": [
1584,
352
]
},
{
"parameters": {
"url": "={{ \"https://graph.facebook.com/\"+($env.SOCIAL_META_GRAPH_VERSION || \"v25.0\")+\"/\"+$(\"Instagram \u00b7 criar carrossel\").item.json.id+\"?fields=status_code\" }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ \"Bearer \"+$(\"Instagram \u00b7 token da P\u00e1gina\").first().json.access_token }}"
}
]
},
"options": {
"timeout": 60000
}
},
"name": "Instagram \u00b7 consultar status do carrossel",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1824,
352
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "const status=String($json.status_code||'').toUpperCase(); if(status!=='FINISHED') throw new Error('O carrossel do Instagram ainda n\u00e3o terminou de processar (status: '+(status||'indispon\u00edvel')+'). A entrega ser\u00e1 tentada novamente.'); return $input.all();"
},
"name": "Instagram \u00b7 validar processamento",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2064,
352
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"method": "POST",
"url": "={{ \"https://graph.facebook.com/\"+($env.SOCIAL_META_GRAPH_VERSION || \"v25.0\")+\"/\"+$env.SOCIAL_META_INSTAGRAM_ACCOUNT_ID+\"/media_publish\" }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ \"Bearer \"+$(\"Instagram \u00b7 token da P\u00e1gina\").first().json.access_token }}"
}
]
},
"sendBody": true,
"contentType": "form-urlencoded",
"bodyParameters": {
"parameters": [
{
"name": "creation_id",
"value": "={{ $(\"Instagram \u00b7 criar carrossel\").item.json.id }}"
}
]
},
"options": {
"timeout": 60000
}
},
"name": "Instagram \u00b7 publicar carrossel",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
2304,
352
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "const parent = $('Roteador por rede').item.json;\n\nif (!Array.isArray(parent.slides) || parent.slides.length < 1) {\n throw new Error('A publica\u00e7\u00e3o do Facebook precisa ter pelo menos uma imagem.');\n}\n\nif (!parent.publicMediaBase || !String(parent.publicMediaBase).startsWith('https://')) {\n throw new Error('Facebook requer uma URL p\u00fablica HTTPS para as imagens neste fluxo.');\n}\n\nreturn parent.slides.map((file, index) => {\n const url = parent.assetUrls?.[file];\n\n if (!url || !String(url).startsWith('https://')) {\n throw new Error(`N\u00e3o foi poss\u00edvel gerar a URL segura da imagem ${file}.`);\n }\n\n return {\n json: {\n ...parent,\n file,\n url,\n slideIndex: index,\n },\n pairedItem: { item: 0 },\n };\n});"
},
"name": "Facebook \u00b7 preparar slides",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
624,
560
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"method": "POST",
"url": "={{ \"https://graph.facebook.com/\"+($env.SOCIAL_META_GRAPH_VERSION || \"v25.0\")+\"/\"+$env.SOCIAL_META_PAGE_ID+\"/photos\" }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ \"Bearer \"+$(\"Facebook \u00b7 token da P\u00e1gina\").first().json.access_token }}"
}
]
},
"sendBody": true,
"contentType": "form-urlencoded",
"bodyParameters": {
"parameters": [
{
"name": "url",
"value": "={{ $json.url }}"
},
{
"name": "published",
"value": "false"
}
]
},
"options": {
"timeout": 60000
}
},
"name": "Facebook \u00b7 enviar fotos n\u00e3o publicadas",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
864,
560
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "const parent=$('Pr\u00e9-validar publica\u00e7\u00e3o').first().json; const media=$input.all().map(item=>item.json.id).filter(Boolean).map(media_fbid=>({media_fbid})); if(media.length!==parent.slides.length) throw new Error('A Meta n\u00e3o retornou todas as fotos n\u00e3o publicadas do Facebook.'); return [{json:{...parent,attachedMedia:media},pairedItem:{item:0}}];"
},
"name": "Facebook \u00b7 reunir IDs das fotos",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1104,
560
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"method": "POST",
"url": "={{ \"https://graph.facebook.com/\"+($env.SOCIAL_META_GRAPH_VERSION || \"v25.0\")+\"/\"+$env.SOCIAL_META_PAGE_ID+\"/feed\" }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ \"Bearer \"+$(\"Facebook \u00b7 token da P\u00e1gina\").first().json.access_token }}"
}
]
},
"sendBody": true,
"specifyBody": "json",
"jsonBody": "={{ JSON.stringify({ message: $json.variant || $json.caption, attached_media: $json.attachedMedia }) }}",
"options": {
"timeout": 60000
}
},
"name": "Facebook \u00b7 publicar carrossel",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1344,
560
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "const parent=$input.first().json; if(!parent.slides.length) throw new Error('N\u00e3o h\u00e1 imagens para enviar ao LinkedIn.'); return parent.slides.map((file,index)=>({json:{...parent,file,filePath:'/files/postagem-redes/entrada/'+parent.folder+'/'+file},pairedItem:{item:0}}));"
},
"name": "LinkedIn \u00b7 preparar arquivos",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
624,
784
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"fileSelector": "={{ $json.filePath }}",
"options": {
"dataPropertyName": "data"
}
},
"name": "LinkedIn \u00b7 ler imagem local",
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1.1,
"position": [
864,
784
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"method": "PUT",
"url": "={{ $json.value.uploadUrl }}",
"sendBody": true,
"contentType": "raw",
"options": {
"timeout": 60000
}
},
"name": "LinkedIn \u00b7 enviar bin\u00e1rio",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1344,
784
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "const parent=$('Pr\u00e9-validar publica\u00e7\u00e3o').first().json; const images=$('Inicializar upload LinkedIn').all().map(item=>item.json.value?.image||item.json.image||item.json.id).filter(Boolean); if(images.length!==parent.slides.length) throw new Error('O LinkedIn n\u00e3o retornou URNs para todas as imagens.'); return [{json:{...parent,images},pairedItem:{item:0}}];"
},
"name": "LinkedIn \u00b7 reunir URNs",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1584,
784
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "const payload=$input.first().json; const thread=Array.isArray(payload.variant)?payload.variant.filter(Boolean):[]; const chunks=(thread.length?thread:[payload.caption]).map(value=>String(value).trim()).filter(Boolean).slice(0,4); if(!chunks.length) throw new Error('N\u00e3o h\u00e1 texto para a thread do X.'); return [{json:{...payload,chunks},pairedItem:{item:0}}];"
},
"name": "X \u00b7 adaptar sequ\u00eancia",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
624,
1024
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"fileSelector": "={{ \"/files/postagem-redes/entrada/\"+$json.folder+\"/\"+$json.slides[0] }}",
"options": {
"dataPropertyName": "data"
}
},
"name": "X \u00b7 ler primeira imagem",
"type": "n8n-nodes-base.readWriteFile",
"typeVersion": 1.1,
"position": [
864,
1024
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"method": "POST",
"url": "https://api.x.com/2/media/upload",
"authentication": "predefinedCredentialType",
"nodeCredentialType": "twitterOAuth2Api",
"sendBody": true,
"contentType": "multipart-form-data",
"bodyParameters": {
"parameters": [
{
"parameterType": "formBinaryData",
"name": "media",
"inputDataFieldName": "data"
},
{
"name": "media_category",
"value": "tweet_image"
},
{
"name": "media_type",
"value": "image/png"
}
]
},
"options": {
"timeout": 60000
}
},
"name": "X \u00b7 enviar m\u00eddia v2",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.4,
"position": [
1104,
1024
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"text": "={{ $(\"X \u00b7 adaptar sequ\u00eancia\").item.json.chunks[0] }}",
"additionalFields": {
"attachments": "={{ $(\"X \u00b7 enviar m\u00eddia v2\").item.json.data.id }}"
}
},
"name": "X \u00b7 publicar post inicial",
"type": "n8n-nodes-base.twitter",
"typeVersion": 2,
"position": [
1344,
1024
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ $(\"X \u00b7 adaptar sequ\u00eancia\").item.json.chunks.length >= 2 }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"name": "X \u00b7 h\u00e1 resposta 2?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
1584,
992
]
},
{
"parameters": {
"text": "={{ $(\"X \u00b7 adaptar sequ\u00eancia\").item.json.chunks[1] }}",
"additionalFields": {
"inReplyToStatusId": {
"mode": "id",
"value": "={{ $json.data.id }}"
}
}
},
"name": "X \u00b7 publicar resposta 2",
"type": "n8n-nodes-base.twitter",
"typeVersion": 2,
"position": [
1824,
944
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ $(\"X \u00b7 adaptar sequ\u00eancia\").item.json.chunks.length >= 3 }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"name": "X \u00b7 h\u00e1 resposta 3?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
2064,
992
]
},
{
"parameters": {
"text": "={{ $(\"X \u00b7 adaptar sequ\u00eancia\").item.json.chunks[2] }}",
"additionalFields": {
"inReplyToStatusId": {
"mode": "id",
"value": "={{ $json.data.id }}"
}
}
},
"name": "X \u00b7 publicar resposta 3",
"type": "n8n-nodes-base.twitter",
"typeVersion": 2,
"position": [
2304,
944
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"leftValue": "={{ $(\"X \u00b7 adaptar sequ\u00eancia\").item.json.chunks.length >= 4 }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true",
"singleValue": true
}
}
],
"combinator": "and"
},
"options": {}
},
"name": "X \u00b7 h\u00e1 resposta 4?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.3,
"position": [
2544,
992
]
},
{
"parameters": {
"text": "={{ $(\"X \u00b7 adaptar sequ\u00eancia\").item.json.chunks[3] }}",
"additionalFields": {
"inReplyToStatusId": {
"mode": "id",
"value": "={{ $json.data.id }}"
}
}
},
"name": "X \u00b7 publicar resposta 4",
"type": "n8n-nodes-base.twitter",
"typeVersion": 2,
"position": [
2784,
944
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"dataTableId": {
"mode": "name",
"value": "Postagem Redes - Ledger"
},
"columns": {
"mappingMode": "defineBelow",
"value": {
"content_id": "={{ $json.contentId }}",
"network": "={{ $json.network }}",
"status": "={{ $json.ok ? \"published\" : \"failed\" }}",
"remote_id": "={{ $json.remoteId || \"\" }}",
"permalink": "={{ $json.permalink || \"\" }}",
"error": "={{ $json.error || \"\" }}",
"dispatch_id": "={{ $json.dispatchId }}",
"occurred_at": "={{ $now.toISO() }}"
}
},
"options": {
"optimizeBulk": true
}
},
"name": "Registrar no Data Table \u00b7 Ledger",
"type": "n8n-nodes-base.dataTable",
"typeVersion": 1.1,
"position": [
3152,
1040
],
"onError": "continueErrorOutput"
},
{
"parameters": {
"jsCode": "\nconst fs = require('fs');\nconst path = require('path');\nconst crypto = require('crypto');\nconst ROOT = '/files/postagem-redes';\nconst INPUT = path.join(ROOT, 'entrada');
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Postagem Redes — Portal: Ações. Uses openAi, googleGemini, ollama, httpRequest. Webhook trigger; 58 nodes.
Source: https://github.com/Mayconxzdev/PostagemRedes/blob/main/workflows/05-portal-acoes.sanitized.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.
This workflow captures new leads via webhook, enriches and scores them with Apollo and Google Gemini, logs everything in Google Sheets, and automates outreach, reply handling, follow-ups, meeting prep
Automatically detects missed Zoom demos booked via Calendly and triggers AI-powered follow-up sequences.
How it works Runs on schedule (Monday-Friday at 9 AM) to automate lead generation Searches for companies on Google Maps by location and category Extracts owner information from company websites and im
Content Creation & Marketing Automation. Uses openAi, httpRequest, brevo, dataTable. Webhook trigger; 24 nodes.
This workflow receives a blog generation request via webhook, uses Google Gemini to write a full SEO-optimized post, generates a featured image with DALL-E 3, and automatically publishes the completed