This workflow follows the HTTP Request → Postgres 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": "",
"name": "06 - Body Recovery (LinkedIn JSON-LD)",
"active": false,
"nodes": [
{
"id": "trigger-manual",
"name": "Manual Trigger",
"type": "n8n-nodes-base.manualTrigger",
"typeVersion": 1,
"position": [
-400,
0
],
"parameters": {}
},
{
"id": "set-config",
"name": "Set: Config",
"type": "n8n-nodes-base.set",
"typeVersion": 3.4,
"position": [
-160,
0
],
"parameters": {
"assignments": {
"assignments": [
{
"id": "cfg-dry-run",
"name": "dry_run",
"value": true,
"type": "boolean"
},
{
"id": "cfg-batch-size",
"name": "batch_size",
"value": 5,
"type": "number"
},
{
"id": "cfg-min-body",
"name": "min_body_length",
"value": 200,
"type": "number"
}
]
},
"options": {}
}
},
{
"id": "pg-select-quarantined",
"name": "Postgres: Select Quarantined LinkedIn",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2,
"position": [
80,
0
],
"parameters": {
"operation": "executeQuery",
"query": "=SELECT id, url, title, company FROM listings WHERE source = 'linkedin' AND quarantine_reason IS NOT NULL AND url LIKE '%linkedin.com/jobs/%' ORDER BY date_seen DESC LIMIT {{ $('Set: Config').first().json.batch_size }};",
"options": {}
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
},
{
"id": "http-fetch-jd",
"name": "HTTP Request: Fetch JD HTML",
"type": "n8n-nodes-base.httpRequest",
"typeVersion": 4.2,
"position": [
320,
0
],
"onError": "continueRegularOutput",
"parameters": {
"method": "GET",
"url": "={{ $json.url }}",
"sendHeaders": true,
"headerParameters": {
"parameters": [
{
"name": "User-Agent",
"value": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/121.0.0.0 Safari/537.36"
},
{
"name": "Accept",
"value": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"
},
{
"name": "Accept-Language",
"value": "en-US,en;q=0.7,de;q=0.3"
}
]
},
"options": {
"response": {
"response": {
"responseFormat": "text",
"neverError": true
}
},
"timeout": 15000,
"redirect": {
"redirect": {
"followRedirects": true,
"maxRedirects": 5
}
}
}
}
},
{
"id": "code-extract-jsonld",
"name": "Code: Extract JSON-LD Description",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
560,
0
],
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// Extract description from a LinkedIn job page's JobPosting JSON-LD block.\n// LinkedIn ships structured data on most public job pages even unauth \u2014 this is\n// the cheapest recovery path. Fallback path (Apify detail actor) is out of scope\n// for this draft.\n\nconst minBody = $('Set: Config').first().json.min_body_length || 200;\n\nconst decodeEntities = (s) => String(s || '')\n .replace(/&/g, '&')\n .replace(/</g, '<')\n .replace(/>/g, '>')\n .replace(/"/g, '\"')\n .replace(/'/g, \"'\")\n .replace(/ /g, ' ');\n\nconst stripTags = (s) => String(s || '')\n .replace(/<br\\s*\\/?>/gi, '\\n')\n .replace(/<\\/p>/gi, '\\n\\n')\n .replace(/<\\/li>/gi, '\\n')\n .replace(/<[^>]+>/g, '')\n .replace(/\\n{3,}/g, '\\n\\n')\n .trim();\n\nconst extractJobPosting = (html) => {\n if (!html || typeof html !== 'string') return null;\n const re = /<script[^>]*type=[\"']application\\/ld\\+json[\"'][^>]*>([\\s\\S]*?)<\\/script>/gi;\n let m;\n const blocks = [];\n while ((m = re.exec(html)) !== null) {\n try {\n const parsed = JSON.parse(m[1].trim());\n blocks.push(parsed);\n } catch (e) {\n // skip malformed blocks\n }\n }\n const flat = blocks.flatMap(b => Array.isArray(b) ? b : [b]);\n return flat.find(b => b && (b['@type'] === 'JobPosting' || (Array.isArray(b['@type']) && b['@type'].includes('JobPosting'))));\n};\n\nconst items = $input.all();\nconst out = [];\nfor (let i = 0; i < items.length; i++) {\n const orig = $('Postgres: Select Quarantined LinkedIn').itemMatching(i)?.json || {};\n const httpItem = items[i].json;\n const html = typeof httpItem === 'string' ? httpItem : (httpItem.data || httpItem.body || '');\n const statusCode = items[i].json?.statusCode ?? null;\n\n const base = {\n id: orig.id,\n url: orig.url,\n title: orig.title,\n company: orig.company,\n http_status: statusCode\n };\n\n const jp = extractJobPosting(html);\n if (!jp || !jp.description) {\n out.push({ json: { ...base, recovered: false, reason: 'no_jobposting_jsonld', body_length: 0 } });\n continue;\n }\n\n const desc = stripTags(decodeEntities(jp.description));\n if (desc.length < minBody) {\n out.push({ json: { ...base, recovered: false, reason: `too_short_${desc.length}`, body_length: desc.length } });\n continue;\n }\n\n out.push({ json: { ...base, recovered: true, description: desc, body_length: desc.length, body_preview: desc.slice(0, 240) } });\n}\nreturn out;"
}
},
{
"id": "if-dry-run",
"name": "IF: Dry Run",
"type": "n8n-nodes-base.if",
"typeVersion": 2.2,
"position": [
800,
0
],
"parameters": {
"conditions": {
"options": {
"caseSensitive": true,
"leftValue": "",
"typeValidation": "strict",
"version": 2
},
"conditions": [
{
"id": "is-dry-run",
"leftValue": "={{ $('Set: Config').first().json.dry_run }}",
"rightValue": true,
"operator": {
"type": "boolean",
"operation": "equals"
}
}
],
"combinator": "and"
},
"options": {}
}
},
{
"id": "code-format-preview",
"name": "Code: Format Preview",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1040,
-120
],
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// Dry-run summary. No DB writes. Inspect output in n8n UI before flipping\n// dry_run=false in Set: Config.\nconst rows = $input.all().map(i => i.json);\nconst recovered = rows.filter(r => r.recovered);\nconst failed = rows.filter(r => !r.recovered);\nreturn [{\n json: {\n mode: 'dry_run',\n total: rows.length,\n recovered_count: recovered.length,\n failed_count: failed.length,\n recovery_rate: rows.length ? Math.round((recovered.length / rows.length) * 100) + '%' : '0%',\n recovered_samples: recovered.map(r => ({ id: r.id, title: r.title, company: r.company, body_length: r.body_length, body_preview: r.body_preview })),\n failures: failed.map(r => ({ id: r.id, title: r.title, http_status: r.http_status, reason: r.reason }))\n }\n}];"
}
},
{
"id": "code-filter-recovered",
"name": "Code: Filter Recovered",
"type": "n8n-nodes-base.code",
"typeVersion": 2,
"position": [
1040,
120
],
"parameters": {
"mode": "runOnceForAllItems",
"language": "javaScript",
"jsCode": "// Live mode: only forward rows that actually recovered a body. Failures stay\n// quarantined as-is.\nreturn $input.all().filter(i => i.json && i.json.recovered === true);"
}
},
{
"id": "pg-update-recovered",
"name": "Postgres: Update Recovered",
"type": "n8n-nodes-base.postgres",
"typeVersion": 2,
"position": [
1280,
120
],
"parameters": {
"operation": "executeQuery",
"query": "UPDATE listings SET description = $1, quarantine_reason = NULL, quarantined_at = NULL WHERE id = $2 RETURNING id, LENGTH(description) AS body_length;",
"options": {
"queryReplacement": "={{ $json.description }},{{ $json.id }}"
}
},
"credentials": {
"postgres": {
"name": "<your credential>"
}
}
}
],
"connections": {
"Manual Trigger": {
"main": [
[
{
"node": "Set: Config",
"type": "main",
"index": 0
}
]
]
},
"Set: Config": {
"main": [
[
{
"node": "Postgres: Select Quarantined LinkedIn",
"type": "main",
"index": 0
}
]
]
},
"Postgres: Select Quarantined LinkedIn": {
"main": [
[
{
"node": "HTTP Request: Fetch JD HTML",
"type": "main",
"index": 0
}
]
]
},
"HTTP Request: Fetch JD HTML": {
"main": [
[
{
"node": "Code: Extract JSON-LD Description",
"type": "main",
"index": 0
}
]
]
},
"Code: Extract JSON-LD Description": {
"main": [
[
{
"node": "IF: Dry Run",
"type": "main",
"index": 0
}
]
]
},
"IF: Dry Run": {
"main": [
[
{
"node": "Code: Format Preview",
"type": "main",
"index": 0
}
],
[
{
"node": "Code: Filter Recovered",
"type": "main",
"index": 0
}
]
]
},
"Code: Filter Recovered": {
"main": [
[
{
"node": "Postgres: Update Recovered",
"type": "main",
"index": 0
}
]
]
}
},
"settings": {
"executionOrder": "v1",
"timezone": "Europe/Berlin",
"callerPolicy": "workflowsFromSameOwner",
"saveDataErrorExecution": "all",
"saveDataSuccessExecution": "all",
"saveExecutionProgress": true,
"saveManualExecutions": true
}
}
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
06 - Body Recovery (LinkedIn JSON-LD). Uses postgres, httpRequest. Event-driven trigger; 9 nodes.
Source: https://github.com/ozlar34/job-match-radar/blob/main/workflows/06-body-recovery/body-recovery.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.
Automate your LinkedIn content calendar. This workflow fetches scheduled posts from a PostgreSQL database (Twenty CRM), downloads attached media from SharePoint, and publishes them seamlessly to Linke
Turn your blog into a set-and-forget content engine: every new article is instantly repurposed into channel-specific social posts with visuals, keeping your brand visible on LinkedIn, X, and Reddit wi
This n8n workflow automatically shares content from a Telegram Channel to multiple platforms like WordPress, Facebook, X/Twitter, and LinkedIn. It uses a Switch node to detect the type of content—text
Hacker News to Video Template - AlexK1919. Uses hackerNews, s3, httpRequest, dropbox. Event-driven trigger; 48 nodes.
Disclaimer: this workflow only works on self-hosted instances due to the file system usage.