AutomationFlowsSocial Media › Body Recovery (linkedin Json-ld)

Body Recovery (linkedin Json-ld)

06 - Body Recovery (LinkedIn JSON-LD). Uses postgres, httpRequest. Event-driven trigger; 9 nodes.

Event trigger★★★★☆ complexity9 nodesPostgresHTTP Request
Social Media Trigger: Event Nodes: 9 Complexity: ★★★★☆ Added:

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 →

Download .json
{
  "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(/&amp;/g, '&')\n  .replace(/&lt;/g, '<')\n  .replace(/&gt;/g, '>')\n  .replace(/&quot;/g, '\"')\n  .replace(/&#39;/g, \"'\")\n  .replace(/&nbsp;/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.

Pro

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 →

More Social Media workflows → · Browse all categories →

Related workflows

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

Social Media

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

Postgres, HTTP Request
Social Media

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

HTTP Request, OpenAI, LinkedIn +4
Social Media

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

Telegram Trigger, Telegram, WordPress +5
Social Media

Hacker News to Video Template - AlexK1919. Uses hackerNews, s3, httpRequest, dropbox. Event-driven trigger; 48 nodes.

Hacker News, S3, HTTP Request +6
Social Media

Disclaimer: this workflow only works on self-hosted instances due to the file system usage.

Execute Workflow Trigger, HTTP Request, Form Trigger +3