This workflow corresponds to n8n.io template #17486 — we link there as the canonical source.
This workflow follows the Airtable → Gmail 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": "OTCqy1KVIzoWMKrr",
"meta": {
"templateCredsSetupCompleted": true
},
"name": "LINKEDIN_AI_OPERATIONS_RSS_EDITORIAL_WORKFLOW",
"tags": [],
"nodes": [
{
"id": "3f5937c9-a525-4126-a30d-05d484663fc9",
"name": "When 8am UK Time",
"type": "n8n-nodes-base.scheduleTrigger",
"position": [
19216,
4656
],
"parameters": {
"rule": {
"interval": [
{
"triggerAtHour": 8
}
]
}
},
"retryOnFail": true,
"typeVersion": 1.2
},
{
"id": "4d441ecb-a0c2-48b8-9b85-c7e49db06981",
"name": "Fetch TechCrunch AI Feed",
"type": "n8n-nodes-base.rssFeedRead",
"position": [
19440,
4560
],
"parameters": {
"url": "https://techcrunch.com/category/artificial-intelligence/feed/",
"options": {}
},
"typeVersion": 1.2
},
{
"id": "223c3fd3-df51-4306-9f03-cd53941d97d2",
"name": "Fetch The Verge AI Feed",
"type": "n8n-nodes-base.rssFeedRead",
"position": [
19440,
4752
],
"parameters": {
"url": "https://www.theverge.com/rss/ai-artificial-intelligence/index.xml",
"options": {}
},
"typeVersion": 1.2
},
{
"id": "c285959c-a378-4259-a09d-20a6b20c8d3e",
"name": "Fetch OpenAI Blog Feed",
"type": "n8n-nodes-base.rssFeedRead",
"position": [
19440,
4944
],
"parameters": {
"url": "https://openai.com/news/rss.xml",
"options": {}
},
"typeVersion": 1.2
},
{
"id": "37613cfa-e9cd-4202-b9cb-0009dc023ba7",
"name": "Merge RSS Feeds",
"type": "n8n-nodes-base.merge",
"position": [
19664,
4736
],
"parameters": {
"numberInputs": 3
},
"typeVersion": 3.2
},
{
"id": "32a9ba4e-41dc-4d97-9035-d204884ced39",
"name": "Clean RSS Signals",
"type": "n8n-nodes-base.code",
"position": [
19888,
4752
],
"parameters": {
"jsCode": "const items = $input.all(); function clean(value) { return String(value || '') .replace(/<[^>]+>/g, ' ') .replace(/ /g, ' ') .replace(/’/g, \"'\") .replace(/“/g, '\"') .replace(/”/g, '\"') .replace(/…/g, '...') .replace(/&/g, '&') .replace(/\\s+/g, ' ') .trim();\n} function getLink(j) { return String(j.link || j.guid || j.id || j.url || '').trim();\n} function detectSource(j) { const link = getLink(j).toLowerCase(); if (link.includes('techcrunch.com')) return 'TechCrunch'; if (link.includes('theverge.com')) return 'The Verge'; if (link.includes('openai.com')) return 'OpenAI Blog'; return clean( j.feedTitle || j.source || j.creator || j['dc:creator'] || j.author || 'RSS Feed' );\n} function getPublishedAt(j) { const dateRaw = j.isoDate || j.pubDate || j.date || j.published || ''; if (!dateRaw) return ''; const parsedDate = new Date(dateRaw); if (isNaN(parsedDate.getTime())) return ''; return parsedDate.toISOString();\n} const rows = items .map((item) => { const j = item.json || {}; const title = clean(j.title); const link = getLink(j); const source = detectSource(j); const published_at = getPublishedAt(j); const summary = clean( j.contentSnippet || j.content || j.description || j.summary || '' ); return { title, link, source, published_at, summary }; }) .filter((row) => row.title && row.link); if (!rows.length) { throw new Error( `No valid RSS articles found. Input items received: ${items.length}. First item: ` + JSON.stringify(items[0]?.json || {}).slice(0, 1000) );\n} const seen = new Set();\nconst deduped = []; for (const row of rows) { const key = row.link.toLowerCase(); if (!seen.has(key)) { seen.add(key); deduped.push(row); }\n} const grouped = {}; for (const article of deduped) { if (!grouped[article.source]) { grouped[article.source] = []; } grouped[article.source].push(article);\n} for (const source of Object.keys(grouped)) { grouped[source].sort((a, b) => { const dateA = a.published_at ? new Date(a.published_at).getTime() : 0; const dateB = b.published_at ? new Date(b.published_at).getTime() : 0; return dateB - dateA; });\n} const sourceOrder = ['TechCrunch', 'The Verge', 'OpenAI Blog'];\nconst balanced = []; for (const source of sourceOrder) { const sourceArticles = grouped[source] || []; balanced.push(...sourceArticles.slice(0, 5));\n} for (const source of Object.keys(grouped)) { if (!sourceOrder.includes(source)) { balanced.push(...grouped[source].slice(0, 3)); }\n} const topArticles = balanced .filter(Boolean) .slice(0, 15); if (!topArticles.length) { throw new Error('No articles remained after source balancing.');\n} const sourceCounts = topArticles.reduce((acc, article) => { acc[article.source] = (acc[article.source] || 0) + 1; return acc;\n}, {}); return [ { json: { run_id: `linkedin-${new Date().toISOString()}`, created_at: new Date().toISOString(), total_input_items: items.length, valid_article_count: rows.length, article_count: topArticles.length, source_counts: sourceCounts, rss_signals: topArticles, rss_signals_text: topArticles .map((r, index) => `${index + 1}. ${r.title}\nSource: ${r.source}\nDate: ${r.published_at || 'Unknown'}\nURL: ${r.link}\nSummary: ${r.summary}`) .join('\\n\\n') } }\n];"
},
"typeVersion": 2
},
{
"id": "d32e1430-bf53-4279-a71f-df014c361bc2",
"name": "OpenAI Signal Editor",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
20272,
4560
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-5",
"cachedResultName": "GPT-5"
},
"options": {},
"responses": {
"values": [
{
"role": "system",
"content": "You are an AI Signal Editor for a business operations content workflow. Your job is NOT to write the LinkedIn post and NOT to design the final image pixel by pixel. Your job is to choose the single strongest AI/business signal, convert it into an operational point of view for the organisation, and hand off a structured content pack that a copy editor and a deterministic card renderer will both consume. the organisation This workflow helps organisations find practical AI opportunities by reviewing real workflows, admin load, handovers, reporting, customer updates, quoting, approvals and repeated tasks. The editorial category is:\nAI Operations Review. The source article is evidence.\nThe operational problem is the story.\nThe artifact is the hook. Choose stories that let the organisation teach a practical lesson about how work actually gets done inside a business. The best selected signal should support at least one of these the organisation themes: 1. Most businesses do not have an AI problem. They have a workflow problem.\n2. AI works best when pointed at repeated work, handoffs, reporting, admin and decisions.\n3. Start with the workflow before choosing the tool.\n4. Hidden admin quietly removes margin and attention.\n5. AI Operations Reviews should produce maps, heatmaps, checklists and roadmaps, not generic AI strategy slides. Selection score out of 10: 3 points: clear operational pain or workflow friction\n2.5 points: relevant to SMEs, mid-market firms, print firms, service businesses or operations teams\n2 points: can become a clear visual artifact\n1.5 points: reinforces the organisation's AI Operations Review positioning\n1 point: fresh, credible and source-backed Prioritise: * enterprise AI deployment\n* workforce AI adoption\n* AI embedded into business processes\n* customer service execution\n* reporting and decision support\n* knowledge work execution\n* process redesign\n* task orchestration\n* business operations\n* measurable operating outcomes\n* operations-heavy sectors\n* commercial print, fulfilment, field service, sales admin, finance admin and customer operations Hard exclusions unless there is a clear operating model lesson: * cybersecurity\n* software vulnerabilities\n* bug fixing\n* open source maintenance\n* AI safety\n* regulation\n* compliance\n* research papers\n* benchmarks\n* coding assistants\n* developer tooling\n* infrastructure tooling\n* model performance improvements\n* consumer chatbots\n* voice assistants\n* localisation\n* layoffs\n* culture-war AI stories Before selecting an article, ask: Would a COO, Managing Director or Operations Director care because it helps them see where work is slow, manual, duplicated, unclear or expensive? If no, skip. VISUAL FORMAT SELECTION In addition to the written signal pack, you must choose exactly one visual_format for the accompanying LinkedIn image. Pick whichever format best fits the specific lesson in this story. Do not default to the same format every time. - checklist: use when the story produces three to five warning signs, actions or diagnostic questions.\n- strong_statement: use when one short insight is the strongest message and detail would dilute it.\n- before_after: use only when the source genuinely supports a clear current-state versus improved-state comparison.\n- process_breakdown: use when the operational issue consists of three or four identifiable workflow stages.\n- mini_framework: use when the post teaches a reusable method or diagnostic with four parts.\n- friction_snapshot: use when the story exposes several distinct sources of wasted time, margin or attention around one central issue. The visual must communicate the specific lesson from the selected article. It must never default to a generic six-box workflow diagram. VISUAL COPY LIMITS (enforce these exactly) - visual_eyebrow: maximum 30 characters, short label like \"OPERATIONAL AI REVIEW\" or a topic tag\n- visual_headline: maximum 10 words, the single sharpest line from the story\n- visual_subheadline: maximum 18 words, one supporting sentence, optional (empty string if not needed)\n- visual_points: an array of three to five objects, each with icon (one simple unicode symbol or emoji), title (maximum 4 words) and description (maximum 10 words)\n- visual_footer: maximum 12 words, a small closing line such as a source credit or the the configured brand name Return ONLY valid JSON.\nNo markdown.\nNo explanation.\nNo code fences."
},
{
"content": "=Choose the single strongest operational AI signal from the RSS articles below. Your job is to create the source pack for a the organisation LinkedIn post AND the structured content for its editorial card image. Do not write the final post. Do not write the final image copy word for word, just structured building blocks. Choose the article that best supports this editorial angle: Most businesses do not need more AI commentary. They need to find where workflows, handovers, admin, reporting, approvals and repeated tasks are slowing the business down. Selection test: Would this signal help the organisation explain an operational problem a business leader recognises inside their own company? If no article scores 8 or above, return skip. Return exactly this JSON shape: {\n\"signal_headline\": \"\",\n\"signal_source\": \"\",\n\"signal_date\": \"\",\n\"signal_url\": \"\",\n\"topic_tag\": \"\",\n\"signal_summary\": \"\",\n\"business_implication\": \"\",\n\"operational_pain\": \"\",\n\"organisation_angle\": \"\",\n\"artifact_type\": \"\",\n\"quality_score\": 0,\n\"publish_decision\": \"\",\n\"cta\": \"\",\n\"post_heading\": \"\",\n\"bullet_points\": [ { \"icon\": \"\", \"text\": \"\" }\n],\n\"recommended_hashtags\": [],\n\"visual_format\": \"\",\n\"visual_eyebrow\": \"\",\n\"visual_headline\": \"\",\n\"visual_subheadline\": \"\",\n\"visual_points\": [ { \"icon\": \"\", \"title\": \"\", \"description\": \"\" }\n],\n\"visual_footer\": \"\"\n} Field rules: signal_summary:\nMaximum 60 words. Purely factual. Describe what happened. business_implication:\nMaximum 80 words. Explain what this means for SMEs, mid-market firms, operations leaders or transformation teams. Focus on how work gets done. operational_pain:\nOne plain-English sentence naming the hidden operational issue exposed by the signal. organisation_angle:\nOne plain-English sentence linking the signal to AI Operations Reviews. artifact_type:\nA short label describing the operational artifact this story maps to, e.g. Reporting Burden Map, Approval Chain Map, Manual Work Heatmap, Margin Leakage Map, 30-Day AI Review Roadmap, Customer Update Flow. Free text, but keep it short and specific to this story. post_heading:\nA short series-style heading for the LinkedIn post, starting with one relevant emoji, e.g. \"\ud83d\udca1 AI Operations Review: Reporting Burden\". Maximum 8 words after the emoji. bullet_points:\nThree to five objects. Each icon is one relevant business symbol or emoji (vary them, do not repeat the same icon across points). Each text is a short, concrete operational fact or pain point, maximum 12 words, no trailing punctuation issues. recommended_hashtags:\nZero to two hashtags, only if genuinely relevant to this specific story. Do not include generic hashtags on every run. Return an empty array if none fit well. Do not include the # symbol, just the word. visual_format:\nExactly one of: checklist, strong_statement, before_after, process_breakdown, mini_framework, friction_snapshot. Choose based on the rules in your instructions. visual_eyebrow, visual_headline, visual_subheadline, visual_points, visual_footer:\nFollow the exact character/word limits in your instructions. visual_points must contain three to five items. cta:\nUse a practical CTA. Prefer one of:\nComment REVIEW if you want the AI Operations Review checklist.\nComment PRINT if you want the print operations AI checklist.\nWhich workflow inside your business would you map first?\nWhere is admin quietly slowing your team down? quality_score:\nNumber from 1 to 10 using the scoring rules. publish_decision:\npublish only if quality_score is 8 or above.\nskip otherwise. If no article scores 8 or above, return: {\n\"signal_headline\": \"\",\n\"signal_source\": \"\",\n\"signal_date\": \"\",\n\"signal_url\": \"\",\n\"topic_tag\": \"\",\n\"signal_summary\": \"\",\n\"business_implication\": \"\",\n\"operational_pain\": \"\",\n\"organisation_angle\": \"\",\n\"artifact_type\": \"\",\n\"quality_score\": 1,\n\"publish_decision\": \"skip\",\n\"cta\": \"\",\n\"post_heading\": \"\",\n\"bullet_points\": [],\n\"recommended_hashtags\": [],\n\"visual_format\": \"\",\n\"visual_eyebrow\": \"\",\n\"visual_headline\": \"\",\n\"visual_subheadline\": \"\",\n\"visual_points\": [],\n\"visual_footer\": \"\"\n} RSS ARTICLES: {{$json.rss_signals_text}}"
}
]
},
"builtInTools": {}
},
"typeVersion": 2.3
},
{
"id": "5f819afe-771a-4f10-ad08-15eea0aee5b9",
"name": "Parse Signal JSON",
"type": "n8n-nodes-base.code",
"position": [
20624,
4560
],
"parameters": {
"jsCode": "const input = $input.first().json || {}; const raw = input.output?.[0]?.content?.find(c => c.type === 'output_text')?.text || input.output?.[0]?.content?.[0]?.text || input.text || input.message || ''; if (!raw) { throw new Error( 'Missing AI Signal Editor output. Found: ' + JSON.stringify(input).slice(0, 1500) );\n} let cleaned = String(raw) .replace(/```json/gi, '') .replace(/```/g, '') .trim(); let parsed; try { parsed = JSON.parse(cleaned);\n} catch (error) { throw new Error( 'Failed to parse AI Signal Editor JSON. Raw output: ' + cleaned.slice(0, 1500) );\n} const requiredFields = [ 'signal_headline', 'signal_source', 'signal_date', 'signal_url', 'topic_tag', 'signal_summary', 'business_implication', 'operational_pain', 'organisation_angle', 'artifact_type', 'quality_score', 'publish_decision', 'cta', 'post_heading', 'recommended_hashtags', 'visual_format', 'visual_eyebrow', 'visual_headline', 'visual_subheadline', 'visual_footer'\n]; for (const field of requiredFields) { if (parsed[field] === undefined || parsed[field] === null) { parsed[field] = ''; }\n} if (!Array.isArray(parsed.bullet_points)) parsed.bullet_points = [];\nif (!Array.isArray(parsed.recommended_hashtags)) parsed.recommended_hashtags = [];\nif (!Array.isArray(parsed.visual_points)) parsed.visual_points = []; parsed.quality_score = Number(parsed.quality_score || 0);\nparsed.publish_decision = String(parsed.publish_decision || '').toLowerCase().trim(); if (!['publish', 'skip'].includes(parsed.publish_decision)) { parsed.publish_decision = parsed.quality_score >= 8 ? 'publish' : 'skip';\n} const VALID_VISUAL_FORMATS = ['checklist', 'strong_statement', 'before_after', 'process_breakdown', 'mini_framework', 'friction_snapshot']; if (parsed.publish_decision === 'publish') { if (!parsed.signal_headline) throw new Error('Missing signal_headline.'); if (!parsed.signal_url) throw new Error('Missing signal_url.'); if (!parsed.business_implication) throw new Error('Missing business_implication.'); if (!parsed.operational_pain) throw new Error('Missing operational_pain.'); if (!parsed.organisation_angle) throw new Error('Missing organisation_angle.'); if (!parsed.artifact_type) throw new Error('Missing artifact_type.'); if (!parsed.post_heading) throw new Error('Missing post_heading.'); if (parsed.bullet_points.length < 3 || parsed.bullet_points.length > 5) { throw new Error(`bullet_points must contain 3 to 5 items. Received ${parsed.bullet_points.length}.`); } // AUDIT FIX (Check 5): validate each bullet_points entry has non-empty icon + text, not just // the array length. A missing field here would previously pass silently and only surface as // a thin/empty bullet line in the final post. parsed.bullet_points.forEach((bp, i) => { if (!bp || typeof bp !== 'object' || !String(bp.icon || '').trim() || !String(bp.text || '').trim()) { throw new Error(`bullet_points[${i}] is missing a non-empty icon or text field.`); } }); if (!VALID_VISUAL_FORMATS.includes(parsed.visual_format)) { throw new Error(`Invalid visual_format \"${parsed.visual_format}\". Must be one of ${VALID_VISUAL_FORMATS.join(', ')}.`); } if (!parsed.visual_headline) throw new Error('Missing visual_headline.'); if (parsed.visual_points.length < 3 || parsed.visual_points.length > 5) { throw new Error(`visual_points must contain 3 to 5 items. Received ${parsed.visual_points.length}.`); } // AUDIT FIX (Check 5): same per-item validation for visual_points (icon + title + description). parsed.visual_points.forEach((vp, i) => { if (!vp || typeof vp !== 'object' || !String(vp.icon || '').trim() || !String(vp.title || '').trim() || !String(vp.description || '').trim()) { throw new Error(`visual_points[${i}] is missing a non-empty icon, title, or description field.`); } }); parsed.recommended_hashtags = parsed.recommended_hashtags.slice(0, 2);\n} return [ { json: { ...parsed, run_id: `linkedin-${new Date().toISOString()}`, created_at: new Date().toISOString() } }\n];"
},
"typeVersion": 2
},
{
"id": "4863c468-4d56-4ce7-9c00-c34a65f0d61d",
"name": "If Publishable Signal",
"type": "n8n-nodes-base.if",
"position": [
20848,
4560
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "eb7dbf04-8c0a-4fca-b7b9-eeb6699086da",
"operator": {
"name": "filter.operator.equals",
"type": "string",
"operation": "equals"
},
"leftValue": "={{ $json.publish_decision }}",
"rightValue": "publish"
},
{
"id": "604f71e3-d815-4049-88a3-edddc14246ab",
"operator": {
"type": "number",
"operation": "gte"
},
"leftValue": "={{ Number($json.quality_score) }}",
"rightValue": 8
}
]
}
},
"typeVersion": 2.3
},
{
"id": "cb40e692-af51-46b9-92db-6d9f3729f050",
"name": "Parse Publish Payload",
"type": "n8n-nodes-base.code",
"position": [
21232,
4560
],
"parameters": {
"jsCode": "const source = $input.first().json; return [ { json: { ...source, run_id: source.run_id || `linkedin-${new Date().toISOString()}`, created_at: source.created_at || new Date().toISOString() } }\n];"
},
"typeVersion": 2
},
{
"id": "6b28a2fd-df4b-4408-aa40-82eedf5182a8",
"name": "OpenAI Refine Post",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
21456,
4560
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-5",
"cachedResultName": "GPT-5"
},
"options": {},
"responses": {
"values": [
{
"role": "system",
"content": "You are a LinkedIn editor for a business operations content workflow. Your job is not to summarise AI news. Your job is to turn one source signal into a practical, scannable the organisation point of view, formatted like a strong educational LinkedIn post. the organisation This workflow helps organisations find practical AI opportunities by reviewing real workflows, admin load, handovers, reporting, customer updates, quoting, approvals and repeated tasks. Core positioning:\nAI Operations Review. Core belief:\nMost businesses do not have an AI problem. They have a workflow problem. The post should make the reader think:\nThat problem exists in my business. The article is evidence.\nThe operational problem is the lead.\nThe practical lesson is the value. Audience:\nSME owners, Managing Directors, COOs, operations leaders, transformation leaders and service business founders. REQUIRED STRUCTURE (each item below is its own line or short block, separated by one blank line): 1. Short heading on its own line. This should reuse or lightly polish the provided post_heading. It should start with one relevant emoji.\n2. Blank line.\n3. Strong opening statement naming the operational pain in one or two sentences. Do not open with the source or vendor.\n4. Blank line.\n5. A short explanation (one to three sentences) of the hidden work causing that pain.\n6. Blank line.\n7. Three to five practical points, one per line, each beginning with a relevant business emoji or symbol. Base these on the provided bullet_points but you may tighten the wording. Do not repeat the same emoji across every line.\n8. Blank line.\n9. One short paragraph using the selected source as supporting evidence. Name the source in plain language, e.g. \"OpenAI's sales example shows...\" Do not lead with this, use it as proof.\n10. Blank line.\n11. One practical the organisation operating lesson, one to two sentences.\n12. Blank line.\n13. One closing question or one CTA, not both. Prefer the supplied cta where it fits naturally. The final post must be 110 to 180 words before hashtags. COPY RULES - Plain English. Short paragraphs.\n- Use three to five relevant emojis or Unicode icons in the bullet section only.\n- Emojis must help the reader scan the post, not decorate it.\n- Do not begin every post with the same emoji as the last few runs; vary the opening emoji naturally.\n- Do not overuse emojis outside the bullet list.\n- Do not place emojis at the end of every sentence.\n- Avoid childish or decorative emojis.\n- Prefer business-relevant symbols such as: insight, intake, drafting, repetition, delay, review, approval, reporting, ownership, risk, customer updates, handoffs, information, diagnosis, process improvement.\n- No markdown bullets such as asterisks or dashes. Use the emoji itself as the bullet marker.\n- Do not use labels such as \"Problem:\", \"Proof:\" or \"Lesson:\".\n- No em dashes. No ellipses. No unfinished sentences.\n- No hype or generic AI commentary.\n- Never start with: TechCrunch reports, The Verge reports, OpenAI announced, According to, New AI tools are, In today's fast-changing world.\n- Never use: game changer, revolutionary, transformative, unlock, leverage, seamless, powerful, operational efficiency, future proof, next generation, businesses are increasingly, AI is changing everything, faster than most realise.\n- The source must be used as evidence, not as the main story.\n- Mention the organisation or AI Operations Review naturally, not in every paragraph.\n- End with one clear question or one CTA, not both.\n- Do not append hashtags yourself, hashtags are handled separately.\n- A post must never contain the word \"undefined\". Return only the final LinkedIn post, formatted with the blank lines described above. If the story should be rejected, return exactly:\nSKIP"
},
{
"content": "=Write a LinkedIn post for a company page using the structure and rules in your instructions. Use only this source context. Post heading (reuse or lightly polish):\n{{ $json.post_heading }} Bullet points (reuse or lightly tighten, keep 3 to 5, one per line, each with its own emoji):\n{{ JSON.stringify($json.bullet_points) }} Headline:\n{{ $json.signal_headline }} Source:\n{{ $json.signal_source }} URL:\n{{ $json.signal_url }} Signal summary:\n{{ $json.signal_summary }} Business implication:\n{{ $json.business_implication }} Operational pain:\n{{ $json.operational_pain }} the organisation angle:\n{{ $json.organisation_angle }} CTA:\n{{ $json.cta }} Write a post that:\n- opens with a short heading line starting with one emoji\n- follows with the operational pain as the opening statement\n- explains the hidden work causing it\n- lists three to five emoji-led practical points\n- uses the source as evidence, not the lead\n- gives one practical the organisation operating lesson\n- ends with one clear question or CTA, not both The final post must be 110 to 180 words before hashtags.\nDo not use markdown bullets.\nDo not use em dashes.\nDo not use ellipses.\nDo not trail off. Return only the final LinkedIn post."
}
]
},
"builtInTools": {}
},
"typeVersion": 2.3
},
{
"id": "3808b018-d27f-45ee-9732-6f5b5c191741",
"name": "Normalize Post Content",
"type": "n8n-nodes-base.code",
"position": [
21808,
4560
],
"parameters": {
"jsCode": "const source = $input.first().json; const postText = source.output?.[0]?.content?.find(c => c.type === 'output_text')?.text || source.output?.[0]?.content?.[0]?.text || ''; if (!postText) { throw new Error( 'Missing refined post text. Found: ' + JSON.stringify(source).slice(0, 1000) );\n} return [ { json: { ...source, post_text: postText } }\n];"
},
"typeVersion": 2
},
{
"id": "006bccef-fb23-437e-88f7-7b99d9eeaa90",
"name": "OpenAI Post QA",
"type": "@n8n/n8n-nodes-langchain.openAi",
"position": [
22032,
4560
],
"parameters": {
"modelId": {
"__rl": true,
"mode": "list",
"value": "gpt-5",
"cachedResultName": "GPT-5"
},
"options": {},
"responses": {
"values": [
{
"role": "system",
"content": "You are the final the organisation LinkedIn copy QA editor. Your job is to improve the draft only if needed. The final post must be 110 to 180 words before hashtags. Required structure, each part separated by a blank line:\n1. Short heading line starting with one emoji\n2. Strong opening statement naming the operational pain\n3. Short explanation of the hidden work causing it\n4. Three to five practical points, one per line, each starting with a relevant emoji\n5. One short paragraph using the source as evidence\n6. One practical the organisation lesson\n7. One closing question or one CTA, not both Checks:\n- opens with a heading line, not the news source\n- opening statement names the operational pain, not the announcement\n- source is used as evidence, not the lead\n- three to five emoji-led points, each on its own line, using varied emojis\n- one practical lesson\n- one clean closing question or CTA, never both\n- no hashtags in the body\n- no markdown bullets (asterisks or dashes)\n- no labels like \"Problem:\", \"Proof:\" or \"Lesson:\"\n- no em dashes\n- no ellipses\n- no hype\n- no unfinished sentence\n- no generic AI commentary\n- no repeated ideas\n- never contains the word \"undefined\" Style:\n- plain English\n- short paragraphs\n- sharp, not explanatory\n- sentence fragments are allowed when they improve clarity The final post must be complete and must not end with an unfinished sentence. Never end with:\n...\nand\nor\nthen\nwhere\nto\nat\nwith\nfor\nincluding Do not use ellipses. This is live production content for a LinkedIn company page. Rewrite once if the draft fails any check.\nPreserve the factual source reference.\nPreserve the blank-line structure and the emoji-led points.\nReturn only the final post text.\nIf the draft says SKIP, return exactly SKIP."
},
{
"content": "=QA and tighten this the organisation LinkedIn post. Draft:\n{{ $json.post_text }} Source headline:\n{{ $('Parse Publish Payload').first().json.signal_headline }} Source:\n{{ $('Parse Publish Payload').first().json.signal_source }} Operational pain:\n{{ $('Parse Publish Payload').first().json.operational_pain }} the organisation angle:\n{{ $('Parse Publish Payload').first().json.organisation_angle }} Rules:\n- 110 to 180 words before hashtags\n- plain English\n- no hashtags in the body\n- no em dashes\n- no ellipses\n- no markdown bullets\n- keep the heading line and the three to five emoji-led points\n- end with one practical question or one CTA, not both\n- no dangling sentence\n- remove over-explaining The final post must be complete and must not end with an unfinished sentence. Never end with:\n...\nand\nor\nthen\nwhere\nto\nat\nwith\nfor\nincluding Return only the improved post."
}
]
},
"builtInTools": {}
},
"typeVersion": 2.3
},
{
"id": "e6104308-3ae5-4223-93fe-d9c76f4fc880",
"name": "Parse Post Details",
"type": "n8n-nodes-base.code",
"position": [
22384,
4560
],
"parameters": {
"jsCode": "const refinerOutput = $input.first().json;\nconst source = $('Parse Publish Payload').first().json; const refined = refinerOutput.output?.[0]?.content?.find(c => c.type === 'output_text')?.text || refinerOutput.output?.[0]?.content?.[0]?.text || refinerOutput.text || ''; if (!refined) { throw new Error( 'No refined post text found. Refiner output: ' + JSON.stringify(refinerOutput).slice(0, 1500) );\n} const banned = [ 'game changer', 'revolutionise', 'revolutionize', 'leverage', 'leveraging', 'unlock', 'transformative', 'revolutionary', 'cutting edge', 'seamless', 'powerful', 'this advancement', 'this update helps', 'operational efficiency', 'synergy', 'paradigm', 'cutting-edge'\n]; let cleaned = refined.trim(); for (const phrase of banned) { cleaned = cleaned.replace(new RegExp(phrase, 'gi'), '');\n} cleaned = cleaned .replace(/\\n{3,}/g, '\\n\\n') .replace(/[ \\t]{2,}/g, ' ') .trim(); if (cleaned.includes('undefined')) { throw new Error('BLOCKED: refined post contains undefined.');\n} if (cleaned.length < 80) { throw new Error('BLOCKED: refined post is too short.');\n} // AUDIT FIX (Check 5): the only length check previously was a character-count floor, which does\n// not validate the intended 110-180 word target at all. This adds an actual word count and hard\n// outer bounds (well outside the target range) that catch clearly broken output \u2014 truncated,\n// runaway, or near-empty \u2014 without over-blocking normal LLM stylistic variance around the target.\nconst wordCount = cleaned.split(/\\s+/).filter(Boolean).length; if (wordCount < 60) { throw new Error(`BLOCKED: refined post is only ${wordCount} words, well under the 110-180 word target.`);\n} if (wordCount > 260) { throw new Error(`BLOCKED: refined post is ${wordCount} words, well over the 110-180 word target.`);\n} return [ { json: { ...source, post_text: cleaned, refined_post: cleaned, original_post_text: source.post_text, status: source.status || 'Approved', error_log: source.error_log || '', word_count: wordCount } }\n];"
},
"typeVersion": 2
},
{
"id": "9c04e1a1-6ae8-415a-b2f6-0d76677b25cc",
"name": "Extract Hashtags",
"type": "n8n-nodes-base.code",
"position": [
22608,
4560
],
"parameters": {
"jsCode": "const raw = Array.isArray($json.recommended_hashtags) ? $json.recommended_hashtags : []; const cleaned = [...new Set( raw .map(h => String(h || '').trim()) .filter(Boolean) .map(h => (h.startsWith('#') ? h : `#${h}`))\n)].slice(0, 2); return [{ json: { ...$json, hashtags: cleaned.join(' ') }\n}];"
},
"typeVersion": 2
},
{
"id": "99a50cc5-186c-4a05-aa3c-3c51622d251d",
"name": "Evaluate Hashtags",
"type": "n8n-nodes-base.code",
"position": [
22832,
4560
],
"parameters": {
"jsCode": "return [$input.all()[0]];"
},
"typeVersion": 2
},
{
"id": "91e7a587-8a59-4c09-825e-d435d160e269",
"name": "Airtable Audit Record",
"type": "n8n-nodes-base.airtable",
"position": [
19008,
5424
],
"parameters": {
"base": {
"__rl": true,
"mode": "id",
"value": "={{ $env.AIRTABLE_BASE_ID }}"
},
"table": {
"__rl": true,
"mode": "id",
"value": "={{ $env.AIRTABLE_TABLE_ID }}"
},
"columns": {
"value": {
"id": "={{ $json.run_id }}",
"CTA": "={{ $json.cta || '' }}",
"Hook": "={{ ($json.post_text || '').split('\\n')[0] }}",
"Topic": "={{ $json.signal_headline }}",
"Run_ID": "={{ $json.run_id }}",
"Status": "={{ $json.status || 'Approved' }}",
"Message": "={{ $json.post_text }}",
"Hashtags": "={{ $json.hashtags || '' }}",
"Error_Log": "={{ $json.error_log || '' }}",
"Created_At": "={{ $json.created_at }}",
"Draft_Copy": "={{ $json.post_text }}",
"Source_URL": "={{ $json.signal_url }}",
"Trend_Score": "={{ Number($json.quality_score || 0) }}",
"Post_Variant": "Opinion-Led",
"Visual_Prompt": "={{ ($json.visual_format ? $json.visual_format + ': ' : '') + ($json.visual_headline || '') }}",
"Content_Pillar": "SMB Automation",
"Carousel_Outline": "={{ 'Artifact: ' + ($json.artifact_type || '') + ' | Pain: ' + ($json.operational_pain || '') + ' | Angle: ' + ($json.organisation_angle || '') }}"
},
"schema": [
{
"id": "id",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "id",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Topic",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Topic",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Source_URL",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Source_URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Trend_Score",
"type": "number",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Trend_Score",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Content_Pillar",
"type": "options",
"display": true,
"options": [
{
"name": "Education",
"value": "Education"
},
{
"name": "Thought Leadership",
"value": "Thought Leadership"
},
{
"name": "Case Study",
"value": "Case Study"
},
{
"name": "Opinion",
"value": "Opinion"
},
{
"name": "SMB Automation",
"value": "SMB Automation"
},
{
"name": "AI Agents",
"value": "AI Agents"
},
{
"name": "n8n Automation",
"value": "n8n Automation"
},
{
"name": "Marketing Operations",
"value": "Marketing Operations"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Content_Pillar",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Post_Variant",
"type": "options",
"display": true,
"options": [
{
"name": "Educational",
"value": "Educational"
},
{
"name": "Opinion-Led",
"value": "Opinion-Led"
},
{
"name": "Case-Study",
"value": "Case-Study"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Post_Variant",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Draft_Copy",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Draft_Copy",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Hook",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Hook",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "CTA",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "CTA",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Hashtags",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Hashtags",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Visual_Prompt",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Visual_Prompt",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Carousel_Outline",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Carousel_Outline",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Status",
"type": "options",
"display": true,
"options": [
{
"name": "Draft",
"value": "Draft"
},
{
"name": "Review",
"value": "Review"
},
{
"name": "Approved",
"value": "Approved"
},
{
"name": "Posted",
"value": "Posted"
},
{
"name": "Rejected",
"value": "Rejected"
},
{
"name": "Failed_Post",
"value": "Failed_Post"
},
{
"name": "No_Posts",
"value": "No_Posts"
},
{
"name": "Skipped",
"value": "Skipped"
}
],
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Status",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Scheduled_Date",
"type": "dateTime",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Scheduled_Date",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Post_URL",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Post_URL",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Error_Log",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Error_Log",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Run_ID",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Run_ID",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Created_At",
"type": "dateTime",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Created_At",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Message",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Message",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "LinkedIn_URN",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "LinkedIn_URN",
"defaultMatch": false,
"canBeUsedToMatch": true
},
{
"id": "Image_URL",
"type": "string",
"display": true,
"removed": false,
"readOnly": false,
"required": false,
"displayName": "Image_URL",
"defaultMatch": false,
"canBeUsedToMatch": true
}
],
"mappingMode": "defineBelow",
"matchingColumns": [],
"attemptToConvertTypes": false,
"convertFieldsToString": false
},
"options": {
"typecast": true
},
"operation": "create"
},
"typeVersion": 2.2
},
{
"id": "b3fb7683-c907-4f57-86a6-00484fabf81c",
"name": "Reattach Content Metadata",
"type": "n8n-nodes-base.code",
"position": [
19232,
5424
],
"parameters": {
"jsCode": "const airtable = $input.first().json || {};\nconst content = $('Parse Post Details').first().json || {};\nconst fields = airtable.fields || {};\nreturn [{ json: { ...content, airtable_record_id: airtable.id || airtable.recordId || '', airtable_fields: fields, airtable_created_time: airtable.createdTime || '' }\n}];"
},
"typeVersion": 2
},
{
"id": "1d4d5c02-910f-4ae2-b3fe-7dfe03f4929f",
"name": "Create Editorial Card HTML",
"type": "n8n-nodes-base.code",
"position": [
19456,
5520
],
"parameters": {
"jsCode": "const content = $input.first().json || {};\nconst brandName = String($env.BRAND_NAME || 'YOUR BRAND').trim(); const VALID_FORMATS = [ 'checklist', 'strong_statement', 'before_after', 'process_breakdown', 'mini_framework', 'friction_snapshot'\n]; const format = VALID_FORMATS.includes(content.visual_format) ? content.visual_format : 'friction_snapshot'; const headline = String( content.visual_headline || content.signal_headline || 'Where is work slowing you down?'\n).trim(); const posterPromptByFormat = { checklist: 'a clean vertical checklist poster with a bold headline and four or five visual rows using icons and abstract shapes only', strong_statement: 'a bold centred statement poster with one large headline, generous negative space and one simple abstract supporting shape', before_after: 'a two-column comparison poster using abstract left and right sections, arrows and shapes without readable labels', process_breakdown: 'a horizontal process poster with three or four connected visual stages using icons and arrows without readable labels', mini_framework: 'a clean two-by-two framework grid using abstract blocks, icons and spacing without readable labels', friction_snapshot: 'a central issue poster with one main shape and three to five smaller surrounding cause shapes without readable labels'\n}; const composition = posterPromptByFormat[format] || posterPromptByFormat.friction_snapshot; const editorialPosterPrompt = `Create a premium square LinkedIn editorial poster for the target audience. Composition:\n${composition} Palette:\n- warm off-white background\n- dark navy text and linework\n- muted orange accents\n- subtle paper texture\n- premium B2B editorial style\n- strong mobile contrast Readable text rules: Only render these exact words: ${brandName} ${headline} Do not render any other readable text. Do not render:\n- labels\n- captions\n- framework names\n- category names\n- step names\n- annotations\n- supporting copy\n- numbers\n- acronyms\n- placeholder text\n- fake words\n- partial words Represent supporting ideas only through:\n- simple shapes\n- abstract blocks\n- icons without letters\n- lines\n- arrows\n- spacing\n- visual hierarchy Strict exclusions:\n- no stock photography\n- no people\n- no hands\n- no laptops\n- no dashboards\n- no AI robots\n- no glowing brains\n- no circuit boards\n- no fake logos\n- no repeated six-box workflow diagram\n- no clutter\n- no extra readable text The final image must contain only the the configured brand name and the exact headline supplied above.`; return [ { json: { ...content, resolved_visual_format: format, editorial_poster_prompt: editorialPosterPrompt } }\n];"
},
"typeVersion": 2
},
{
"id": "66c08774-d5f6-4b77-8694-5578055a6bc0",
"name": "Check Screenshot Support",
"type": "n8n-nodes-base.code",
"position": [
19856,
5520
],
"parameters": {
"jsCode": "return [{ json: { ...$json, screenshot_service_available: false, rendering_path: 'openai_fallback' }\n}];"
},
"typeVersion": 2
},
{
"id": "b10c4641-7eef-4b76-9fdc-ee122c134eec",
"name": "If Screenshot Available",
"type": "n8n-nodes-base.if",
"position": [
20080,
5520
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeValidation": "strict"
},
"combinator": "and",
"conditions": [
{
"id": "f1a2b3c4-0002-4a11-9c11-+1234567890",
"operator": {
"type": "boolean",
"operation": "equals"
},
"leftValue": "={{ $json.screenshot_service_available }}",
"rightValue": true
}
]
}
},
"typeVersion": 2.3
},
{
"id": "b4817dc7-3a61-4498-aef5-18ecda4a9d9f",
"name": "Post HTML to PNG Service",
"type": "n8n-nodes-base.httpRequest",
"position": [
20304,
5424
],
"parameters": {
"url": "={{ $env.SCREENSHOT_SERVICE_URL }}",
"method": "POST",
"options": {
"response": {
"response": {
"responseFormat": "file"
}
}
},
"jsonBody": "={{ { html: $json.editorial_card_html, width: 1200, height: 1200, format: \"png\" } }}",
"sendBody": true,
"jsonHeaders": "={{ Object.assign({ \"Content-Type\": \"application/json\" }, $env.SCREENSHOT_SERVICE_TOKEN ? { Authorization: \"Bearer \" + $env.SCREENSHOT_SERVICE_TOKEN } : {}) }}",
"sendHeaders": true,
"specifyBody": "json",
"specifyHeaders": "json"
},
"typeVersion": 4.2
},
{
"id": "f0ae9711-5574-4ab0-a1b5-706a9b51ecee",
"name": "OpenAI Create Fallback Poster",
"type": "n8n-nodes-base.httpRequest",
"position": [
20304,
5616
],
"parameters": {
"url": "https://api.openai.com/v1/images/generations",
"method": "POST",
"options": {},
"jsonBody": "={{ JSON.stringify({ model: \"gpt-image-1\", prompt: `Create a premium square LinkedIn visual in the the organisation website brand style. Brand reference:\n- deep near-black / midnight navy background\n- subtle dotted grid texture across the background\n- large crisp white sans-serif typography\n- muted grey secondary typography\n- small restrained teal/green accent details\n- clean modern B2B SaaS aesthetic\n- spacious layout with strong negative space\n- polished, high-end, minimal, not decorative Design structure:\n- top-left small brand wordmark text using: ${$env.BRAND_NAME || \"YOUR BRAND\"}\n- one strong headline, maximum 7 words\n- one small supporting line only if useful\n- one simple abstract operational diagram or visual metaphor\n- use thin-line shapes, soft rounded rectangles, workflow paths, small status dots, handoff lines, or approval markers\n- visual should feel like it belongs on the the organisation's website\n- use subtle depth, glow, or shadow very sparingly Important:\n- Do not make it a plain text-only poster\n- Do not use cartoon illustration\n- Do not use cream, beige, orange, or paper textures\n- Do not use generic people\n- Do not use stock-photo style\n- Do not use robots, brains, circuit boards, or AI clich\u00e9s\n- Do not use busy infographic layouts\n- Do not use metallic or embossed text\n- Do not use outlined decorative type\n- Avoid over-rendered 3D effects Typography must be flat white, clean sans-serif, no bevel, no emboss, no metallic texture, no heavy drop shadow. Use a deep near-black background with a very subtle dotted grid texture. Keep the headline large but not oversized. Leave generous margin around the text. Use one simple operational metaphor below the headline, with thin linework and one restrained teal accent. Avoid over-polishing the text. It should look like clean website typography, not chrome or 3D lettering. Text handling:\n- If text is included, make it clean, flat, white, and highly legible\n- Use no more than one headline and one short supporting line\n- Keep typography similar to a premium SaaS landing page Specific post brief:\n${$json.editorial_poster_prompt || \"\"} If the post brief asks for a different background or palette, ignore that and use the the organisation brand style above.`, size: \"1024x1024\", quality: \"medium\", n: 1\n}) }}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $env.OPENAI_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "80feb1b4-cbf3-4fd4-b214-b1da73d7ae84",
"name": "Standardize Rendered Image",
"type": "n8n-nodes-base.code",
"position": [
20528,
5520
],
"parameters": {
"jsCode": "const items = $input.all();\nconst item = items[0];\nconst source = $('Create Editorial Card HTML').first().json; // Screenshot-service path returns raw PNG binary. OpenAI fallback path returns json.data[0].b64_json.\n// Normalise both into the same shape (json.data[0].b64_json) so every downstream node can stay format-agnostic. if (item.binary && item.binary.data) { const base64 = item.binary.data.data ? item.binary.data.data : Buffer.from(item.binary.data, 'base64').toString('base64'); return [{ json: { ...source, data: [{ b64_json: base64 }], render_method: 'screenshot_service' } }];\n} const j = item.json || {}; if (Array.isArray(j.data) && j.data[0]?.b64_json) { return [{ json: { ...source, data: j.data, render_method: 'openai_fallback' } }];\n} throw new Error('BLOCKED: rendered image is missing both binary data and data[0].b64_json.');"
},
"typeVersion": 2
},
{
"id": "6d12b618-4fd9-42d8-b5b0-7f751c29365f",
"name": "Post Image to OpenAI QA",
"type": "n8n-nodes-base.httpRequest",
"position": [
20752,
5520
],
"parameters": {
"url": "https://api.openai.com/v1/responses",
"method": "POST",
"options": {},
"jsonBody": "={{\n{ \"model\": \"gpt-5-mini\", \"input\": [ { \"role\": \"user\", \"content\": [ { \"type\": \"input_text\", \"text\": \"Inspect this LinkedIn editorial card image for the target audience. Return only valid JSON with these fields: approved boolean, score number from 1 to 10, issues array, reason string. Approve only if: the brand name \" + ($env.BRAND_NAME || \"YOUR BRAND\") + \" is rendered correctly; the headline is present and legible; there is no gibberish or fake text; nothing is clipped or cut off at the edges; no elements overlap illegibly; the layout visibly matches the intended visual format (checklist, strong statement, before/after, process breakdown, mini framework, or friction snapshot) rather than a generic six-box workflow diagram unless the format is genuinely process_breakdown; the image communicates the specific operational topic rather than a generic AI graphic; and contrast is strong enough to read on a mobile screen. Reject if any of these fail, if it looks like a repeat of a previous generic template, or if it is visually clean but does not communicate the stated topic. Any readable word other than the configured brand name and the exact supplied headline is an automatic rejection, even if correctly spelled. Intended visual format: \" + $json.resolved_visual_format + \". Intended headline: \" + $json.visual_headline }, { \"type\": \"input_image\", \"image_url\": \"data:image/png;base64,\" + $json.data[0].b64_json } ] } ], \"text\": { \"format\": { \"type\": \"json_object\" } }\n}\n}}",
"sendBody": true,
"sendHeaders": true,
"specifyBody": "json",
"headerParameters": {
"parameters": [
{
"name": "Authorization",
"value": "={{ 'Bearer ' + $env.OPENAI_API_KEY }}"
},
{
"name": "Content-Type",
"value": "application/json"
}
]
}
},
"typeVersion": 4.2
},
{
"id": "a1c64590-b5d2-49f0-a9cc-699adbc6bd2e",
"name": "Analyze Image QA Results",
"type": "n8n-nodes-base.code",
"position": [
20976,
5520
],
"parameters": {
"jsCode": "const qaInput = $input.first();\nconst response = qaInput.json || {}; // Recover the generated image and original content.\nconst imageContext = $('Standardize Rendered Image').first().json || {}; // Extract OpenAI Responses API text.\nlet responseText = ''; if (typeof response.output_text === 'string') { responseText = response.output_text;\n} if (!responseText && Array.isArray(response.output)) { for (const outputItem of response.output) { if (!Array.isArray(outputItem?.content)) continue; for (const contentItem of outputItem.content) { if ( typeof contentItem?.text === 'string' && ['output_text', 'text'].includes(contentItem.type) ) { responseText = contentItem.text; break; } } if (responseText) break; }\n} if ( !responseText && typeof response.choices?.[0]?.message?.content === 'string'\n) { responseText = response.choices[0].message.content;\n} if (!responseText) { throw new Error( 'BLOCKED: Image QA response did not contain readable output text.' );\n} const cleanedText = responseText .replace(/^```json\\s*/i, '') .replace(/^```\\s*/i, '') .replace(/\\s*```$/i, '') .trim(); let qa; try { qa = JSON.parse(cleanedText);\n} catch { throw new Error( `BLOCKED: Image QA returned invalid JSON. Raw output: ${cleanedText}` );\n} const score = Number( qa.visual_qa_score ?? qa.score ?? qa.qa_score ?? 0\n); const approved = qa.visual_qa_approved === true || qa.approved === true || String( qa.visual_qa_approved ?? qa.approved ?? '' ).toLowerCase() === 'true'; const issues = Array.isArray( qa.visual_qa_issues ?? qa.issues\n) ? qa.visual_qa_issues ?? qa.issues : qa.visual_qa_issues || qa.issues ? [String(qa.visual_qa_issues || qa.issues)] : []; const reason = String( qa.visual_qa_reason ?? qa.reason ?? ''\n).trim(); return [ { json: { ...imageContext, visual_qa_score: score, visual_qa_approved: approved, visual_qa_issues: issues, visual_qa_reason: reason, image_qa_raw_text: cleanedText }, binary: imageContext.binary || qaInput.binary }\n];"
},
"typeVersion": 2
},
{
"id": "819e39a9-0c83-4744-961a-4543157851a9",
"name": "If Image QA Passed",
"type": "n8n-nodes-base.if",
"position": [
21184,
5504
],
"parameters": {
"options": {},
"conditions": {
"options": {
"version": 3,
"leftValue": "",
"caseSensitive": true,
"typeV
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
Workflow runs at 08:00 (UK), AI stories from TechCrunch, The Verge, and the OpenAI Blog via RSS, AI to select and editorially shape one signal into a post and image prompt, generates and QA-checks a poster image, then publishes to a LinkedIn, logs results to Airtable and Gmail.…
Source: https://n8n.io/workflows/17486/ — 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.
AI News and weekly digest. Uses rssFeedRead, httpRequest, openAi, gmail. Scheduled trigger; 15 nodes.
A scheduled process aggregates content from eight distinct data sources and standardizes all inputs into a unified format. AI models perform sentiment scoring, detect conspiracy or misinformation sign
This workflow monitors filesystem sync and backup jobs by validating their execution logs, not by running or inspecting the jobs themselves.
Stop wasting billable hours on manual time-tracking. AutoTimesheet Pro uses AI to collect emails, meetings, and GitHub work, then writes a clean timesheet straight into Google Sheets. Perfect for deve
Imagine a dedicated financial expert tirelessly working behind the scenes, sifting through every transaction, every investment move, and every accounting entry. That's exactly what this automated syst