AutomationFlowsData & Sheets › Wf 004 Asset Generation

Wf 004 Asset Generation

WF-004-asset-generation. Uses postgres. Scheduled trigger; 35 nodes.

Cron / scheduled trigger★★★★★ complexity35 nodesPostgres
Data & Sheets Trigger: Cron / scheduled Nodes: 35 Complexity: ★★★★★ Added:

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 →

Download .json
{
  "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.

Pro

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 →

More Data & Sheets workflows → · Browse all categories →

Related workflows

Workflows that share integrations, category, or trigger type with this one. All free to copy and import.

Data & Sheets

Disparador 1.8. Uses itemLists, postgres, emailSend, httpRequest. Scheduled trigger; 85 nodes.

Item Lists, Postgres, Email Send +1
Data & Sheets

공유회_알림톡_크론. Uses postgres, httpRequest, n8n-nodes-solapi. Scheduled trigger; 39 nodes.

Postgres, HTTP Request, N8N Nodes Solapi
Data & Sheets

QuepasaAutomatic. Uses postgres, postgresTrigger, httpRequest. Scheduled trigger; 39 nodes.

Postgres, Postgres Trigger, HTTP Request
Data & Sheets

QuepasaAutomatic. Uses postgres, postgresTrigger, httpRequest. Scheduled trigger; 39 nodes.

Postgres, Postgres Trigger, HTTP Request
Data & Sheets

QuepasaAutomatic. Uses postgres, postgresTrigger, httpRequest. Scheduled trigger; 39 nodes.

Postgres, Postgres Trigger, HTTP Request