{
  "name": "02 - Fetch Post Engagement",
  "settings": {
    "executionOrder": "v1"
  },
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "hours",
              "hoursInterval": 6
            }
          ]
        }
      },
      "id": "trigger-6h",
      "name": "Every 6 Hours",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1.2,
      "position": [
        -240,
        0
      ]
    },
    {
      "parameters": {
        "operation": "executeQuery",
        "query": "SELECT id, linkedin_urn\nFROM posts\nWHERE status = 'posted'\n  AND linkedin_urn IS NOT NULL\n  AND posted_at > now() - interval '14 days'\nORDER BY posted_at DESC;",
        "options": {}
      },
      "id": "get-recent-posts",
      "name": "Get Recent Posts",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        -20,
        0
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      },
      "notes": "Returns one item per post. Every downstream node runs once per item."
    },
    {
      "parameters": {
        "method": "GET",
        "url": "=https://api.linkedin.com/rest/socialActions/{{ encodeURIComponent($json.linkedin_urn) }}",
        "authentication": "genericCredentialType",
        "genericAuthType": "httpHeaderAuth",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "LinkedIn-Version",
              "value": "202601"
            },
            {
              "name": "X-Restli-Protocol-Version",
              "value": "2.0.0"
            }
          ]
        },
        "options": {
          "response": {
            "response": {
              "neverError": true,
              "fullResponse": true
            }
          }
        }
      },
      "id": "get-social-actions",
      "name": "Get Likes and Comments",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [
        200,
        0
      ],
      "credentials": {
        "httpHeaderAuth": {
          "name": "<your credential>"
        }
      },
      "notes": "neverError keeps one bad post from aborting the whole batch; fullResponse exposes statusCode so the next node can tell a genuine zero from a failed request."
    },
    {
      "parameters": {
        "mode": "runOnceForEachItem",
        "jsCode": "// Runs once per post. Distinguishes a real zero from a failed lookup so that\n// an auth error or a deleted post is never recorded as \"0 engagement\".\nconst res = $json;\nconst post = $('Get Recent Posts').item.json;\nconst body = res.body || {};\nconst ok = res.statusCode >= 200 && res.statusCode < 300;\n\nreturn {\n  json: {\n    post_id: post.id,\n    ok,\n    status_code: res.statusCode,\n    likes: ok ? (body.likesSummary?.totalLikes ?? 0) : null,\n    comments: ok ? (body.commentsSummary?.totalFirstLevelComments ?? 0) : null,\n    reason: ok ? null : (body.message || `LinkedIn returned ${res.statusCode}`)\n  }\n};"
      },
      "id": "normalize-stats",
      "name": "Normalize Stats",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        420,
        0
      ],
      "notes": "runOnceForEachItem is REQUIRED. In the default run-once-for-all-items mode this node would silently process only the first post and discard the rest."
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": true,
            "leftValue": "",
            "typeValidation": "strict",
            "version": 2
          },
          "conditions": [
            {
              "id": "fetch-succeeded",
              "leftValue": "={{ $json.ok }}",
              "rightValue": "",
              "operator": {
                "type": "boolean",
                "operation": "true",
                "singleValue": true
              }
            }
          ],
          "combinator": "and"
        },
        "options": {}
      },
      "id": "filter-ok",
      "name": "Fetch Succeeded?",
      "type": "n8n-nodes-base.if",
      "typeVersion": 2.2,
      "position": [
        640,
        0
      ],
      "notes": "Only successful lookups are written. Failed ones fall out of the false branch and are skipped, leaving the previous good snapshot as the latest."
    },
    {
      "parameters": {
        "operation": "insert",
        "schema": {
          "value": "public"
        },
        "table": {
          "value": "post_stats"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "post_id": "={{ $json.post_id }}",
            "likes": "={{ $json.likes }}",
            "comments": "={{ $json.comments }}"
          }
        },
        "options": {}
      },
      "id": "save-stats",
      "name": "Save Stats to Postgres",
      "type": "n8n-nodes-base.postgres",
      "typeVersion": 2.5,
      "position": [
        880,
        -80
      ],
      "credentials": {
        "postgres": {
          "name": "<your credential>"
        }
      }
    },
    {
      "parameters": {
        "jsCode": "// Failed lookups land here. Nothing is written to the database; this node\n// exists so the failures are visible in the execution log.\nfor (const item of $input.all()) {\n  console.log(`Skipped post ${item.json.post_id}: ${item.json.reason}`);\n}\nreturn $input.all();"
      },
      "id": "log-skipped",
      "name": "Log Skipped",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        880,
        100
      ]
    }
  ],
  "connections": {
    "Every 6 Hours": {
      "main": [
        [
          {
            "node": "Get Recent Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Recent Posts": {
      "main": [
        [
          {
            "node": "Get Likes and Comments",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get Likes and Comments": {
      "main": [
        [
          {
            "node": "Normalize Stats",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize Stats": {
      "main": [
        [
          {
            "node": "Fetch Succeeded?",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Fetch Succeeded?": {
      "main": [
        [
          {
            "node": "Save Stats to Postgres",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Skipped",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}