{
  "id": "5MEc8sTlud67zx8f",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "LinkedIn Top Posts to Google Sheets Content Engine",
  "tags": [],
  "nodes": [
    {
      "id": "8d84cc77-aa9b-4b0b-b6ee-c28631e75a45",
      "name": "Google Sheets Export",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        768,
        1488
      ],
      "parameters": {
        "color": 7,
        "width": 596,
        "height": 332,
        "content": "## Google Sheets export"
      },
      "typeVersion": 1
    },
    {
      "id": "4dfd7269-1bf3-4d1d-8b6d-31241ea48332",
      "name": "Analysis and Generation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        208,
        1488
      ],
      "parameters": {
        "color": 7,
        "width": 544,
        "height": 332,
        "content": "## Analysis and generation"
      },
      "typeVersion": 1
    },
    {
      "id": "2bf9ebb2-deeb-4dda-aa78-15e22137d07f",
      "name": "Collection and Ranking",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -352,
        1488
      ],
      "parameters": {
        "color": 7,
        "width": 556,
        "height": 332,
        "content": "## Post collection and ranking\n"
      },
      "typeVersion": 1
    },
    {
      "id": "930b7fb1-e8b5-40df-bb56-fb3c54a757af",
      "name": "Input and Validation",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1056,
        1488
      ],
      "parameters": {
        "color": 7,
        "width": 692,
        "height": 332,
        "content": "## Input and validation"
      },
      "typeVersion": 1
    },
    {
      "id": "9ef0a92e-5bbf-4d99-adc0-be95901e9411",
      "name": "Overview",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        -1616,
        1200
      ],
      "parameters": {
        "width": 540,
        "height": 1088,
        "content": "# LinkedIn Top Posts to Google Sheets Content Engine\n\n### HOW IT WORKS:\n\nThis workflow helps you study what is already working on LinkedIn and turn those patterns into new post ideas.\n\nIt pulls posts from a LinkedIn source through Apify, ranks them by impressions, sends the strongest examples to OpenAI for analysis, and writes the results to Google Sheets.\n\n### HOW TO SET UP:\n\nAdd your Apify token, LinkedIn actor ID, and OpenAI API key in the \"Set Config\" step.\n\nReplace the example LinkedIn profile URL with the profile you want to analyze.\n\nIn the Google Sheets node, choose the spreadsheet and worksheet where you want the output to go.\n\nBefore running the workflow, make sure your sheet already has these columns:\n\nGenerated At\nSource Profile\nTop Post Rank\nTop Post Text\nTop Post Impressions\nInsight 1\nInsight 2\nInsight 3\nNew Post Title\nNew Post Hook\nNew Post Text\nNew Post Cta\nFormat Type\nWhy It Should Work\nTotal Posts Analyzed\nTop Post Reactions\nTop Post Comments\nTop Post Reposts\nInsight 4\nNew Post Number\n\nOnce that is in place, run the workflow and check the first output row to confirm everything is landing where you expect."
      },
      "typeVersion": 1
    },
    {
      "id": "87804ce5-beea-4980-8fd6-06d94d561c7a",
      "name": "Append Rows to Google Sheet",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        1152,
        1600
      ],
      "parameters": {
        "columns": {
          "value": {
            "Insight 1": "={{$json.insight_1}}",
            "Insight 2": "={{$json.insight_2}}",
            "Insight 3": "={{$json.insight_3}}",
            "Insight 4": "={{$json.insight_4}}",
            "Format Type": "={{$json.format_type}}",
            "Generated At": "={{$json.generated_at}}",
            "New Post Cta": "={{$json.new_post_cta}}",
            "New Post Hook": "={{$json.new_post_hook}}",
            "New Post Text": "={{$json.new_post_text}}",
            "Top Post Rank": "={{$json.source_top_post_rank}}",
            "Top Post Text": "={{$json.source_top_post_text}}",
            "New Post Title": "={{$json.new_post_title}}",
            "Source Profile": "={{$json.source_profile}}",
            "New Post Number": "={{$json.new_post_number}}",
            "Top Post Reposts": "={{$json.source_top_post_reposts}}",
            "Top Post Comments": "={{$json.source_top_post_comments}}",
            "Top Post Reactions": "={{$json.source_top_post_reactions}}",
            "Why It Should Work": "={{$json.why_it_should_work}}",
            "Top Post Impressions": "={{$json.source_top_post_impressions}}",
            "Total Posts Analyzed": "={{$json.total_posts_analyzed}}"
          },
          "schema": [
            {
              "id": "Generated At",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Generated At",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Source Profile",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Source Profile",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Top Post Rank",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Top Post Rank",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Top Post Text",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Top Post Text",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Top Post Impressions",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Top Post Impressions",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Insight 1",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Insight 1",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Insight 2",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Insight 2",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Insight 3",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Insight 3",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "New Post Title",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "New Post Title",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "New Post Hook",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "New Post Hook",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "New Post Text",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "New Post Text",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "New Post Cta",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "New Post Cta",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Format Type",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Format Type",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Why It Should Work",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Why It Should Work",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Total Posts Analyzed",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Total Posts Analyzed",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Top Post Reactions",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Top Post Reactions",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Top Post Comments",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Top Post Comments",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Top Post Reposts",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "Top Post Reposts",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "Insight 4",
              "type": "string",
              "display": true,
              "removed": false,
              "required": false,
              "displayName": "Insight 4",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            },
            {
              "id": "New Post Number",
              "type": "string",
              "display": true,
              "required": false,
              "displayName": "New Post Number",
              "defaultMatch": false,
              "canBeUsedToMatch": true
            }
          ],
          "mappingMode": "defineBelow",
          "matchingColumns": [],
          "attemptToConvertTypes": false,
          "convertFieldsToString": false
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "list",
          "value": "gid=0",
          "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1OnnfqCErNWrvGIGwL4VhLRDUYamUkUrzAvP96O7LfZs/edit#gid=0",
          "cachedResultName": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "url",
          "value": "https://docs.google.com/spreadsheets/d/1OnnfqCErNWrvGIGwL4VhLRDUYamUkUrzAvP96O7LfZs/edit?usp=drivesdk",
          "__regex": "https:\\/\\/(?:drive|docs)\\.google\\.com(?:\\/.*|)\\/d\\/([0-9a-zA-Z\\-_]+)(?:\\/.*|)"
        }
      },
      "credentials": {
        "googleSheetsOAuth2Api": {
          "name": "<your credential>"
        }
      },
      "typeVersion": 4.7
    },
    {
      "id": "d5f4387f-5f82-4e53-a018-3563041f82d6",
      "name": "Prepare Google Sheets Rows",
      "type": "n8n-nodes-base.code",
      "position": [
        864,
        1600
      ],
      "parameters": {
        "jsCode": "const data = $input.first().json;\nconst topPosts = data.top_posts || [];\nconst insights = data.insights || [];\nconst posts = data.generated_posts || [];\nconst generatedAt = new Date().toISOString();\n\nreturn posts.map((post, index) => {\n  const sourcePost = topPosts[index % Math.max(topPosts.length, 1)] || {};\n\n  return {\n    json: {\n      generated_at: generatedAt,\n      source_profile: data.source_profile,\n      total_posts_analyzed: data.total_posts_found,\n      source_top_post_rank: sourcePost.rank ?? '',\n      source_top_post_text: sourcePost.text ?? '',\n      source_top_post_impressions: sourcePost.impressions ?? '',\n      source_top_post_reactions: sourcePost.reactions ?? '',\n      source_top_post_comments: sourcePost.comments ?? '',\n      source_top_post_reposts: sourcePost.reposts ?? '',\n      insight_1: insights[0] ?? '',\n      insight_2: insights[1] ?? '',\n      insight_3: insights[2] ?? '',\n      insight_4: insights[3] ?? '',\n      new_post_number: index + 1,\n      new_post_title: post.title ?? '',\n      new_post_hook: post.hook ?? '',\n      new_post_text: post.post_text ?? '',\n      new_post_cta: post.cta ?? '',\n      format_type: post.format_type ?? '',\n      why_it_should_work: post.why_it_should_work ?? ''\n    }\n  };\n});"
      },
      "typeVersion": 2
    },
    {
      "id": "4c0aca70-fedd-46f7-b269-a69d7015b1a8",
      "name": "Parse OpenAI Output",
      "type": "n8n-nodes-base.code",
      "position": [
        592,
        1600
      ],
      "parameters": {
        "jsCode": "const response = $json;\n\nconst rawText = response?.output?.[0]?.content?.[0]?.text ?? response?.output_text ?? '';\n\nif (!rawText) {\n  throw new Error('No usable text found in the OpenAI response.');\n}\n\nlet parsed;\ntry {\n  parsed = JSON.parse(rawText);\n} catch (error) {\n  throw new Error(`OpenAI JSON could not be parsed: ${error.message}\\n\\nRaw response:\\n${rawText}`);\n}\n\nconst source = $('Normalize + Rank LinkedIn Posts').first().json;\n\nreturn [\n  {\n    json: {\n      source_profile: source.source_profile,\n      total_posts_found: source.total_posts_found,\n      top_posts: source.top_posts,\n      insights: parsed.insights,\n      generated_posts: parsed.posts,\n      generated_count: parsed.posts.length\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "b1233856-aa7b-4ff4-94e2-69497559efad",
      "name": "OpenAI Analyze + Generate",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        304,
        1600
      ],
      "parameters": {
        "url": "https://api.openai.com/v1/responses",
        "method": "POST",
        "options": {
          "response": {
            "response": {}
          }
        },
        "jsonBody": "={{ {\n  model: 'gpt-5.4-mini',\n  input: [\n    {\n      role: 'system',\n      content: 'You are a LinkedIn content strategist and copywriter. Analyze top-performing LinkedIn posts, extract useful patterns, and create original post ideas that fit the platform. Return only valid JSON matching the required schema.'\n    },\n    {\n      role: 'user',\n      content: $json.analysis_prompt\n    }\n  ],\n  text: {\n    format: {\n      type: 'json_schema',\n      name: 'linkedin_content_engine_output',\n      strict: true,\n      schema: {\n        type: 'object',\n        properties: {\n          insights: {\n            type: 'array',\n            minItems: 3,\n            maxItems: 10,\n            items: { type: 'string' }\n          },\n          posts: {\n            type: 'array',\n            minItems: 7,\n            maxItems: 7,\n            items: {\n              type: 'object',\n              properties: {\n                title: { type: 'string' },\n                hook: { type: 'string' },\n                post_text: { type: 'string' },\n                cta: { type: 'string' },\n                format_type: { type: 'string' },\n                why_it_should_work: { type: 'string' }\n              },\n              required: ['title', 'hook', 'post_text', 'cta', 'format_type', 'why_it_should_work'],\n              additionalProperties: false\n            }\n          }\n        },\n        required: ['insights', 'posts'],\n        additionalProperties: false\n      }\n    }\n  }\n} }}",
        "sendBody": true,
        "sendHeaders": true,
        "specifyBody": "json",
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{$node[\"Set Config\"].json[\"openai_api_key\"]}}"
            },
            {
              "name": "Content-Type",
              "value": "application/json"
            }
          ]
        }
      },
      "typeVersion": 4.2
    },
    {
      "id": "e28d19a2-b9df-495c-a6a3-b12df0fd19dd",
      "name": "Normalize + Rank LinkedIn Posts",
      "type": "n8n-nodes-base.code",
      "position": [
        32,
        1600
      ],
      "parameters": {
        "jsCode": "const items = $input.all();\n\nconst posts = items\n  .map((item, index) => {\n    const p = item.json;\n\n    const text = (\n      p.text ??\n      p.postText ??\n      p.content ??\n      p.caption ??\n      p.description ??\n      p.fullText ??\n      p.body ??\n      p.linkedinText ??\n      p.postContent ??\n      ''\n    ).toString().trim();\n\n    const impressions = Number(\n      p.impressions ??\n      p.impressionCount ??\n      p.metrics?.impressions ??\n      p.statistics?.impressions ??\n      p.analytics?.impressions ??\n      p.views ??\n      p.viewCount ??\n      p.engagementStats?.impressions ??\n      0\n    );\n\n    const reactions = Number(\n      p.reactions ??\n      p.reactionCount ??\n      p.likes ??\n      p.likeCount ??\n      p.metrics?.reactions ??\n      p.statistics?.reactions ??\n      0\n    );\n\n    const comments = Number(\n      p.comments ??\n      p.commentCount ??\n      p.metrics?.comments ??\n      p.statistics?.comments ??\n      0\n    );\n\n    const reposts = Number(\n      p.reposts ??\n      p.shares ??\n      p.shareCount ??\n      p.metrics?.shares ??\n      p.statistics?.shares ??\n      0\n    );\n\n    return {\n      rank: 0,\n      id: p.id ?? p.postId ?? p.urn ?? `post_${index + 1}`,\n      text,\n      impressions,\n      reactions,\n      comments,\n      reposts,\n      post_url: p.url ?? p.postUrl ?? p.linkedinUrl ?? '',\n      published_at: p.publishedAt ?? p.createdAt ?? p.timestamp ?? '',\n      raw: p\n    };\n  })\n  .filter((post) => post.text && !Number.isNaN(post.impressions));\n\nif (!posts.length) {\n  throw new Error('No LinkedIn posts with usable text and impression data were found in the Apify response.');\n}\n\nposts.sort((a, b) => b.impressions - a.impressions);\nposts.forEach((post, index) => {\n  post.rank = index + 1;\n});\n\nconst topPosts = posts.slice(0, Math.min(5, posts.length));\n\nconst summaryForPrompt = topPosts.map((post) => ({\n  rank: post.rank,\n  id: post.id,\n  impressions: post.impressions,\n  reactions: post.reactions,\n  comments: post.comments,\n  reposts: post.reposts,\n  text: post.text\n}));\n\nconst analysisPrompt = `Analyze the following top-performing LinkedIn posts ranked by impressions.\\n\\nTop posts:\\n${JSON.stringify(summaryForPrompt, null, 2)}\\n\\nTasks:\\n1. Identify patterns in hooks, tone, structure, readability, CTA style, formatting, topic angle, and repeated language choices.\\n2. Summarize the most important success factors.\\n3. Write 7 completely new LinkedIn posts inspired by these patterns, but do not copy phrasing or structure too closely.\\n4. Write in natural, polished English for a professional audience.\\n5. Make the posts feel native to LinkedIn: strong opening line, short readable paragraphs, clear idea progression, and thoughtful closing line.\\n6. Avoid filler, generic motivation, and repeated wording.\\n7. Return only JSON matching the schema.`;\n\nreturn [\n  {\n    json: {\n      source_profile: $('Validate Config + Build Actor Input').first().json.linkedin_profile_url,\n      total_posts_found: posts.length,\n      top_posts: topPosts,\n      summary_for_prompt: summaryForPrompt,\n      analysis_prompt: analysisPrompt\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "b60aabbb-da40-4375-aa04-c0d1f5e6801f",
      "name": "Apify Get LinkedIn Posts",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        -256,
        1600
      ],
      "parameters": {
        "url": "=https://api.apify.com/v2/acts/{{$json.apify_actor_id}}/run-sync-get-dataset-items?token={{$json.apify_token}}&clean=true&format=json",
        "method": "POST",
        "options": {
          "response": {
            "response": {}
          }
        },
        "jsonBody": "={{ $json.actor_input }}",
        "sendBody": true,
        "specifyBody": "json"
      },
      "typeVersion": 4.2
    },
    {
      "id": "cd361e5b-c1ca-448d-ab98-ec9c4b55a608",
      "name": "Validate Config + Build Actor Input",
      "type": "n8n-nodes-base.code",
      "position": [
        -544,
        1600
      ],
      "parameters": {
        "jsCode": "const config = $input.first().json;\n\nconst requiredFields = ['apify_token', 'apify_actor_id', 'openai_api_key', 'linkedin_profile_url'];\nconst missing = requiredFields.filter((field) => {\n  const value = config[field];\n  return value === undefined || value === null || value === '' || String(value).startsWith('replace_with_your_');\n});\n\nif (missing.length) {\n  throw new Error(`Missing required configuration fields: ${missing.join(', ')}`);\n}\n\nreturn [\n  {\n    json: {\n      ...config,\n      actor_input: {\n        profileUrls: [config.linkedin_profile_url],\n        resultsLimit: Number(config.results_limit || 20)\n      }\n    }\n  }\n];"
      },
      "typeVersion": 2
    },
    {
      "id": "19a507b5-03d2-4196-bc5b-ec050fdb9bf2",
      "name": "Set Config",
      "type": "n8n-nodes-base.set",
      "position": [
        -784,
        1600
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "cfg-apify-token",
              "name": "apify_token",
              "type": "string",
              "value": "replace_with_your_apify_token"
            },
            {
              "id": "cfg-apify-actor-id",
              "name": "apify_actor_id",
              "type": "string",
              "value": "replace_with_your_linkedin_actor_id"
            },
            {
              "id": "cfg-openai-key",
              "name": "openai_api_key",
              "type": "string",
              "value": "replace_with_your_openai_api_key"
            },
            {
              "id": "cfg-linkedin-profile-url",
              "name": "linkedin_profile_url",
              "type": "string",
              "value": "https://www.linkedin.com/in/your-profile/"
            },
            {
              "id": "cfg-results-limit",
              "name": "results_limit",
              "type": "number",
              "value": 20
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "17179595-096a-44fe-a18b-e6ffa6f585b4",
      "name": "Manual Trigger",
      "type": "n8n-nodes-base.manualTrigger",
      "position": [
        -1008,
        1600
      ],
      "parameters": {},
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "4ea02da9-1233-45c8-aaa8-be2c720ef809",
  "connections": {
    "Set Config": {
      "main": [
        [
          {
            "node": "Validate Config + Build Actor Input",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Manual Trigger": {
      "main": [
        [
          {
            "node": "Set Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse OpenAI Output": {
      "main": [
        [
          {
            "node": "Prepare Google Sheets Rows",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Apify Get LinkedIn Posts": {
      "main": [
        [
          {
            "node": "Normalize + Rank LinkedIn Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "OpenAI Analyze + Generate": {
      "main": [
        [
          {
            "node": "Parse OpenAI Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Prepare Google Sheets Rows": {
      "main": [
        [
          {
            "node": "Append Rows to Google Sheet",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize + Rank LinkedIn Posts": {
      "main": [
        [
          {
            "node": "OpenAI Analyze + Generate",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Validate Config + Build Actor Input": {
      "main": [
        [
          {
            "node": "Apify Get LinkedIn Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}