This workflow corresponds to n8n.io template #13465 — we link there as the canonical source.
This workflow follows the Agent → Google Sheets 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 →
{
"id": "OaJZv61mZgAxF68I",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "Automatically publish LinkedIn posts using trends and AI quality checks",
"tags": [
{
"id": "mHQntpbppFUpZPGm",
"name": "template",
"createdAt": "2026-02-17T08:30:26.885Z",
"updatedAt": "2026-02-17T08:30:26.885Z"
}
],
"nodes": [
{
"id": "28905460-bb4c-4447-ba62-deb4d546861c",
"name": "Sticky Note",
"type": "n8n-nodes-base.stickyNote",
"position": [
-1440,
864
],
"parameters": {
"width": 700,
"height": 1060,
"content": "## Automatically publish LinkedIn posts using trends and AI quality checks\n\nAutomate your LinkedIn content pipeline: scan trending tech topics daily, generate drafts with AI, then let an AI selector and quality gate pick and publish the best one.\n\n## How it works\n\n**Flow 1 \u2014 Daily Research (6 AM):**\n1. Fetches trending topics from Hacker News, Reddit (8 subreddits), and Product Hunt in parallel\n2. Merges all sources, runs 7-layer deduplication, and ranks by relevance\n3. AI generates 3 LinkedIn post drafts with different angles (build story, insight, how-to)\n4. Saves all drafts to a Google Sheets queue with status tracking\n\n**Flow 2 \u2014 Smart Publish (Tue\u2013Thu 9:30 AM):**\n1. Reads all unpublished drafts from Google Sheets\n2. AI Selector picks the single best draft for today\n3. Selected post passes through an AI Quality Gate (scored 1\u201310)\n4. Approved \u2192 publish to LinkedIn + hashtag comment\n5. Needs revision \u2192 AI rewrites, then publishes\n6. Rejected \u2192 skipped, next best queued\n7. Telegram notification on every action\n\n## Setup steps\n\n1. **Google Sheets** \u2014 Create spreadsheet with columns: Post ID, Angle, Hook Line, Full Post, Hashtags, Trend Referenced, Word Count, Best Day, Posting Notes, Status, Created Date, Published Date, LinkedIn URL, AI Review, Revised Post, Dedup Stats, Generated At\n2. **Ollama** \u2014 Install locally and pull your model (e.g., `ollama pull mistral`)\n3. **LinkedIn OAuth** \u2014 Register app at developer.linkedin.com with `w_member_social` scope, get Person URN\n4. **Telegram Bot** \u2014 Create via @BotFather, get bot token and chat ID\n5. **AI Prompts** \u2014 Edit all 3 system prompts (Writer, Selector, Quality Gate) with your name, expertise, and tone\n6. **Test** \u2014 Run research flow manually first, then test publish flow\n\n## Customization\n\n- Tweak AI Selector weights in its system prompt\n- Adjust Quality Gate score thresholds as needed\n- Set any draft status to `force_publish` to override\n- Add more sources by duplicating the fetch/normalize pattern\n- Change schedule via cron expressions in Schedule Trigger nodes"
},
"typeVersion": 1
},
{
"id": "453d29b4-33aa-47f0-bf85-7593e80f2ca9",
"name": "Sticky Note1",
"type": "n8n-nodes-base.stickyNote",
"position": [
-256,
720
],
"parameters": {
"color": 7,
"width": 344,
"height": 616,
"content": "## Fetch trending topics\nHacker News, Reddit, and Product Hunt fetched in parallel."
},
"typeVersion": 1
},
{
"id": "06951d31-821b-4bac-ad83-6b172cd7c3a5",
"name": "Sticky Note8",
"type": "n8n-nodes-base.stickyNote",
"position": [
112,
720
],
"parameters": {
"color": 7,
"width": 300,
"height": 600,
"content": "## Normalize fetched data\nStandardize output format from each source."
},
"typeVersion": 1
},
{
"id": "9d91bd5e-9b7c-4f87-b6f8-2969b7bfe292",
"name": "Sticky Note2",
"type": "n8n-nodes-base.stickyNote",
"position": [
608,
496
],
"parameters": {
"color": 7,
"width": 1200,
"height": 420,
"content": "## Merge, deduplicate, and generate drafts\nWait for all sources, run 7-layer dedup, rank, then AI writes 3 drafts."
},
"typeVersion": 1
},
{
"id": "e98844f7-48e2-456e-a60a-fd0cb043b742",
"name": "Sticky Note9",
"type": "n8n-nodes-base.stickyNote",
"position": [
2096,
576
],
"parameters": {
"color": 7,
"width": 380,
"height": 324,
"content": "## Save drafts to Google Sheets\nAppend generated posts to the queue with status \"draft\"."
},
"typeVersion": 1
},
{
"id": "9abded7f-8f92-40c8-b43a-f85801b49218",
"name": "Sticky Note10",
"type": "n8n-nodes-base.stickyNote",
"position": [
-464,
2240
],
"parameters": {
"color": 7,
"width": 692,
"height": 344,
"content": "## Read drafts and select best post\nLoad all drafts, filter unpublished, AI Selector picks the best one for today."
},
"typeVersion": 1
},
{
"id": "6310c9eb-b360-48fb-ba2e-40787cb06b3e",
"name": "Sticky Note11",
"type": "n8n-nodes-base.stickyNote",
"position": [
480,
2080
],
"parameters": {
"color": 7,
"width": 1004,
"height": 636,
"content": "## AI Quality Gate and review\nScore the selected post across 5 criteria, then route to approve, revise, or reject."
},
"typeVersion": 1
},
{
"id": "b07ad6a2-3f61-44ef-be78-718ad08854c7",
"name": "Sticky Note12",
"type": "n8n-nodes-base.stickyNote",
"position": [
1856,
2144
],
"parameters": {
"color": 7,
"width": 1452,
"height": 636,
"content": "## Publish to LinkedIn and notify\nPublish approved or revised post, add hashtag comment, update sheet, send Telegram notification."
},
"typeVersion": 1
},
{
"id": "56d71465-5a9a-4ee5-b98c-675cccb9b783",
"name": "Sticky Note6",
"type": "n8n-nodes-base.stickyNote",
"position": [
2320,
1920
],
"parameters": {
"color": 2,
"width": 484,
"height": 152,
"content": "## \u26a0\ufe0f Replace YOUR_LINKEDIN_PERSON_ID\nUpdate the Publish and Comment nodes with your LinkedIn Person URN."
},
"typeVersion": 1
},
{
"id": "96b5a172-1663-4e41-9f96-e71b41b5f3cd",
"name": "\u23f0 Daily 6 AM \u2014 Research",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-448,
992
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 6 * * *"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "c9e2a585-79cd-4b96-a924-bac5a004e99e",
"name": "\ud83d\udfe0 Fetch Hacker News",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
-176,
832
],
"parameters": {
"url": "https://hn.algolia.com/api/v1/search?tags=front_page&hitsPerPage=30",
"options": {
"timeout": 15000,
"response": {
"response": {
"responseFormat": "json"
}
}
}
},
"typeVersion": 4.2
},
{
"id": "d9abf24d-14a5-4956-8a8b-193ae12c5c8f",
"name": "\ud83d\udd34 Fetch Reddit",
"type": "n8n-nodes-base.code",
"position": [
-176,
1008
],
"parameters": {
"jsCode": "const subreddits = ['technology','programming','startups','machinelearning','artificial','SaaS','webdev','entrepreneurship'];\nconst results = [];\nfor (const sub of subreddits) {\n try {\n const response = await this.helpers.httpRequest({ method:'GET', url:`https://www.reddit.com/r/${sub}/hot.json?limit=8`, headers:{'User-Agent':'n8n-linkedin-poster/1.0'}, timeout:10000, json:true });\n if (response?.data?.children) {\n for (const post of response.data.children) {\n const d = post.data;\n if (d?.title && !d.stickied) {\n results.push({json:{title:d.title,url:d.url||`https://reddit.com${d.permalink}`,permalink:`https://reddit.com${d.permalink}`,score:d.score||0,comments:d.num_comments||0,subreddit:sub,selftext:(d.selftext||'').substring(0,300),source:'reddit',sourceLabel:`r/${sub}`,created:d.created_utc?new Date(d.created_utc*1000).toISOString():new Date().toISOString()}});\n }\n }\n }\n } catch(e) {}\n}\nreturn results.length>0?results:[{json:{source:'reddit',noData:true,items:0}}];"
},
"typeVersion": 2
},
{
"id": "1122d916-99b6-4b30-a24d-63517f1f6d99",
"name": "\ud83d\udfe3 Fetch Product Hunt",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
-176,
1184
],
"parameters": {
"url": "https://www.producthunt.com/feed",
"options": {
"timeout": 15000,
"response": {
"response": {
"responseFormat": "text"
}
}
},
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "User-Agent",
"value": "Mozilla/5.0"
},
{
"name": "Accept",
"value": "application/rss+xml,text/xml,*/*"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "466b92a7-9003-4945-9244-08354bd22f66",
"name": "\ud83d\udd27 Normalize HN",
"type": "n8n-nodes-base.code",
"position": [
176,
816
],
"parameters": {
"jsCode": "const items=$input.all();const n=[];\nfor(const item of items){const d=item.json;if(d.hits&&Array.isArray(d.hits)){for(const h of d.hits){if(h.title&&h.title.length>3){n.push({json:{title:h.title,url:h.url||`https://news.ycombinator.com/item?id=${h.objectID}`,discussionUrl:`https://news.ycombinator.com/item?id=${h.objectID}`,score:h.points||0,comments:h.num_comments||0,author:h.author||'',source:'hackernews',sourceLabel:'Hacker News',created:h.created_at||new Date().toISOString(),_normalized:true}});}}}else if(d.title){n.push({json:{title:d.title,url:d.url||'',score:d.points||d.score||0,comments:d.num_comments||0,source:'hackernews',sourceLabel:'Hacker News',created:d.created_at||new Date().toISOString(),_normalized:true}});}}\nreturn n.length>0?n:[{json:{source:'hackernews',noData:true,_normalized:true}}];"
},
"typeVersion": 2
},
{
"id": "3da3f175-2a43-4cc9-8c7c-405aca3fcef4",
"name": "\ud83d\udd27 Normalize Reddit",
"type": "n8n-nodes-base.code",
"position": [
176,
1008
],
"parameters": {
"jsCode": "const items=$input.all();const n=[];\nfor(const item of items){const d=item.json;if(d.title&&!d.noData){n.push({json:{...d,_normalized:true}});}}\nreturn n.length>0?n:[{json:{source:'reddit',noData:true,_normalized:true}}];"
},
"typeVersion": 2
},
{
"id": "c20ba66a-0cf5-4af0-86c5-f41d86527c09",
"name": "\ud83d\udd27 Normalize PH",
"type": "n8n-nodes-base.code",
"position": [
176,
1200
],
"parameters": {
"jsCode": "const items=$input.all();const n=[];\nfor(const item of items){let raw=typeof item.json==='string'?item.json:(item.json.data||item.json.body||JSON.stringify(item.json));raw=String(raw);const ms=raw.match(/<item[\\s\\S]*?<\\/item>/gi)||[];\nfor(const xml of ms.slice(0,20)){const g=(tag)=>{const m=xml.match(new RegExp(`<${tag}[^>]*><!\\\\[CDATA\\\\[([^\\\\]]*?)\\\\]\\\\]><\\\\/${tag}>`,'i'))||xml.match(new RegExp(`<${tag}[^>]*>([^<]*)<\\\\/${tag}>`,'i'));return m?m[1].trim():'';};const t=g('title'),l=g('link')||g('guid');\nif(t&&l){n.push({json:{title:t,url:l,description:g('description').replace(/<[^>]+>/g,' ').substring(0,300),score:0,comments:0,source:'producthunt',sourceLabel:'Product Hunt',created:g('pubDate')||new Date().toISOString(),_normalized:true}});}}}\nreturn n.length>0?n:[{json:{source:'producthunt',noData:true,_normalized:true}}];"
},
"typeVersion": 2
},
{
"id": "f4cb691e-9351-4965-8412-9932bf9d275c",
"name": "\u23f3 Wait for ALL Sources",
"type": "n8n-nodes-base.merge",
"position": [
432,
960
],
"parameters": {
"numberInputs": 3
},
"typeVersion": 3.1
},
{
"id": "913e3121-6adb-43b3-8e62-3a6e7160d7d4",
"name": "\ud83d\udce6 Collect All Items",
"type": "n8n-nodes-base.code",
"position": [
688,
688
],
"parameters": {
"jsCode": "const all=$input.all();const c=[];\nfor(const item of all){const d=item.json;if(d.noData||!d.title||!d._normalized||d.title.length<5)continue;c.push({title:d.title||'',url:d.url||'',discussionUrl:d.discussionUrl||d.permalink||'',score:d.score||0,comments:d.comments||0,source:d.source||'unknown',sourceLabel:d.sourceLabel||d.source||'Unknown',created:d.created||new Date().toISOString(),description:d.description||d.selftext||'',author:d.author||'',subreddit:d.subreddit||''});}\nconst sc={};for(const i of c){sc[i.source]=(sc[i.source]||0)+1;}\nreturn{json:{allItems:c,totalCollected:c.length,sourceCounts:sc,collectedAt:new Date().toISOString()}};"
},
"typeVersion": 2
},
{
"id": "a0e55c6e-91e8-4e6a-b874-e56fd0a4761e",
"name": "\ud83e\uddf9 Deduplicate + Rank",
"type": "n8n-nodes-base.code",
"position": [
880,
672
],
"parameters": {
"jsCode": "const input=$json;const allItems=input.allItems||[];\nif(!allItems.length)return{json:{itemCount:0,totalScanned:0,topItems:[],topItemsFormatted:'No items.',stats:{totalFetched:0,duplicatesRemoved:0,afterDedup:0},scannedAt:new Date().toISOString()}};\nconst sw=new Set(['the','and','for','are','but','not','you','all','can','has','its','may','new','now','old','see','way','who','did','get','got','had','her','him','his','how','let','our','own','say','she','too','use','was','why','with','from','that','this','what','when','will','your','been','have','just','more','most','much','than','them','then','they','very','about','after','being','could','every','first','their','there','these','thing','those','using','which','would','should','into','some','also','like','over','such','take','only','come','make','know','year','back','even','give','many']);\nfunction nu(u){if(!u)return'';return u.toLowerCase().replace(/https?:\\/\\//,'').replace(/^www\\./,'').replace(/\\/+$/,'').replace(/\\?.*$/,'').replace(/#.*$/,'').replace(/\\/amp\\/?$/,'').trim();}\nfunction ec(u){if(!u)return'';const n=nu(u);if(n.includes('reddit.com/r/'))return'';if(n.includes('news.ycombinator.com'))return'';if(n.includes('producthunt.com/posts/')){const m=n.match(/producthunt\\.com\\/posts\\/([a-z0-9-]+)/);return m?m[1]:n;}return n;}\nfunction nt(t){return(t||'').toLowerCase().replace(/^(show hn|ask hn|tell hn|launch hn):\\s*/i,'').replace(/^\\[.*?\\]\\s*/,'').replace(/\\(.*?\\)/g,'').replace(/[^a-z0-9\\s]/g,' ').replace(/\\s+/g,' ').trim();}\nfunction gw(t){return nt(t).split(' ').filter(w=>w.length>2&&!sw.has(w));}\nfunction jc(a,b){const sA=new Set(a),sB=new Set(b);if(!sA.size||!sB.size)return 0;let i=0;for(const w of sA)if(sB.has(w))i++;return i/(sA.size+sB.size-i);}\nconst su=new Set(),sc=new Set(),ac=[],cr={};\nfor(const item of allItems){const nurl=nu(item.url),cu=ec(item.url),ntl=nt(item.title),ws=gw(item.title),p4=ws.slice(0,4).join(' '),p3=ws.slice(0,3).join(' ');\nlet dup=false,mi=-1;\nif(nurl&&su.has(nurl)){dup=true;mi=ac.findIndex(a=>nu(a.item.url)===nurl);}\nif(!dup&&cu&&cu.length>5&&sc.has(cu)){dup=true;mi=ac.findIndex(a=>ec(a.item.url)===cu);}\nif(!dup&&ntl.length>10){for(let i=0;i<ac.length;i++)if(ac[i].nt===ntl){dup=true;mi=i;break;}}\nif(!dup&&p4.length>12){for(let i=0;i<ac.length;i++)if(ac[i].p4.length>12&&ac[i].p4===p4){dup=true;mi=i;break;}}\nif(!dup&&p3.length>8){for(let i=0;i<ac.length;i++)if(ac[i].p3.length>8&&ac[i].p3===p3){dup=true;mi=i;break;}}\nif(!dup&&ws.length>=3){for(let i=0;i<ac.length;i++)if(ac[i].ws.length>=3&&jc(ws,ac[i].ws)>0.6){dup=true;mi=i;break;}}\nif(!dup&&ntl.length>15){for(let i=0;i<ac.length;i++)if(ac[i].nt.length>15&&(ntl.includes(ac[i].nt)||ac[i].nt.includes(ntl))){dup=true;mi=i;break;}}\nif(dup&&mi>=0){const tk=ac[mi].p4||ac[mi].nt;if(!cr[tk])cr[tk]={sources:new Set([ac[mi].item.source]),ms:ac[mi].item.score||0,tc:ac[mi].item.comments||0};cr[tk].sources.add(item.source);cr[tk].ms=Math.max(cr[tk].ms,item.score||0);cr[tk].tc+=(item.comments||0);continue;}\nif(nurl)su.add(nurl);if(cu&&cu.length>5)sc.add(cu);const tk=p4||ntl;cr[tk]={sources:new Set([item.source]),ms:item.score||0,tc:item.comments||0};ac.push({item,nt:ntl,ws,p4,p3});}\nconst en=ac.map(e=>{const tk=e.p4||e.nt,tr=cr[tk]||{sources:new Set([e.item.source]),ms:e.item.score,tc:e.item.comments};const s=tr.sources.size,sn=Math.min((tr.ms||0)/100,10),cn=Math.min((tr.tc||0)/50,10);const rel=Math.round(sn*3+cn*2+(s-1)*8+(e.item.created&&Date.now()-new Date(e.item.created).getTime()<12*3600000?3:0));return{...e.item,relevanceScore:rel,crossSourceCount:s,crossSources:Array.from(tr.sources).join(', '),isMultiSource:s>1,combinedScore:tr.ms,combinedComments:tr.tc};});\nen.sort((a,b)=>b.relevanceScore-a.relevanceScore);const top20=en.slice(0,20);const dr=allItems.length-ac.length;\nreturn{json:{stats:{totalFetched:allItems.length,duplicatesRemoved:dr,deduplicationRate:`${Math.round(dr/Math.max(allItems.length,1)*100)}%`,afterDedup:ac.length,topItemsCount:top20.length,multiSourceItems:en.filter(i=>i.isMultiSource).length,sourceCounts:input.sourceCounts},itemCount:top20.length,totalScanned:allItems.length,topItems:top20,topItemsFormatted:top20.map((item,i)=>{const m=item.isMultiSource?` \u2b50 [TRENDING on ${item.crossSources}]`:'';return`${i+1}. [${item.sourceLabel}] ${item.title}${m}\\n Score: ${item.combinedScore} | Comments: ${item.combinedComments} | Relevance: ${item.relevanceScore}\\n URL: ${item.url}`;}).join('\\n\\n'),scannedAt:new Date().toISOString()}};"
},
"typeVersion": 2
},
{
"id": "f52fe204-cd16-4e46-a943-35c6e483ff57",
"name": "\ud83d\udcca Has Items?",
"type": "n8n-nodes-base.if",
"position": [
1104,
672
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "has-items",
"operator": {
"type": "number",
"operation": "gt"
},
"leftValue": "={{ $json.itemCount }}",
"rightValue": 0
}
]
}
},
"typeVersion": 2.2
},
{
"id": "c638109c-0e58-427f-aefc-06d92c2b1ebb",
"name": "\ud83e\udd16 AI LinkedIn Writer",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
1440,
528
],
"parameters": {
"text": "=Top {{ $json.itemCount }} trending topics:\n\n{{ $json.topItemsFormatted }}\n\nGenerate 3 LinkedIn post drafts. Each DIFFERENT angle.",
"options": {
"systemMessage": "You are a LinkedIn content strategist writing for: [YOUR NAME]\nExpertise: [YOUR EXPERTISE]\nProduct: [YOUR PRODUCT]\nTone: Smart, conversational, no corporate speak.\n\nGENERATE 3 POSTS:\n1. BUILD STORY \u2014 \"I built X that does Y.\"\n2. INSIGHT \u2014 \"Everyone talks about X. Here's what they miss.\"\n3. HOW-TO \u2014 \"How to do X in 5 steps.\"\n\nRULES: 150-250 words. Hook first line. Short paragraphs. Max 3 emoji. No hashtags in body. Soft CTA. Include specifics. Human tone. Reference real trends. \u2b50 items = prioritize.\n\nRAW JSON ONLY. No markdown fences.\n{\"posts\":[{\"angle\":\"build_story\",\"hookLine\":\"...\",\"fullPost\":\"...\",\"hashtags\":[\"#tag\"],\"trendReferenced\":\"...\",\"estimatedWordCount\":180,\"bestPostingDay\":\"Tuesday\",\"postingNotes\":\"...\"},{\"angle\":\"insight_opinion\",\"hookLine\":\"...\",\"fullPost\":\"...\",\"hashtags\":[],\"trendReferenced\":\"...\",\"estimatedWordCount\":200,\"bestPostingDay\":\"Wednesday\",\"postingNotes\":\"...\"},{\"angle\":\"howto_tutorial\",\"hookLine\":\"...\",\"fullPost\":\"...\",\"hashtags\":[],\"trendReferenced\":\"...\",\"estimatedWordCount\":220,\"bestPostingDay\":\"Thursday\",\"postingNotes\":\"...\"}],\"weekTheme\":\"...\",\"contentCalendarNote\":\"...\"}"
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 2.1
},
{
"id": "80f18a69-b090-437e-9343-7d7486fe9a85",
"name": "\ud83d\udcdd Extract Posts",
"type": "n8n-nodes-base.code",
"position": [
1936,
720
],
"parameters": {
"jsCode": "const sourceData=$('\ud83d\udcca Has Items?').item.json;const raw=$json;let pd={};\nfunction ej(s){if(typeof s!=='string')return null;let c=s.replace(/```json\\s*/gi,'').replace(/```\\s*/g,'').trim();try{return JSON.parse(c);}catch(e){const m=c.match(/\\{[\\s\\S]*\"posts\"\\s*:[\\s\\S]*\\}/);if(m){try{return JSON.parse(m[0]);}catch(e2){return null;}}return null;}}\nfunction fa(o,d=0){if(d>5||!o||typeof o!=='object')return null;if(o.posts&&Array.isArray(o.posts))return o;const ps=['output','text','message','content','response','result','data','json','kwargs','lc_kwargs'];for(const p of ps){if(o[p]!==undefined){if(typeof o[p]==='string'){const r=ej(o[p]);if(r&&r.posts)return r;}else if(typeof o[p]==='object'){const f=fa(o[p],d+1);if(f)return f;}}}for(const k of Object.keys(o)){if(ps.includes(k))continue;const v=o[k];if(typeof v==='string'&&v.includes('\"posts\"')){const r=ej(v);if(r&&r.posts)return r;}else if(typeof v==='object'&&v!==null){const f=fa(v,d+1);if(f)return f;}}return null;}\ntry{if(raw&&raw.posts)pd=raw;else{const f=fa(raw);if(f)pd=f;}if(!pd.posts){const s=JSON.stringify(raw);const p=ej(s);if(p&&p.posts)pd=p;}}catch(e){}\nif(!pd.posts||!Array.isArray(pd.posts)||pd.posts.length===0){pd={posts:[{angle:'build_story',hookLine:'AI failed',fullPost:'Manual draft needed.',hashtags:['#n8n'],trendReferenced:'N/A',estimatedWordCount:0,bestPostingDay:'Tuesday',postingNotes:'AI failed'}],weekTheme:'N/A',contentCalendarNote:'Check AI'};}\nconst today=new Date().toISOString().split('T')[0];const results=[];\nfor(let i=0;i<pd.posts.length;i++){const p=pd.posts[i];results.push({json:{postId:`${today}-${i+1}-${p.angle}`,angle:p.angle||'unknown',hookLine:p.hookLine||'',fullPost:p.fullPost||'',hashtags:Array.isArray(p.hashtags)?p.hashtags.join(' '):(p.hashtags||''),trendReferenced:p.trendReferenced||'',wordCount:p.estimatedWordCount||0,bestPostingDay:p.bestPostingDay||'Tuesday',postingNotes:p.postingNotes||'',status:'draft',createdDate:today,weekTheme:pd.weekTheme||'',dedupStats:`${sourceData.stats.totalFetched}\u2192${sourceData.stats.duplicatesRemoved} dupes (${sourceData.stats.deduplicationRate})\u2192${sourceData.stats.afterDedup} unique`,generatedAt:new Date().toISOString()}});}\nreturn results;"
},
"typeVersion": 2
},
{
"id": "4137872b-da6d-43c9-a027-e2fec38881a5",
"name": "\ud83d\udccb Save to Queue",
"type": "n8n-nodes-base.googleSheets",
"position": [
2208,
720
],
"parameters": {
"columns": {
"value": {
"Angle": "={{ $json.angle }}",
"Status": "={{ $json.status }}",
"Post ID": "={{ $json.postId }}",
"Best Day": "={{ $json.bestPostingDay }}",
"Hashtags": "={{ $json.hashtags }}",
"AI Review": "",
"Full Post": "={{ $json.fullPost }}",
"Hook Line": "={{ $json.hookLine }}",
"Word Count": "={{ $json.wordCount }}",
"Dedup Stats": "={{ $json.dedupStats }}",
"Created Date": "={{ $json.createdDate }}",
"Generated At": "={{ $json.generatedAt }}",
"LinkedIn URL": "",
"Revised Post": "",
"Posting Notes": "={{ $json.postingNotes }}",
"Published Date": "",
"Trend Referenced": "={{ $json.trendReferenced }}"
},
"schema": [
{
"id": "Post ID",
"type": "string",
"display": true,
"required": false,
"displayName": "Post ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Angle",
"type": "string",
"display": true,
"required": false,
"displayName": "Angle",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Hook Line",
"type": "string",
"display": true,
"required": false,
"displayName": "Hook Line",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Full Post",
"type": "string",
"display": true,
"required": false,
"displayName": "Full Post",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Hashtags",
"type": "string",
"display": true,
"required": false,
"displayName": "Hashtags",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Trend Referenced",
"type": "string",
"display": true,
"required": false,
"displayName": "Trend Referenced",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Word Count",
"type": "string",
"display": true,
"required": false,
"displayName": "Word Count",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Best Day",
"type": "string",
"display": true,
"required": false,
"displayName": "Best Day",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Posting Notes",
"type": "string",
"display": true,
"required": false,
"displayName": "Posting Notes",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Created Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Created Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Published Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Published Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "LinkedIn URL",
"type": "string",
"display": true,
"required": false,
"displayName": "LinkedIn URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "AI Review",
"type": "string",
"display": true,
"required": false,
"displayName": "AI Review",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Revised Post",
"type": "string",
"display": true,
"required": false,
"displayName": "Revised Post",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Dedup Stats",
"type": "string",
"display": true,
"required": false,
"displayName": "Dedup Stats",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Generated At",
"type": "string",
"display": true,
"required": false,
"displayName": "Generated At",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {},
"operation": "append",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultName": "Post Queue"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.6
},
{
"id": "9b689f0a-f4d5-4ccd-9c69-88735a88501b",
"name": "\ud83d\udca4 No Items",
"type": "n8n-nodes-base.code",
"position": [
1472,
832
],
"parameters": {
"jsCode": "return{json:{status:'no_trends',message:'No items after dedup.',checkedAt:new Date().toISOString()}};"
},
"typeVersion": 2
},
{
"id": "2168cdbf-bf4c-4fc9-abe7-c3f1486b042f",
"name": "\u23f0 Tue-Thu 9:30 AM \u2014 Publish",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
-608,
2368
],
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "30 9 * * 2,3,4"
}
]
}
},
"typeVersion": 1.2
},
{
"id": "86b157cb-dcc5-447f-bc4a-301d673d47e6",
"name": "\ud83d\udccb Read ALL Drafts",
"type": "n8n-nodes-base.googleSheets",
"position": [
-384,
2368
],
"parameters": {
"options": {},
"sheetName": {
"__rl": true,
"mode": "list",
"value": "YOUR_SHEET_GID",
"cachedResultName": "Post Queue"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.6
},
{
"id": "1fcae955-f777-407e-bc67-dcc4175b059b",
"name": "\ud83d\udce6 Collect All Drafts",
"type": "n8n-nodes-base.code",
"position": [
-160,
2368
],
"parameters": {
"jsCode": "// ============================================\n// COLLECT ALL UNPUBLISHED DRAFTS FOR AI SELECTION\n// ============================================\n\nconst allRows = $input.all();\nconst drafts = [];\nconst recentlyPublished = [];\n\nconst dayNames = ['Sunday','Monday','Tuesday','Wednesday','Thursday','Friday','Saturday'];\nconst today = new Date();\nconst todayName = dayNames[today.getDay()];\nconst todayDate = today.toISOString().split('T')[0];\n\nfor (const row of allRows) {\n const r = row.json;\n const status = (r.Status || r.status || '').toLowerCase().trim();\n const published = r['Published Date'] || r.publishedDate || '';\n \n // Collect unpublished drafts\n if ((status === 'draft' || status === 'pending_review' || status === 'approved') && !published) {\n drafts.push({\n postId: r['Post ID'] || r.postId || '',\n angle: r.Angle || r.angle || '',\n hookLine: r['Hook Line'] || r.hookLine || '',\n fullPost: r['Full Post'] || r.fullPost || '',\n hashtags: r.Hashtags || r.hashtags || '',\n trendReferenced: r['Trend Referenced'] || r.trendReferenced || '',\n wordCount: r['Word Count'] || r.wordCount || 0,\n bestDay: r['Best Day'] || r.bestPostingDay || '',\n postingNotes: r['Posting Notes'] || r.postingNotes || '',\n createdDate: r['Created Date'] || r.createdDate || '',\n status: status\n });\n }\n \n // Track recently published to avoid similar angles\n if (status === 'published' || status === 'revised_and_published') {\n recentlyPublished.push({\n angle: r.Angle || r.angle || '',\n publishedDate: published,\n trendReferenced: r['Trend Referenced'] || ''\n });\n }\n}\n\nif (drafts.length === 0) {\n return { json: {\n hasDrafts: false,\n message: 'No unpublished drafts in queue.',\n checkedAt: new Date().toISOString()\n }};\n}\n\n// Format drafts for AI selection\nconst draftsFormatted = drafts.map((d, i) => {\n const dayMatch = d.bestDay === todayName ? ' \u2705 BEST DAY MATCH' : '';\n return `--- DRAFT ${i + 1} ---\\nPost ID: ${d.postId}\\nAngle: ${d.angle}\\nHook: ${d.hookLine}\\nTrend: ${d.trendReferenced}\\nWord Count: ${d.wordCount}\\nBest Day: ${d.bestDay}${dayMatch}\\nCreated: ${d.createdDate}\\nStatus: ${d.status}\\n\\nFull Post:\\n${d.fullPost}\\n`;\n}).join('\\n');\n\n// Format recent posts for context\nconst recentContext = recentlyPublished.slice(-5).map(p => \n `- ${p.angle} about \"${p.trendReferenced}\" on ${p.publishedDate}`\n).join('\\n') || 'No recent posts.';\n\nreturn { json: {\n hasDrafts: true,\n totalDrafts: drafts.length,\n drafts: drafts,\n draftsFormatted: draftsFormatted,\n recentPostsContext: recentContext,\n todayName: todayName,\n todayDate: todayDate,\n checkedAt: new Date().toISOString()\n}};"
},
"typeVersion": 2
},
{
"id": "33472742-2b39-4a52-b0fd-835a09682c84",
"name": "\ud83d\udcc4 Has Drafts?",
"type": "n8n-nodes-base.if",
"position": [
64,
2368
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "has-drafts",
"operator": {
"type": "boolean",
"operation": "true"
},
"leftValue": "={{ $json.hasDrafts }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.2
},
{
"id": "4869d6b6-cdc5-4915-9c53-6c9cba6898f0",
"name": "\ud83e\udde0 AI Post Selector",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
544,
2176
],
"parameters": {
"text": "=You have {{ $json.totalDrafts }} unpublished LinkedIn post drafts to choose from.\nToday is {{ $json.todayName }}, {{ $json.todayDate }}.\n\n--- AVAILABLE DRAFTS ---\n{{ $json.draftsFormatted }}\n\n--- RECENTLY PUBLISHED (avoid repeating) ---\n{{ $json.recentPostsContext }}\n\n--- SELECT THE BEST POST FOR TODAY ---",
"options": {
"systemMessage": "You are a LinkedIn content strategist. Your job is to analyze ALL available post drafts and select the SINGLE BEST one to publish today.\n\nYou are selecting for: [YOUR NAME]\nAudience: [YOUR AUDIENCE \u2014 e.g., founders, developers, tech leaders]\n\n---\n\nSELECTION CRITERIA (rank by importance):\n\n1. TODAY'S DAY ALIGNMENT (30% weight)\n - If a post has bestDay matching today, it gets a strong boost\n - Tuesday = build stories work best\n - Wednesday = insights/opinions peak\n - Thursday = how-to/tutorials perform well\n - But don't blindly follow this \u2014 a great post beats a day-matched mediocre one\n\n2. HOOK STRENGTH (25% weight)\n - Which draft has the most scroll-stopping first line?\n - Specific > generic\n - Curiosity/tension > announcement\n - Does it make you want to read more?\n\n3. ANGLE VARIETY (20% weight)\n - Look at recently published posts\n - DO NOT pick the same angle as the last 2 posts\n - Rotate: build_story \u2192 insight \u2192 howto \u2192 build_story\n - If last post was insight, prefer build_story or howto\n\n4. TREND FRESHNESS (15% weight)\n - Prefer posts about current/recent trends\n - Older trends lose relevance fast\n - Check the created date\n\n5. ENGAGEMENT POTENTIAL (10% weight)\n - Will this generate comments?\n - Is there a discussion hook?\n - Is it polarizing enough to get reactions but not controversial?\n\n---\n\nDECISION OUTPUT:\n\nYou MUST select exactly ONE post. Provide the Post ID of your selection.\n\nIf ALL drafts are terrible, still pick the best one \u2014 the Quality Gate will catch truly bad posts.\n\nIf a post has status \"approved\" or \"force_publish\", prioritize it (human override).\n\n---\n\nRESPOND WITH ONLY RAW JSON. No markdown fences.\n\n{\n \"selectedPostId\": \"the Post ID of the chosen draft\",\n \"selectionReason\": \"2-3 sentences explaining why this post was chosen over others\",\n \"dayAlignment\": \"matches\" or \"acceptable\" or \"mismatched\",\n \"angleVariety\": \"good_rotation\" or \"acceptable\" or \"repetitive\",\n \"hookRating\": 1 to 10,\n \"trendFreshness\": \"fresh\" or \"recent\" or \"stale\",\n \"engagementPrediction\": \"high\" or \"medium\" or \"low\",\n \"overallConfidence\": 0.0 to 1.0,\n \"alternatePostId\": \"backup Post ID in case primary is rejected by quality gate\",\n \"alternateReason\": \"Why this is the backup choice\"\n}"
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 2.1
},
{
"id": "e9e97a87-8755-4bfb-819f-22fb0302c9b2",
"name": "\ud83c\udfaf Extract Selection",
"type": "n8n-nodes-base.code",
"position": [
896,
2272
],
"parameters": {
"jsCode": "// Extract AI selection and prepare the chosen post\nconst draftsData = $('\ud83d\udcc4 Has Drafts?').item.json;\nconst rawAiOutput = $json;\n\nlet selectionData = {};\n\nfunction extractJSON(str) { if(typeof str!=='string')return null;let c=str.replace(/```json\\s*/gi,'').replace(/```\\s*/g,'').trim();try{return JSON.parse(c);}catch(e){const m=c.match(/\\{[\\s\\S]*\"selectedPostId\"\\s*:[\\s\\S]*\\}/);if(m){try{return JSON.parse(m[0]);}catch(e2){return null;}}return null;}}\nfunction findAI(obj,d=0){if(d>5||!obj||typeof obj!=='object')return null;if(obj.selectedPostId&&typeof obj.selectedPostId==='string')return obj;const ps=['output','text','message','content','response','result','data','json','kwargs','lc_kwargs'];for(const p of ps){if(obj[p]!==undefined){if(typeof obj[p]==='string'){const r=extractJSON(obj[p]);if(r&&r.selectedPostId)return r;}else if(typeof obj[p]==='object'){const f=findAI(obj[p],d+1);if(f)return f;}}}for(const k of Object.keys(obj)){if(ps.includes(k))continue;const v=obj[k];if(typeof v==='string'&&v.includes('\"selectedPostId\"')){const r=extractJSON(v);if(r&&r.selectedPostId)return r;}else if(typeof v==='object'&&v!==null){const f=findAI(v,d+1);if(f)return f;}}return null;}\n\ntry {\n if (rawAiOutput && rawAiOutput.selectedPostId) selectionData = rawAiOutput;\n else { const f = findAI(rawAiOutput); if (f) selectionData = f; }\n if (!selectionData.selectedPostId) { const s = JSON.stringify(rawAiOutput); const p = extractJSON(s); if (p && p.selectedPostId) selectionData = p; }\n} catch (e) {}\n\n// Fallback: pick the first draft\nif (!selectionData.selectedPostId) {\n selectionData = {\n selectedPostId: draftsData.drafts[0].postId,\n selectionReason: 'AI selector failed to parse \u2014 defaulting to first available draft.',\n dayAlignment: 'unknown',\n hookRating: 5,\n overallConfidence: 0.3,\n alternatePostId: draftsData.drafts.length > 1 ? draftsData.drafts[1].postId : '',\n alternateReason: 'Backup in case primary fails.'\n };\n}\n\n// Find the selected draft\nconst selectedDraft = draftsData.drafts.find(d => d.postId === selectionData.selectedPostId);\n\n// If selected post not found, use first draft\nconst finalDraft = selectedDraft || draftsData.drafts[0];\n\nreturn { json: {\n // Selected post content\n postId: finalDraft.postId,\n fullPost: finalDraft.fullPost,\n hookLine: finalDraft.hookLine,\n hashtags: finalDraft.hashtags,\n angle: finalDraft.angle,\n trendReferenced: finalDraft.trendReferenced,\n wordCount: finalDraft.wordCount,\n postingNotes: finalDraft.postingNotes,\n \n // AI selection metadata\n selectionReason: selectionData.selectionReason || '',\n dayAlignment: selectionData.dayAlignment || '',\n hookRating: selectionData.hookRating || 0,\n engagementPrediction: selectionData.engagementPrediction || '',\n overallConfidence: selectionData.overallConfidence || 0,\n alternatePostId: selectionData.alternatePostId || '',\n alternateReason: selectionData.alternateReason || '',\n \n // Context\n totalDraftsConsidered: draftsData.totalDrafts,\n todayName: draftsData.todayName,\n selectedAt: new Date().toISOString()\n}};"
},
"typeVersion": 2
},
{
"id": "c43512d3-fcc5-4cca-9ba7-38a8609b5dd7",
"name": "\ud83d\udee1\ufe0f AI Quality Gate",
"type": "@n8n/n8n-nodes-langchain.agent",
"position": [
1120,
2272
],
"parameters": {
"text": "=Review this LinkedIn post selected for publishing today.\n\n--- AI SELECTOR CONTEXT ---\nWhy selected: {{ $json.selectionReason }}\nDay alignment: {{ $json.dayAlignment }}\nHook rating: {{ $json.hookRating }}/10\nEngagement prediction: {{ $json.engagementPrediction }}\nConfidence: {{ $json.overallConfidence }}\n\n--- POST DETAILS ---\nAngle: {{ $json.angle }}\nHook: {{ $json.hookLine }}\nTrend: {{ $json.trendReferenced }}\nWord Count: {{ $json.wordCount }}\n\n--- FULL POST ---\n{{ $json.fullPost }}\n\n--- REVIEW NOW ---",
"options": {
"systemMessage": "You are a senior LinkedIn content quality reviewer for: [YOUR NAME]\nBrand voice: [YOUR TONE]\nAudience: [YOUR AUDIENCE]\n\nREVIEW CRITERIA (1-10 each):\n1. HOOK STRENGTH \u2014 Does it stop the scroll? Specific, not generic?\n2. VALUE DENSITY \u2014 Every sentence adds value? No fluff?\n3. AUTHENTICITY \u2014 Sounds human, not AI? Has specifics?\n4. ENGAGEMENT POTENTIAL \u2014 Will people comment/share?\n5. BRAND SAFETY \u2014 Nothing risky or off-brand?\n\nDECISION:\n- approve: Average \u2265 7, no score below 5\n- revise: Average 5-7 OR fixable issues. MUST provide complete revised post.\n- reject: Average < 5 OR brand safety < 5\n\nIF REVISE: Keep same angle/trend. Fix issues. Make MORE human. Improve hook if < 7. Return COMPLETE revised post.\n\nRAW JSON ONLY. No markdown fences.\n{\"decision\":\"approve\",\"scores\":{\"hookStrength\":8,\"valueDensity\":7,\"authenticity\":9,\"engagementPotential\":7,\"brandSafety\":10},\"averageScore\":8.2,\"reviewSummary\":\"2-3 sentences\",\"issuesFound\":[],\"revisedPost\":\"\",\"revisedHookLine\":\"\",\"confidenceLevel\":\"high\",\"publishRecommendation\":\"One sentence\"}"
},
"promptType": "define",
"hasOutputParser": true
},
"typeVersion": 2.1
},
{
"id": "3f282eb2-d920-4c0b-888c-3a30625450aa",
"name": "\ud83c\udfaf Extract Review",
"type": "n8n-nodes-base.code",
"position": [
1920,
2272
],
"parameters": {
"jsCode": "const postData = $('\ud83c\udfaf Extract Selection').item.json;\nconst raw = $json;\nlet rv = {};\n\nfunction ej(s){if(typeof s!=='string')return null;let c=s.replace(/```json\\s*/gi,'').replace(/```\\s*/g,'').trim();try{return JSON.parse(c);}catch(e){const m=c.match(/\\{[\\s\\S]*\"decision\"\\s*:[\\s\\S]*\\}/);if(m){try{return JSON.parse(m[0]);}catch(e2){return null;}}return null;}}\nfunction fa(o,d=0){if(d>5||!o||typeof o!=='object')return null;if(o.decision&&typeof o.decision==='string')return o;const ps=['output','text','message','content','response','result','data','json','kwargs','lc_kwargs'];for(const p of ps){if(o[p]!==undefined){if(typeof o[p]==='string'){const r=ej(o[p]);if(r&&r.decision)return r;}else if(typeof o[p]==='object'){const f=fa(o[p],d+1);if(f)return f;}}}for(const k of Object.keys(o)){if(ps.includes(k))continue;const v=o[k];if(typeof v==='string'&&v.includes('\"decision\"')){const r=ej(v);if(r&&r.decision)return r;}else if(typeof v==='object'&&v!==null){const f=fa(v,d+1);if(f)return f;}}return null;}\ntry{if(raw&&raw.decision)rv=raw;else{const f=fa(raw);if(f)rv=f;}if(!rv.decision){const s=JSON.stringify(raw);const p=ej(s);if(p&&p.decision)rv=p;}}catch(e){}\n\nif(!rv.decision||!['approve','revise','reject'].includes(rv.decision)){\n rv={decision:'approve',scores:{hookStrength:7,valueDensity:7,authenticity:7,engagementPotential:7,brandSafety:8},averageScore:7.2,reviewSummary:'AI review parse failed \u2014 defaulting to approve.',issuesFound:['parse failed'],revisedPost:'',revisedHookLine:'',confidenceLevel:'low',publishRecommendation:'Review manually.'};\n}\n\nlet finalPost=postData.fullPost;\nlet finalHook=postData.hookLine;\nlet wasRevised=false;\n\nif(rv.decision==='revise'&&rv.revisedPost){\n finalPost=rv.revisedPost;\n finalHook=rv.revisedHookLine||rv.revisedPost.split('\\n')[0];\n wasRevised=true;\n}\n\nreturn{json:{\n postId:postData.postId,angle:postData.angle,hashtags:postData.hashtags,trendReferenced:postData.trendReferenced,\n fullPost:finalPost,hookLine:finalHook,wasRevised:wasRevised,\n decision:rv.decision,scores:rv.scores||{},averageScore:rv.averageScore||0,\n reviewSummary:rv.reviewSummary||'',issuesFound:Array.isArray(rv.issuesFound)?rv.issuesFound.join('; '):'',\n confidenceLevel:rv.confidenceLevel||'medium',publishRecommendation:rv.publishRecommendation||'',\n selectionReason:postData.selectionReason,totalDraftsConsidered:postData.totalDraftsConsidered,\n alternatePostId:postData.alternatePostId,\n reviewedAt:new Date().toISOString()\n}};"
},
"typeVersion": 2
},
{
"id": "847a584d-db26-49f3-96c2-843182055170",
"name": "\ud83d\udd00 Review Decision",
"type": "n8n-nodes-base.switch",
"position": [
2080,
2272
],
"parameters": {
"rules": {
"values": [
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.decision }}",
"rightValue": "approve"
}
]
},
"renameOutput": true
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.decision }}",
"rightValue": "revise"
}
]
},
"renameOutput": true
},
{
"conditions": {
"options": {
"version": 2,
"leftValue": "",
"caseSensitive": false,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"operator": {
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.decision }}",
"rightValue": "reject"
}
]
},
"renameOutput": true
}
]
},
"options": {
"allMatchingOutputs": false
}
},
"typeVersion": 3.2
},
{
"id": "7904330d-bbeb-46a8-9b47-df4daaacd0b7",
"name": "\ud83d\udce4 Publish to LinkedIn",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
2528,
2256
],
"parameters": {
"url": "https://api.linkedin.com/v2/ugcPosts",
"method": "POST",
"options": {
"timeout": 30000
},
"jsonBody": "={\n \"author\": \"urn:li:person:YOUR_LINKEDIN_PERSON_ID\",\n \"lifecycleState\": \"PUBLISHED\",\n \"specificContent\": {\n \"com.linkedin.ugc.ShareContent\": {\n \"shareCommentary\": {\n \"text\": {{ JSON.stringify($json.fullPost) }}\n },\n \"shareMediaCategory\": \"NONE\"\n }\n },\n \"visibility\": {\n \"com.linkedin.ugc.MemberNetworkVisibility\": \"PUBLIC\"\n }\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "oAuth2Api",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
},
{
"name": "X-Restli-Protocol-Version",
"value": "2.0.0"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "2b8ea7f7-8be2-4147-924b-ab200d877d5c",
"name": "\ud83d\udcac Hashtags Comment",
"type": "n8n-nodes-base.httpRequest",
"onError": "continueRegularOutput",
"position": [
2752,
2256
],
"parameters": {
"url": "=https://api.linkedin.com/v2/socialActions/{{ encodeURIComponent($json.id) }}/comments",
"method": "POST",
"options": {
"timeout": 15000
},
"jsonBody": "={\n \"actor\": \"urn:li:person:YOUR_LINKEDIN_PERSON_ID\",\n \"message\": {\n \"text\": {{ JSON.stringify($('\ud83c\udfaf Extract Review').item.json.hashtags) }}\n }\n}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"authentication": "genericCredentialType",
"genericAuthType": "oAuth2Api",
"headerParameters": {
"parameters": [
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "e58d4a3e-13a9-4711-bbcf-6c59deba9aa6",
"name": "\u2705 Mark Published",
"type": "n8n-nodes-base.googleSheets",
"position": [
2976,
2368
],
"parameters": {
"columns": {
"value": {
"Status": "={{ $('\ud83c\udfaf Extract Review').item.json.wasRevised ? 'revised_and_published' : 'published' }}",
"Post ID": "={{ $('\ud83c\udfaf Extract Review').item.json.postId }}",
"AI Review": "=Selected: {{ $('\ud83c\udfaf Extract Review').item.json.selectionReason }} | Review: {{ $('\ud83c\udfaf Extract Review').item.json.reviewSummary }} | Score: {{ $('\ud83c\udfaf Extract Review').item.json.averageScore }}",
"LinkedIn URL": "={{ $('\ud83d\udce4 Publish to LinkedIn').item.json.id || 'published' }}",
"Revised Post": "={{ $('\ud83c\udfaf Extract Review').item.json.wasRevised ? $('\ud83c\udfaf Extract Review').item.json.fullPost : '' }}",
"Published Date": "={{ new Date().toISOString() }}"
},
"schema": [
{
"id": "Post ID",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Post ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Published Date",
"type": "string",
"display": true,
"required": false,
"displayName": "Published Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "LinkedIn URL",
"type": "string",
"display": true,
"required": false,
"displayName": "LinkedIn URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "AI Review",
"type": "string",
"display": true,
"required": false,
"displayName": "AI Review",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Revised Post",
"type": "string",
"display": true,
"required": false,
"displayName": "Revised Post",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Post ID"
],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultName": "Post Queue"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.6
},
{
"id": "84d1f3f2-5b8c-4508-81d4-9d8b4c1966ad",
"name": "\ud83d\udcf2 Published \u2705",
"type": "n8n-nodes-base.telegram",
"position": [
2976,
2176
],
"parameters": {
"text": "={{ (() => {\nconst r=$('\ud83c\udfaf Extract Review').item.json;\nconst rev=r.wasRevised?'\\n\u270f\ufe0f *Auto-revised by AI*':'';\nconst sel=r.selectionReason?`\\n\ud83e\udde0 *Why selected:* ${r.selectionReason}`:'';\nreturn `\u2705 *LinkedIn Post Published!*\\n\\n\ud83d\udcdd *Angle:* ${r.angle}\\n\ud83c\udfa3 *Hook:* ${r.hookLine}\\n\ud83d\udcca *Trend:* ${r.trendReferenced}\\n\\n\ud83d\udee1\ufe0f *Quality Score:* ${r.averageScore}/10\\n\ud83d\udcac ${r.reviewSummary}${rev}${sel}\\n\\n\ud83d\udccb *Selected from ${r.totalDraftsConsidered} drafts*\\n\u23f0 ${new Date().toISOString()}`;\n})() }}",
"chatId": "YOUR_TELEGRAM_CHAT_ID",
"additionalFields": {
"parse_mode": "Markdown"
}
},
"typeVersion": 1.2
},
{
"id": "b82b0d8e-4f37-4287-9179-2dc988442031",
"name": "\ud83d\udcbe Save Revised",
"type": "n8n-nodes-base.googleSheets",
"position": [
2304,
2352
],
"parameters": {
"columns": {
"value": {
"Status": "revised_and_published",
"Post ID": "={{ $json.postId }}",
"AI Review": "=REVISED | {{ $json.reviewSummary }} | Score: {{ $json.averageScore }} | Issues: {{ $json.issuesFound }}",
"Full Post": "={{ $json.fullPost }}",
"Hook Line": "={{ $json.hookLine }}"
},
"schema": [
{
"id": "Post ID",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Post ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Full Post",
"type": "string",
"display": true,
"required": false,
"displayName": "Full Post",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Hook Line",
"type": "string",
"display": true,
"required": false,
"displayName": "Hook Line",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "AI Review",
"type": "string",
"display": true,
"required": false,
"displayName": "AI Review",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Post ID"
],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {},
"operation": "appendOrUpdate",
"sheetName": {
"__rl": true,
"mode": "list",
"value": "gid=0",
"cachedResultName": "Post Queue"
},
"documentId": {
"__rl": true,
"mode": "id",
"value": "YOUR_GOOGLE_SHEET_ID"
}
},
"typeVersion": 4.6
},
{
"id": "51003564-ba71-482d-8d7a-b8001acf4e13",
"name": "\u274c Mark Rejected",
"type": "n8n-nodes-base.googleSheets",
"position": [
2304,
2544
],
"parameters": {
"columns": {
"value": {
"Status": "rejected",
"Post ID": "={{ $json.postId }}",
"AI Review": "=REJECTED | {{ $json.reviewSummary }} | Score: {{ $json.averageScore }} | Issues: {{ $json.issuesFound }}"
},
"schema": [
{
"id": "Post ID",
"type": "string",
"display": true,
"removed": false,
"required": false,
"displayName": "Post ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "string",
"display": true,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "AI Review",
"type": "string",
"display": true,
"required": false,
"displayName": "AI Review",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [
"Post ID"
],
"attemptToConvertTypes": false,
"convertFieldsToString": true
},
"options": {},
"operatio
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
This workflow is for solopreneurs, founders, creators, and marketers who want a consistent LinkedIn presence without spending hours writing posts. Ideal for anyone in tech, SaaS, or AI who wants trend-aware content on autopilot.
Source: https://n8n.io/workflows/13465/ — 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 is for beauty salons who want consistent, high‑quality social media content without writing every post manually. It also suits agencies and automation builders who manage multiple beauty
This workflow is designed for marketers, content creators, agencies, and solo founders who want to publish long‑form posts with visuals on autopilot using n8n and AI agents.
Who Is This For?
This cutting-edge n8n workflow is a comprehensive automation solution designed to streamline various Instagram operations. It combines an intelligent AI chatbot for direct message management, automate
This workflow is built for founders, sales teams, solopreneurs, and agencies who want to automate outbound sales without expensive tools. Perfect for anyone doing cold email outreach who wants AI-powe