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": "wf004-asset-generation-000000000",
"name": "WF-004-asset-generation",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 */2 * * *"
}
]
}
},
"id": "c40400000-0004-0004-0004-000000000001",
"name": "Schedule 2hr",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
240,
400
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT id, title, description, channel_id FROM topics WHERE status = 'assigned' ORDER BY created_at ASC LIMIT 1",
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000002",
"name": "Poll Assigned Topic",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
460,
400
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c40400000-0004-0004-0004-000000000003-c",
"leftValue": "={{ $json.id }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000003",
"name": "Has Topic?",
"type": "n8n-nodes-base.filter",
"typeVersion": 2.2,
"position": [
680,
400
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ `SELECT kr.record_type, kr.content, kr.source_url, kr.source_name, kr.confidence_score, kr.id AS knowledge_record_id, ${$json.id} AS topic_id, ${$json.channel_id} AS channel_id, '${(($json.title)||'').replace(/'/g, \"''\")}' AS topic_title, '${(($json.description)||'').replace(/'/g, \"''\")}' AS topic_description FROM knowledge_records kr WHERE kr.topic_id = ${$json.id}` }}",
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000004",
"name": "Fetch Knowledge Records",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
900,
400
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const items = $input.all();\nconst first = items[0].json;\nconst records = items.map(i => ({\n content: i.json.content,\n record_type: i.json.record_type,\n source_url: i.json.source_url,\n source_name: i.json.source_name,\n confidence_score: i.json.confidence_score,\n knowledge_record_id: i.json.knowledge_record_id\n}));\n\nreturn [{\n json: {\n topic_id: first.topic_id,\n channel_id: first.channel_id,\n topic_title: first.topic_title,\n topic_description: first.topic_description,\n knowledge_records: records\n }\n}];"
},
"id": "c40400000-0004-0004-0004-000000000005",
"name": "Merge Knowledge",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1120,
400
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ `SELECT mission, target_audience, tone, reading_level, hook_style, writing_rules, forbidden_phrases, prompt_prefix FROM brand_profiles WHERE channel_id = ${$json.channel_id} LIMIT 1` }}",
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000006",
"name": "Fetch Brand Profile",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1340,
400
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT pv.system_prompt, pv.user_prompt_template, pv.model_params FROM prompt_versions pv JOIN prompts p ON p.id = pv.prompt_id WHERE p.name = 'asset_generate' AND pv.version = p.current_version AND p.is_active = true LIMIT 1",
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000007",
"name": "Load Prompt",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1560,
400
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const knowledge = $('Merge Knowledge').item.json;\nconst brand = $('Fetch Brand Profile').item.json;\nconst prompt = $input.item.json;\n\nconst userPrompt = (prompt.user_prompt_template || '')\n .replace('{{ topic.title }}', knowledge.topic_title || '')\n .replace('{{ knowledge_records_json_array }}', JSON.stringify(knowledge.knowledge_records || []))\n .replace(\"{{ brand_profile.forbidden_phrases | join(', ') }}\", (brand.forbidden_phrases || []).join(', '));\n\nreturn [{\n json: {\n topic_id: knowledge.topic_id,\n channel_id: knowledge.channel_id,\n topic_title: knowledge.topic_title,\n target_audience: brand.target_audience,\n tone: brand.tone,\n system_prompt: prompt.system_prompt,\n user_prompt: userPrompt,\n task_type: 'asset_generate',\n output_format: 'json',\n max_tokens: 4000,\n workflow_name: 'WF-004-asset-generation'\n }\n}];"
},
"id": "c40400000-0004-0004-0004-000000000008",
"name": "Build Prompt",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1780,
400
]
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "2673a82a-e155-4d0c-85b4-92922b74849d",
"mode": "id"
},
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000009",
"name": "Call SUB-001 Pass1",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.1,
"position": [
2000,
400
]
},
{
"parameters": {
"jsCode": "const raw = $input.item.json;\nconst ctx = $('Build Prompt').item.json;\n\nlet asset = raw.response_json;\nif (!asset || typeof asset !== 'object' || Array.isArray(asset)) {\n try { asset = JSON.parse(raw.response_text); } catch (e) { asset = {}; }\n}\n\nconst errors = [];\n\nif (!asset.core_premise || typeof asset.core_premise !== 'string' || asset.core_premise.trim().split(/\\s+/).length > 25) {\n errors.push('core_premise missing or over 25 words');\n}\nconst arcKeys = ['setup', 'inciting_incident', 'climax', 'resolution', 'key_takeaway'];\nif (!asset.story_arc || typeof asset.story_arc !== 'object' || !arcKeys.every(k => asset.story_arc[k])) {\n errors.push('story_arc missing required keys');\n}\nif (!Array.isArray(asset.hook_variations) || asset.hook_variations.length < 3) {\n errors.push(`hook_variations needs >=3, has ${Array.isArray(asset.hook_variations) ? asset.hook_variations.length : 0}`);\n}\nif (!Array.isArray(asset.facts) || asset.facts.length < 2) {\n errors.push(`facts needs >=2, has ${Array.isArray(asset.facts) ? asset.facts.length : 0}`);\n}\nif (!Array.isArray(asset.statistics) || asset.statistics.length < 1 || !asset.statistics.every(s => s.source_url)) {\n errors.push(`statistics needs >=1 items each with source_url, has ${Array.isArray(asset.statistics) ? asset.statistics.length : 0}`);\n}\nconst seo = asset.seo_data || {};\nconst seoErrs = [];\nif (!seo.primary_keyword) seoErrs.push('primary_keyword missing');\nif (!Array.isArray(seo.secondary_keywords) || seo.secondary_keywords.length < 4) seoErrs.push(`secondary_keywords needs >=4, has ${Array.isArray(seo.secondary_keywords) ? seo.secondary_keywords.length : 0}`);\nif (!Array.isArray(seo.tags) || seo.tags.length < 8) seoErrs.push(`tags needs >=8, has ${Array.isArray(seo.tags) ? seo.tags.length : 0}`);\nif (seoErrs.length > 0) errors.push(`seo_data incomplete: ${seoErrs.join('; ')}`);\nif (!Array.isArray(asset.visual_concepts) || asset.visual_concepts.length < 4) {\n errors.push(`visual_concepts needs >=4, has ${Array.isArray(asset.visual_concepts) ? asset.visual_concepts.length : 0}`);\n}\nif (!Array.isArray(asset.thumbnail_concepts) || asset.thumbnail_concepts.length < 2) {\n errors.push(`thumbnail_concepts needs >=2, has ${Array.isArray(asset.thumbnail_concepts) ? asset.thumbnail_concepts.length : 0}`);\n}\nif (!Array.isArray(asset.cta_variations) || asset.cta_variations.length < 1) {\n errors.push('cta_variations must have at least 1 item');\n}\n\nreturn [{\n json: {\n valid: errors.length === 0,\n errors,\n asset,\n topic_id: ctx.topic_id,\n channel_id: ctx.channel_id,\n topic_title: ctx.topic_title,\n target_audience: ctx.target_audience,\n tone: ctx.tone,\n provider_used: raw.provider_name || raw.provider_used || null,\n generation_cost_usd: raw.cost_usd || 0,\n generation_time_ms: raw.latency_ms || 0,\n input_tokens: raw.input_tokens || 0,\n output_tokens: raw.output_tokens || 0\n }\n}];"
},
"id": "c40400000-0004-0004-0004-000000000010",
"name": "Validate Asset JSON Pass1",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2220,
400
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c40400000-0004-0004-0004-000000000011-c",
"leftValue": "={{ $json.valid }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000011",
"name": "Valid Pass1?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
2440,
400
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ (() => {\n const j = $json;\n const a = j.asset;\n const esc = s => String(s == null ? '' : s).replace(/'/g, \"''\");\n return `INSERT INTO content_assets (topic_id, channel_id, status, core_premise, target_audience, tone, story_arc, hook_variations, facts, quotes, statistics, seo_data, visual_concepts, thumbnail_concepts, cta_variations, provider_used, generation_cost_usd, generation_time_ms, input_tokens, output_tokens, created_at, updated_at) VALUES (${j.topic_id}, ${j.channel_id}, 'draft', '${esc(a.core_premise)}', '${esc(j.target_audience)}', '${esc(j.tone)}', '${esc(JSON.stringify(a.story_arc || {}))}'::jsonb, '${esc(JSON.stringify(a.hook_variations || []))}'::jsonb, '${esc(JSON.stringify(a.facts || []))}'::jsonb, '${esc(JSON.stringify(a.quotes || []))}'::jsonb, '${esc(JSON.stringify(a.statistics || []))}'::jsonb, '${esc(JSON.stringify(a.seo_data || {}))}'::jsonb, '${esc(JSON.stringify(a.visual_concepts || []))}'::jsonb, '${esc(JSON.stringify(a.thumbnail_concepts || []))}'::jsonb, '${esc(JSON.stringify(a.cta_variations || []))}'::jsonb, ${j.provider_used ? `'${esc(j.provider_used)}'` : 'NULL'}, ${Number(j.generation_cost_usd) || 0}, ${Number(j.generation_time_ms) || 0}, ${Number(j.input_tokens) || 0}, ${Number(j.output_tokens) || 0}, NOW(), NOW()) RETURNING id`;\n})() }}",
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000012",
"name": "Insert Content Asset",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
2660,
260
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ `UPDATE topics SET status = 'archived', updated_at = NOW() WHERE id = ${$('Validate Asset JSON Pass1').item.json.topic_id} RETURNING id` }}",
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000013",
"name": "Update Topic Archived",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
2880,
260
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const asset = $('Validate Asset JSON Pass1').item.json;\nconst inserted = $('Insert Content Asset').item.json;\nreturn [{\n json: {\n asset_id: inserted.id,\n asset: asset.asset,\n topic_id: asset.topic_id,\n topic_title: asset.topic_title,\n channel_id: asset.channel_id\n }\n}];"
},
"id": "c40400000-0004-0004-0004-000000000014",
"name": "Prepare Quality Gate Input",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3100,
260
]
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "gCiRbYhxPKIGchWd",
"mode": "id"
},
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000015",
"name": "Call WF-004b Quality Gate",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.1,
"position": [
3320,
260
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c40400000-0004-0004-0004-000000000016-c",
"leftValue": "={{ $json.passed }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000016",
"name": "QG Passed?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
3540,
260
]
},
{
"parameters": {
"jsCode": "const esc = s => String(s == null ? '' : s).replace(/_/g, ' ').replace(/[*`\\[]/g, '');\nconst asset = $('Validate Asset JSON Pass1').item.json.asset;\nconst inserted = $('Insert Content Asset').item.json;\nconst bestHook = esc((asset.hook_variations && asset.hook_variations[0] && asset.hook_variations[0].text) || '');\nconst arc = asset.story_arc || {};\n\nconst message = `\\ud83d\\udccb *NEW ASSET READY FOR APPROVAL*\\n\\nTopic: ${esc($('Validate Asset JSON Pass1').item.json.topic_title)}\\nPremise: ${esc(asset.core_premise)}\\n\\nBest hook: ${bestHook}\\n\\nStory: ${esc(arc.setup || '')} \\u2192 ${esc(arc.key_takeaway || '')}\\n\\nVisuals: ${(asset.visual_concepts || []).length} scenes planned\\nFormats: YouTube Long + YouTube Short`;\n\nreturn [{\n json: {\n message,\n parse_mode: 'Markdown',\n inline_keyboard: [[\n { text: '\\u2705 Approve', callback_data: `approve_asset_${inserted.id}` },\n { text: '\\u274c Reject', callback_data: `reject_asset_${inserted.id}` },\n { text: '\\ud83d\\udd01 Regenerate', callback_data: `regenerate_asset_${inserted.id}` }\n ]],\n asset_id: inserted.id\n }\n}];"
},
"id": "c40400000-0004-0004-0004-000000000017",
"name": "Build Approval Message",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3760,
200
]
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "sub003-telegram-notification-000000",
"mode": "id"
},
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000018",
"name": "Notify Asset Approval",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.1,
"position": [
3980,
200
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ `INSERT INTO workflow_logs (workflow_name, workflow_id, execution_id, status, input_summary, output_summary, created_at) VALUES ('WF-004-asset-generation', 'wf004-asset-generation-000000000', '${$execution.id}', 'completed', '{}', '${JSON.stringify({asset_id: $('Insert Content Asset').item.json.id, generation_cost_usd: $('Validate Asset JSON Pass1').item.json.generation_cost_usd}).replace(/'/g, \"''\")}', NOW())` }}",
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000019",
"name": "Log Result Success",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
4200,
200
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ `INSERT INTO workflow_logs (workflow_name, workflow_id, execution_id, status, input_summary, output_summary, created_at) VALUES ('WF-004-asset-generation', 'wf004-asset-generation-000000000', '${$execution.id}', 'completed', '{}', '${JSON.stringify({asset_id: $json.asset_id, status: 'manual_review'}).replace(/'/g, \"''\")}', NOW())` }}",
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000020",
"name": "Log Result Manual Review",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
3760,
340
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const v = $input.item.json;\nconst base = $('Build Prompt').item.json;\n\nconst countChecklist = 'Every one of these minimums applies to the regenerated JSON, not just the fields listed as failed below: hook_variations >=3 items; facts >=3 items each with knowledge_record_id; statistics >=2 items each with source_url; seo_data.secondary_keywords >=5 items; seo_data.tags >=10 items; visual_concepts >=5 items; thumbnail_concepts >=2 items; cta_variations >=1 item.';\nconst correctivePrompt = base.user_prompt +\n `\\n\\nPrevious generation failed validation: ${v.errors.join('; ')}. Regenerate the FULL JSON object, fixing these specific issues while keeping all other valid fields intact. ${countChecklist}`;\n\nreturn [{\n json: {\n topic_id: v.topic_id,\n channel_id: v.channel_id,\n topic_title: v.topic_title,\n target_audience: v.target_audience,\n tone: v.tone,\n system_prompt: base.system_prompt,\n user_prompt: correctivePrompt,\n task_type: 'asset_generate',\n output_format: 'json',\n max_tokens: 4000,\n workflow_name: 'WF-004-asset-generation'\n }\n}];"
},
"id": "c40400000-0004-0004-0004-000000000021",
"name": "Build Corrective Prompt",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2660,
560
]
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "2673a82a-e155-4d0c-85b4-92922b74849d",
"mode": "id"
},
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000022",
"name": "Call SUB-001 Pass2",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.1,
"position": [
2880,
560
]
},
{
"parameters": {
"jsCode": "const raw = $input.item.json;\nconst ctx = $('Build Corrective Prompt').item.json;\n\nlet asset = raw.response_json;\nif (!asset || typeof asset !== 'object' || Array.isArray(asset)) {\n try { asset = JSON.parse(raw.response_text); } catch (e) { asset = {}; }\n}\n\nconst errors = [];\n\nif (!asset.core_premise || typeof asset.core_premise !== 'string' || asset.core_premise.trim().split(/\\s+/).length > 25) {\n errors.push('core_premise missing or over 25 words');\n}\nconst arcKeys = ['setup', 'inciting_incident', 'climax', 'resolution', 'key_takeaway'];\nif (!asset.story_arc || typeof asset.story_arc !== 'object' || !arcKeys.every(k => asset.story_arc[k])) {\n errors.push('story_arc missing required keys');\n}\nif (!Array.isArray(asset.hook_variations) || asset.hook_variations.length < 3) {\n errors.push(`hook_variations needs >=3, has ${Array.isArray(asset.hook_variations) ? asset.hook_variations.length : 0}`);\n}\nif (!Array.isArray(asset.facts) || asset.facts.length < 2) {\n errors.push(`facts needs >=2, has ${Array.isArray(asset.facts) ? asset.facts.length : 0}`);\n}\nif (!Array.isArray(asset.statistics) || asset.statistics.length < 1 || !asset.statistics.every(s => s.source_url)) {\n errors.push(`statistics needs >=1 items each with source_url, has ${Array.isArray(asset.statistics) ? asset.statistics.length : 0}`);\n}\nconst seo = asset.seo_data || {};\nconst seoErrs = [];\nif (!seo.primary_keyword) seoErrs.push('primary_keyword missing');\nif (!Array.isArray(seo.secondary_keywords) || seo.secondary_keywords.length < 4) seoErrs.push(`secondary_keywords needs >=4, has ${Array.isArray(seo.secondary_keywords) ? seo.secondary_keywords.length : 0}`);\nif (!Array.isArray(seo.tags) || seo.tags.length < 8) seoErrs.push(`tags needs >=8, has ${Array.isArray(seo.tags) ? seo.tags.length : 0}`);\nif (seoErrs.length > 0) errors.push(`seo_data incomplete: ${seoErrs.join('; ')}`);\nif (!Array.isArray(asset.visual_concepts) || asset.visual_concepts.length < 4) {\n errors.push(`visual_concepts needs >=4, has ${Array.isArray(asset.visual_concepts) ? asset.visual_concepts.length : 0}`);\n}\nif (!Array.isArray(asset.thumbnail_concepts) || asset.thumbnail_concepts.length < 2) {\n errors.push(`thumbnail_concepts needs >=2, has ${Array.isArray(asset.thumbnail_concepts) ? asset.thumbnail_concepts.length : 0}`);\n}\nif (!Array.isArray(asset.cta_variations) || asset.cta_variations.length < 1) {\n errors.push('cta_variations must have at least 1 item');\n}\n\nreturn [{\n json: {\n valid: errors.length === 0,\n errors,\n asset,\n topic_id: ctx.topic_id,\n channel_id: ctx.channel_id,\n topic_title: ctx.topic_title,\n target_audience: ctx.target_audience,\n tone: ctx.tone,\n provider_used: raw.provider_name || raw.provider_used || null,\n generation_cost_usd: raw.cost_usd || 0,\n generation_time_ms: raw.latency_ms || 0,\n input_tokens: raw.input_tokens || 0,\n output_tokens: raw.output_tokens || 0\n }\n}];"
},
"id": "c40400000-0004-0004-0004-000000000023",
"name": "Validate Asset JSON Pass2",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3100,
560
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c40400000-0004-0004-0004-000000000024-c",
"leftValue": "={{ $json.valid }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000024",
"name": "Valid Pass2?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
3320,
560
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ (() => {\n const j = $json;\n const a = j.asset;\n const esc = s => String(s == null ? '' : s).replace(/'/g, \"''\");\n return `INSERT INTO content_assets (topic_id, channel_id, status, core_premise, target_audience, tone, story_arc, hook_variations, facts, quotes, statistics, seo_data, visual_concepts, thumbnail_concepts, cta_variations, provider_used, generation_cost_usd, generation_time_ms, input_tokens, output_tokens, created_at, updated_at) VALUES (${j.topic_id}, ${j.channel_id}, 'draft', '${esc(a.core_premise)}', '${esc(j.target_audience)}', '${esc(j.tone)}', '${esc(JSON.stringify(a.story_arc || {}))}'::jsonb, '${esc(JSON.stringify(a.hook_variations || []))}'::jsonb, '${esc(JSON.stringify(a.facts || []))}'::jsonb, '${esc(JSON.stringify(a.quotes || []))}'::jsonb, '${esc(JSON.stringify(a.statistics || []))}'::jsonb, '${esc(JSON.stringify(a.seo_data || {}))}'::jsonb, '${esc(JSON.stringify(a.visual_concepts || []))}'::jsonb, '${esc(JSON.stringify(a.thumbnail_concepts || []))}'::jsonb, '${esc(JSON.stringify(a.cta_variations || []))}'::jsonb, ${j.provider_used ? `'${esc(j.provider_used)}'` : 'NULL'}, ${Number(j.generation_cost_usd) || 0}, ${Number(j.generation_time_ms) || 0}, ${Number(j.input_tokens) || 0}, ${Number(j.output_tokens) || 0}, NOW(), NOW()) RETURNING id`;\n})() }}",
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000025",
"name": "Insert Content Asset Pass2",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
3540,
480
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ `UPDATE topics SET status = 'archived', updated_at = NOW() WHERE id = ${$('Validate Asset JSON Pass2').item.json.topic_id} RETURNING id` }}",
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000026",
"name": "Update Topic Archived Pass2",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
3760,
480
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const asset = $('Validate Asset JSON Pass2').item.json;\nconst inserted = $('Insert Content Asset Pass2').item.json;\nreturn [{\n json: {\n asset_id: inserted.id,\n asset: asset.asset,\n topic_id: asset.topic_id,\n topic_title: asset.topic_title,\n channel_id: asset.channel_id\n }\n}];"
},
"id": "c40400000-0004-0004-0004-000000000027",
"name": "Prepare Quality Gate Input Pass2",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3980,
480
]
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "gCiRbYhxPKIGchWd",
"mode": "id"
},
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000028",
"name": "Call WF-004b Quality Gate Pass2",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.1,
"position": [
4200,
480
]
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict"
},
"conditions": [
{
"id": "c40400000-0004-0004-0004-000000000029-c",
"leftValue": "={{ $json.passed }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "true"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000029",
"name": "QG Passed Pass2?",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
4420,
480
]
},
{
"parameters": {
"jsCode": "const esc = s => String(s == null ? '' : s).replace(/_/g, ' ').replace(/[*`\\[]/g, '');\nconst asset = $('Validate Asset JSON Pass2').item.json.asset;\nconst inserted = $('Insert Content Asset Pass2').item.json;\nconst bestHook = esc((asset.hook_variations && asset.hook_variations[0] && asset.hook_variations[0].text) || '');\nconst arc = asset.story_arc || {};\n\nconst message = `\\ud83d\\udccb *NEW ASSET READY FOR APPROVAL*\\n\\nTopic: ${esc($('Validate Asset JSON Pass2').item.json.topic_title)}\\nPremise: ${esc(asset.core_premise)}\\n\\nBest hook: ${bestHook}\\n\\nStory: ${esc(arc.setup || '')} \\u2192 ${esc(arc.key_takeaway || '')}\\n\\nVisuals: ${(asset.visual_concepts || []).length} scenes planned\\nFormats: YouTube Long + YouTube Short`;\n\nreturn [{\n json: {\n message,\n parse_mode: 'Markdown',\n inline_keyboard: [[\n { text: '\\u2705 Approve', callback_data: `approve_asset_${inserted.id}` },\n { text: '\\u274c Reject', callback_data: `reject_asset_${inserted.id}` },\n { text: '\\ud83d\\udd01 Regenerate', callback_data: `regenerate_asset_${inserted.id}` }\n ]],\n asset_id: inserted.id\n }\n}];"
},
"id": "c40400000-0004-0004-0004-000000000030",
"name": "Build Approval Message Pass2",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
4640,
420
]
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "sub003-telegram-notification-000000",
"mode": "id"
},
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000031",
"name": "Notify Asset Approval Pass2",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.1,
"position": [
4860,
420
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ `INSERT INTO workflow_logs (workflow_name, workflow_id, execution_id, status, input_summary, output_summary, created_at) VALUES ('WF-004-asset-generation', 'wf004-asset-generation-000000000', '${$execution.id}', 'completed', '{}', '${JSON.stringify({asset_id: $('Insert Content Asset Pass2').item.json.id, generation_cost_usd: $('Validate Asset JSON Pass2').item.json.generation_cost_usd}).replace(/'/g, \"''\")}', NOW())` }}",
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000032",
"name": "Log Result Success Pass2",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
5080,
420
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "={{ `INSERT INTO workflow_logs (workflow_name, workflow_id, execution_id, status, input_summary, output_summary, created_at) VALUES ('WF-004-asset-generation', 'wf004-asset-generation-000000000', '${$execution.id}', 'completed', '{}', '${JSON.stringify({asset_id: $json.asset_id, status: 'manual_review'}).replace(/'/g, \"''\")}', NOW())` }}",
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000033",
"name": "Log Result Manual Review Pass2",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
4640,
560
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const v = $input.item.json;\nreturn [{\n json: {\n workflow_name: 'WF-004-asset-generation',\n execution_id: String($execution.id),\n node_name: 'Validate Asset JSON Pass2',\n error_type: 'validation_failed',\n error_message: `Asset JSON failed validation after retry: ${v.errors.join('; ')}`,\n payload: { topic_id: v.topic_id, errors: v.errors }\n }\n}];"
},
"id": "c40400000-0004-0004-0004-000000000034",
"name": "Build DLQ Payload",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
3540,
660
]
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "sub002-dead-letter-queue-0000000",
"mode": "id"
},
"options": {}
},
"id": "c40400000-0004-0004-0004-000000000035",
"name": "Log to SUB-002 DLQ",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.1,
"position": [
3760,
660
]
}
],
"connections": {
"Schedule 2hr": {
"main": [
[
{
"node": "Poll Assigned Topic",
"type": "main",
"index": 0
}
]
]
},
"Poll Assigned Topic": {
"main": [
[
{
"node": "Has Topic?",
"type": "main",
"index": 0
}
]
]
},
"Has Topic?": {
"main": [
[
{
"node": "Fetch Knowledge Records",
"type": "main",
"index": 0
}
]
]
},
"Fetch Knowledge Records": {
"main": [
[
{
"node": "Merge Knowledge",
"type": "main",
"index": 0
}
]
]
},
"Merge Knowledge": {
"main": [
[
{
"node": "Fetch Brand Profile",
"type": "main",
"index": 0
}
]
]
},
"Fetch Brand Profile": {
"main": [
[
{
"node": "Load Prompt",
"type": "main",
"index": 0
}
]
]
},
"Load Prompt": {
"main": [
[
{
"node": "Build Prompt",
"type": "main",
"index": 0
}
]
]
},
"Build Prompt": {
"main": [
[
{
"node": "Call SUB-001 Pass1",
"type": "main",
"index": 0
}
]
]
},
"Call SUB-001 Pass1": {
"main": [
[
{
"node": "Validate Asset JSON Pass1",
"type": "main",
"index": 0
}
]
]
},
"Validate Asset JSON Pass1": {
"main": [
[
{
"node": "Valid Pass1?",
"type": "main",
"index": 0
}
]
]
},
"Valid Pass1?": {
"main": [
[
{
"node": "Insert Content Asset",
"type": "main",
"index": 0
}
],
[
{
"node": "Build Corrective Prompt",
"type": "main",
"index": 0
}
]
]
},
"Insert Content Asset": {
"main": [
[
{
"node": "Update Topic Archived",
"type": "main",
"index": 0
}
]
]
},
"Update Topic Archived": {
"main": [
[
{
"node": "Prepare Quality Gate Input",
"type": "main",
"index": 0
}
]
]
},
"Prepare Quality Gate Input": {
"main": [
[
{
"node": "Call WF-004b Quality Gate",
"type": "main",
"index": 0
}
]
]
},
"Call WF-004b Quality Gate": {
"main": [
[
{
"node": "QG Passed?",
"type": "main",
"index": 0
}
]
]
},
"QG Passed?": {
"main": [
[
{
"node": "Build Approval Message",
"type": "main",
"index": 0
}
],
[
{
"node": "Log Result Manual Review",
"type": "main",
"index": 0
}
]
]
},
"Build Approval Message": {
"main": [
[
{
"node": "Notify Asset Approval",
"type": "main",
"index": 0
}
]
]
},
"Notify Asset Approval": {
"main": [
[
{
"node": "Log Result Success",
"type": "main",
"index": 0
}
]
]
},
"Build Corrective Prompt": {
"main": [
[
{
"node": "Call SUB-001 Pass2",
"type": "main",
"index": 0
}
]
]
},
"Call SUB-001 Pass2": {
"main": [
[
{
"node": "Validate Asset JSON Pass2",
"type": "main",
"index": 0
}
]
]
},
"Validate Asset JSON Pass2": {
"main": [
[
{
"node": "Valid Pass2?",
"type": "main",
"index": 0
}
]
]
},
"Valid Pass2?": {
"main": [
[
{
"node": "Insert Content Asset Pass2",
"type": "main",
"index": 0
}
],
[
{
"node": "Build DLQ Payload",
"type": "main",
"index": 0
}
]
]
},
"Insert Content Asset Pass2": {
"main": [
[
{
"node": "Update Topic Archived Pass2",
"type": "main",
"index": 0
}
]
]
},
"Update Topic Archived Pass2": {
"main": [
[
{
"node": "Prepare Quality Gate Input Pass2",
"type": "main",
"index": 0
}
]
]
},
"Prepare Quality Gate Input Pass2": {
"main": [
[
{
"node": "Call WF-004b Quality Gate Pass2",
"type": "main",
"index": 0
}
]
]
},
"Call WF-004b Quality Gate Pass2": {
"main": [
[
{
"node": "QG Passed Pass2?",
"type": "main",
"index": 0
}
]
]
},
"QG Passed Pass2?": {
"main": [
[
{
"node": "Build Approval Message Pass2",
"type": "main",
"index": 0
}
],
[
{
"node": "Log Result Manual Review Pass2",
"type": "main",
"index": 0
}
]
]
},
"Build Approval Message Pass2": {
"main": [
[
{
"node": "Notify Asset Approval Pass2",
"type": "main",
"index": 0
}
]
]
},
"Notify Asset Approval Pass2": {
"main": [
[
{
"node": "Log Result Success Pass2",
"type": "main",
"index": 0
}
]
]
},
"Build DLQ Payload": {
"main": [
[
{
"node": "Log to SUB-002 DLQ",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1"
}
}
Credentials you'll need
Each integration node will prompt for credentials when you import. We strip credential IDs before publishing — you'll add your own.
postgres
For the full experience including quality scoring and batch install features for each workflow upgrade to Pro
About this workflow
WF-004-asset-generation. Uses postgres. Scheduled trigger; 35 nodes.
Source: https://github.com/navin-labs/content-factory-os/blob/main/n8n/workflows/main/WF-004-asset-generation.json — original creator credit. Request a take-down →
Related workflows
Workflows that share integrations, category, or trigger type with this one. All free to copy and import.
Disparador 1.8. Uses itemLists, postgres, emailSend, httpRequest. Scheduled trigger; 85 nodes.
공유회_알림톡_크론. Uses postgres, httpRequest, n8n-nodes-solapi. Scheduled trigger; 39 nodes.
QuepasaAutomatic. Uses postgres, postgresTrigger, httpRequest. Scheduled trigger; 39 nodes.
QuepasaAutomatic. Uses postgres, postgresTrigger, httpRequest. Scheduled trigger; 39 nodes.
QuepasaAutomatic. Uses postgres, postgresTrigger, httpRequest. Scheduled trigger; 39 nodes.