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": "wf003-knowledge-extraction-00000",
"name": "WF-003-knowledge-extraction",
"nodes": [
{
"parameters": {
"rule": {
"interval": [
{
"field": "cronExpression",
"expression": "0 * * * *"
}
]
}
},
"id": "c4000000-0003-0003-0003-000000000001",
"name": "Schedule 1hr",
"type": "n8n-nodes-base.scheduleTrigger",
"typeVersion": 1.2,
"position": [
240,
512
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "SELECT t.id, t.title, t.description, t.source FROM topics t WHERE t.status = 'approved' AND EXISTS (SELECT 1 FROM raw_research rr WHERE rr.topic_id = t.id AND rr.status = 'ingested') ORDER BY t.created_at ASC LIMIT 1",
"options": {}
},
"id": "c4000000-0003-0003-0003-000000000002",
"name": "Poll Approved Topics",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
464,
512
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 1
},
"conditions": [
{
"id": "c4000000-0003-0003-0003-000000000030",
"leftValue": "={{ $json.id }}",
"rightValue": 0,
"operator": {
"type": "number",
"operation": "gt"
}
}
],
"combinator": "and"
},
"options": {}
},
"id": "c4000000-0003-0003-0003-000000000003",
"name": "Has Topics?",
"type": "n8n-nodes-base.filter",
"typeVersion": 2.2,
"position": [
688,
512
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "{{ `SELECT rr.content_text, rr.source_url, rr.source_name, ${$json.id} AS topic_id, '${($json.title || '').replace(/'/g, \"''\")}' AS topic_title, '${($json.description || '').replace(/'/g, \"''\")}' AS topic_description FROM raw_research rr WHERE rr.topic_id = ${$json.id} AND rr.status = 'ingested' LIMIT 20` }}",
"options": {}
},
"id": "c4000000-0003-0003-0003-000000000004",
"name": "Fetch Raw Research",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
912,
512
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const items = $input.all();\nreturn items.map((i, idx) => ({\n json: {\n topic_id: i.json.topic_id,\n topic_title: i.json.topic_title,\n topic_description: i.json.topic_description,\n research_text: i.json.content_text || '',\n source_url: i.json.source_url || null,\n source_name: i.json.source_name || null\n },\n pairedItem: { item: idx }\n}));"
},
"id": "c4000000-0003-0003-0003-000000000005",
"name": "Merge Research",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1120,
512
]
},
{
"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 = 'knowledge_extract' AND pv.version = p.current_version AND p.is_active = true LIMIT 1",
"options": {}
},
"id": "c4000000-0003-0003-0003-000000000006",
"name": "Load Prompt",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
1344,
512
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"jsCode": "const research = $('Merge Research').item.json;\nconst prompt = $input.item.json;\n\nconst userPrompt = (prompt.user_prompt_template || '')\n .replace('{{ topic_title }}', research.topic_title || '')\n .replace('{{ research_text }}', research.research_text || '');\n\nreturn [{\n json: {\n topic_id: research.topic_id,\n topic_title: research.topic_title,\n source_url: research.source_url,\n source_name: research.source_name,\n system_prompt: prompt.system_prompt,\n user_prompt: userPrompt,\n task_type: 'knowledge_extract',\n output_format: 'text',\n workflow_name: 'WF-003-knowledge-extraction'\n }\n}];"
},
"id": "c4000000-0003-0003-0003-000000000007",
"name": "Build Prompt",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1568,
512
]
},
{
"parameters": {
"workflowId": {
"__rl": true,
"value": "2673a82a-e155-4d0c-85b4-92922b74849d",
"mode": "id"
},
"options": {}
},
"id": "c4000000-0003-0003-0003-000000000008",
"name": "Call SUB-001",
"type": "n8n-nodes-base.executeWorkflow",
"typeVersion": 1.1,
"position": [
1792,
512
]
},
{
"parameters": {
"jsCode": "const raw = $input.item.json;\nconst ctx = $('Build Prompt').item.json;\nconst topicId = ctx.topic_id;\n\n// Lenient extractor: prefer clean JSON, else salvage record-by-record from text.\nfunction extractRecords(rj, txt) {\n // 1. Response was structured JSON \u2014 the LLM built the array for us.\n if (Array.isArray(rj)) return rj;\n if (rj && typeof rj === 'object') {\n const arr = Object.values(rj).find(v => Array.isArray(v));\n if (arr) return arr;\n }\n const text = String(txt || '');\n if (!text) return [];\n\n // 2. Try full-array parse.\n const arrMatch = text.match(/\\[[\\s\\S]*\\]/);\n if (arrMatch) {\n try { const p = JSON.parse(arrMatch[0]); if (Array.isArray(p)) return p; } catch (_) {}\n }\n\n // 3. Salvage: greedy scan for balanced {...} objects and try each in isolation.\n // One bad quote in one record no longer nukes the whole batch.\n const salvaged = [];\n const start = text.indexOf('{');\n if (start === -1) return [];\n let depth = 0, from = -1, inStr = false, esc = false;\n for (let i = start; i < text.length; i++) {\n const c = text[i];\n if (esc) { esc = false; continue; }\n if (c === '\\\\') { esc = true; continue; }\n if (c === '\"') { inStr = !inStr; continue; }\n if (inStr) continue;\n if (c === '{') { if (depth === 0) from = i; depth++; }\n else if (c === '}') {\n depth--;\n if (depth === 0 && from !== -1) {\n const chunk = text.slice(from, i + 1);\n try { salvaged.push(JSON.parse(chunk)); }\n catch (_) {\n // Repair pass \u2014 collapse stray \" before a comma or } that follows text.\n const repaired = chunk.replace(/\"\\s*-\\s*([A-Z][a-zA-Z .'-]{1,60})\"/g, ' - $1\"');\n try { salvaged.push(JSON.parse(repaired)); } catch (__) { /* drop this record */ }\n }\n from = -1;\n }\n }\n }\n return salvaged;\n}\n\nconst records = extractRecords(raw.response_json, raw.response_text);\n\nconst validTypes = ['fact','statistic','quote','entity','event','definition'];\nconst esc = s => String(s || '').replace(/'/g, \"''\");\nconst srcUrl = ctx.source_url ? `'${esc(ctx.source_url)}'` : 'NULL';\nconst srcName = ctx.source_name ? `'${esc(ctx.source_name)}'` : 'NULL';\n\nconst values = records.slice(0, 30).map(r => {\n const recType = validTypes.includes(r.record_type) ? r.record_type : 'fact';\n const content = esc(String(r.content || '').substring(0, 2000));\n const score = parseFloat(r.confidence_score) || 0.8;\n return `(${topicId}, '${recType}', '${content}', ${srcUrl}, ${srcName}, ${score}, NOW())`;\n}).filter(v => v.includes(\"''\") === false || v.length > 40); // drop truly empty content\n\nconst bulkSql = values.length > 0\n ? `INSERT INTO knowledge_records (topic_id, record_type, content, source_url, source_name, confidence_score, created_at) VALUES ${values.join(', ')} ON CONFLICT DO NOTHING`\n : `SELECT 0 AS inserted`;\n\nreturn [{ json: { topic_id: topicId, record_count: values.length, bulkSql, salvaged: records.length } }];\n"
},
"id": "c4000000-0003-0003-0003-000000000009",
"name": "Parse & Build SQL",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
2000,
512
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "{{ $json.bulkSql }}",
"options": {}
},
"id": "c4000000-0003-0003-0003-000000000010",
"name": "Bulk Insert Records",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
2224,
512
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"options": {}
},
"id": "c4000000-0003-0003-0003-000000000015",
"name": "Loop Over Sources",
"type": "n8n-nodes-base.splitInBatches",
"typeVersion": 3,
"position": [
1376,
800
]
},
{
"parameters": {
"operation": "executeQuery",
"query": "{{ `SELECT COUNT(*) AS record_count, ${$('Poll Approved Topics').first().json.id} AS topic_id FROM knowledge_records WHERE topic_id = ${$('Poll Approved Topics').first().json.id}` }}",
"options": {}
},
"id": "c4000000-0003-0003-0003-000000000012",
"name": "Count Records",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
2672,
512
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"parameters": {
"operation": "executeQuery",
"query": "{{ `UPDATE topics SET status = '${parseInt($json.record_count) >= 3 ? 'assigned' : 'archived'}', updated_at = NOW() WHERE id = ${$json.topic_id} RETURNING id, title, status` }}",
"options": {}
},
"id": "c4000000-0003-0003-0003-000000000013",
"name": "Update Topic",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
2880,
512
],
"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-003-knowledge-extraction', 'wf003-knowledge-extraction-00000', '${$execution.id}', 'completed', '{}', '{}', NOW())` }}",
"options": {}
},
"id": "c4000000-0003-0003-0003-000000000014",
"name": "Log Result",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2.5,
"position": [
3104,
512
],
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Schedule 1hr": {
"main": [
[
{
"node": "Poll Approved Topics",
"type": "main",
"index": 0
}
]
]
},
"Poll Approved Topics": {
"main": [
[
{
"node": "Has Topics?",
"type": "main",
"index": 0
}
]
]
},
"Has Topics?": {
"main": [
[
{
"node": "Fetch Raw Research",
"type": "main",
"index": 0
}
]
]
},
"Fetch Raw Research": {
"main": [
[
{
"node": "Merge Research",
"type": "main",
"index": 0
}
]
]
},
"Merge Research": {
"main": [
[
{
"node": "Loop Over Sources",
"type": "main",
"index": 0
}
]
]
},
"Loop Over Sources": {
"main": [
[
{
"node": "Count Records",
"type": "main",
"index": 0
}
],
[
{
"node": "Load Prompt",
"type": "main",
"index": 0
}
]
]
},
"Load Prompt": {
"main": [
[
{
"node": "Build Prompt",
"type": "main",
"index": 0
}
]
]
},
"Build Prompt": {
"main": [
[
{
"node": "Call SUB-001",
"type": "main",
"index": 0
}
]
]
},
"Call SUB-001": {
"main": [
[
{
"node": "Parse & Build SQL",
"type": "main",
"index": 0
}
]
]
},
"Parse & Build SQL": {
"main": [
[
{
"node": "Bulk Insert Records",
"type": "main",
"index": 0
}
]
]
},
"Bulk Insert Records": {
"main": [
[
{
"node": "Loop Over Sources",
"type": "main",
"index": 0
}
]
]
},
"Count Records": {
"main": [
[
{
"node": "Update Topic",
"type": "main",
"index": 0
}
]
]
},
"Update Topic": {
"main": [
[
{
"node": "Log Result",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"binaryMode": "separate"
}
}
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-003-knowledge-extraction. Uses postgres. Scheduled trigger; 14 nodes.
Source: https://github.com/navin-labs/content-factory-os/blob/main/n8n/workflows/main/WF-003-knowledge-extraction.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.