{
  "id": "7AinkHj2WNEeixb0",
  "meta": {
    "templateCredsSetupCompleted": true
  },
  "name": "Social analytics",
  "tags": [],
  "nodes": [
    {
      "id": "0222e36c-a40a-4f64-b370-242ee629a445",
      "name": "Every Monday 9 AM",
      "type": "n8n-nodes-base.scheduleTrigger",
      "position": [
        3984,
        2864
      ],
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "weeks",
              "triggerAtDay": 1,
              "triggerAtHour": 9
            }
          ]
        }
      },
      "typeVersion": 1.1
    },
    {
      "id": "75fdb746-6543-4289-8c4b-c63b8d770f51",
      "name": "WES Config",
      "type": "n8n-nodes-base.set",
      "position": [
        4208,
        2864
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "w1",
              "name": "weight_like",
              "type": "number",
              "value": 1
            },
            {
              "id": "w2",
              "name": "weight_comment",
              "type": "number",
              "value": 3
            },
            {
              "id": "w3",
              "name": "weight_share",
              "type": "number",
              "value": 5
            },
            {
              "id": "w4",
              "name": "weight_save",
              "type": "number",
              "value": 4
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "d3645c22-520c-4200-8b7f-97cb7412772c",
      "name": "Get LinkedIn Posts",
      "type": "n8n-nodes-base.httpRequest",
      "position": [
        4528,
        2608
      ],
      "settings": {
        "alwaysOutputData": true
      },
      "parameters": {
        "url": "=https://api.linkedin.com/v2/ugcPosts?q=authors&authors=List({{$env.LINKEDIN_OWNER_URN}})&count=100",
        "options": {},
        "sendHeaders": true,
        "authentication": "predefinedCredentialType",
        "headerParameters": {
          "parameters": [
            {
              "name": "LinkedIn-Version",
              "value": "202304"
            }
          ]
        },
        "nodeCredentialType": "linkedInOAuth2Api"
      },
      "typeVersion": 4.2,
      "continueOnFail": true
    },
    {
      "id": "ecd61a8b-c77f-4cc1-b31a-c5fc92656b91",
      "name": "Search Tweets",
      "type": "n8n-nodes-base.twitter",
      "position": [
        4528,
        2848
      ],
      "settings": {
        "alwaysOutputData": true
      },
      "parameters": {
        "limit": 100,
        "operation": "search",
        "searchText": "={{$env.TWITTER_SEARCH_QUERY}}",
        "additionalFields": {
          "startTime": "={{$now.minus({days: 7}).toISO()}}"
        }
      },
      "typeVersion": 2,
      "continueOnFail": true
    },
    {
      "id": "d8261e5c-934a-4761-ac33-52cb6df0ee46",
      "name": "Get media",
      "type": "@mookielianhd/n8n-nodes-instagram.instagram",
      "position": [
        4528,
        3040
      ],
      "settings": {
        "alwaysOutputData": true
      },
      "parameters": {
        "limit": 100,
        "resource": "igUser",
        "operation": "getMedia",
        "graphApiVersion": "v24.0"
      },
      "typeVersion": 1,
      "continueOnFail": true
    },
    {
      "id": "2caeebbd-37fa-492f-9fcb-09c871782899",
      "name": "Merge LI & TW",
      "type": "n8n-nodes-base.merge",
      "position": [
        4800,
        2720
      ],
      "parameters": {},
      "typeVersion": 2.1
    },
    {
      "id": "0454934a-3f2b-427c-b889-d2ce0fc4531f",
      "name": "Merge All Streams",
      "type": "n8n-nodes-base.merge",
      "position": [
        5024,
        2848
      ],
      "parameters": {},
      "typeVersion": 2.1
    },
    {
      "id": "44a305b6-b7a4-445d-bf48-5e452942e15f",
      "name": "Normalize & Validate Posts",
      "type": "n8n-nodes-base.code",
      "position": [
        5328,
        2752
      ],
      "parameters": {
        "jsCode": "// Weights from WES Config node\nconst weights  = $('WES Config').first().json;\nconst wLike    = weights.weight_like    ?? 1;\nconst wComment = weights.weight_comment ?? 3;\nconst wShare   = weights.weight_share   ?? 5;\nconst wSave    = weights.weight_save    ?? 4;\n\n// Use native JS Date.now() \u2014 safe in all n8n hosting environments\nconst weekAgo = Date.now() - (7 * 24 * 60 * 60 * 1000);\n\nreturn $input.all().reduce((acc, item) => {\n  const d = item.json;\n  if (!d || Object.keys(d).length === 0) return acc;\n\n  // Platform detection\n  const isLinkedIn  = d.firstPublishedAt !== undefined || d.specificContent !== undefined;\n  const isInstagram = d.permalink !== undefined && d.timestamp !== undefined;\n  const isTwitter   = d.public_metrics !== undefined;\n\n  // Direct timestamp extraction \u2014 no intermediate ISO string conversion\n  let ts = null;\n  if (d.firstPublishedAt)                                   ts = new Date(d.firstPublishedAt).getTime();\n  else if (d.publishedDate || d.created_at || d.timestamp) ts = new Date(d.publishedDate || d.created_at || d.timestamp).getTime();\n\n  if (!ts || isNaN(ts) || ts < weekAgo) return acc;\n\n  const platform = d.platform\n    || (isLinkedIn  ? 'linkedin'\n    :  isInstagram  ? 'instagram'\n    :  isTwitter    ? 'twitter'\n    : 'unknown');\n\n  const pm       = d.public_metrics || {};\n  const likes    = pm.like_count     || d.like_count     || d.likes    || d.likeCount        || 0;\n  const comments = pm.reply_count    || d.comments_count || d.comments || d.reply_count      || 0;\n  const shares   = pm.retweet_count  || d.shares         || d.retweet_count || d.shareCount  || 0;\n  const saves    = pm.bookmark_count || d.saves          || d.bookmark_count                 || 0;\n  const impressions = pm.impression_count\n    || d.impressions || d.view_count || d.reach\n    || d.statistics?.totalShareStatistics?.impressionCount\n    || null;\n\n  const liText  = d.specificContent?.['com.linkedin.ugc.ShareContent']?.shareCommentary?.text || '';\n  const rawText = liText || d.text || d.caption || '';\n\n  const twitterUrl = isTwitter && d.id\n    ? 'https://twitter.com/i/web/status/' + d.id\n    : null;\n  const url = d.url || d.permalink || twitterUrl || '#';\n\n  acc.push({\n    json: {\n      _platform:    platform,\n      _date:        new Date(ts).toISOString(),\n      _likes:       likes,\n      _comments:    comments,\n      _shares:      shares,\n      _saves:       saves,\n      _impressions: impressions,\n      _text:        rawText.trim(),\n      _url:         url,\n      _wLike:       wLike,\n      _wComment:    wComment,\n      _wShare:      wShare,\n      _wSave:       wSave\n    }\n  });\n\n  return acc;\n}, []);"
      },
      "typeVersion": 2
    },
    {
      "id": "ab264826-54f3-42c9-9cd9-a95d2f31756e",
      "name": "Loop Over Batches",
      "type": "n8n-nodes-base.splitInBatches",
      "position": [
        5680,
        2560
      ],
      "parameters": {
        "options": {},
        "batchSize": 50
      },
      "typeVersion": 3
    },
    {
      "id": "58ea49ce-4ebc-43c3-a959-42b6a060c906",
      "name": "Log Raw Posts to Sheets",
      "type": "n8n-nodes-base.googleSheets",
      "position": [
        5856,
        2704
      ],
      "parameters": {
        "columns": {
          "value": {
            "URL": "={{$json._url}}",
            "Date": "={{$json._date}}",
            "Content": "={{$json._text.slice(0, 100)}}",
            "Platform": "={{$json._platform}}",
            "Engagement": "={{$json._likes + $json._comments + $json._shares + $json._saves}}",
            "Impressions": "={{$json._impressions ?? 'unknown'}}"
          },
          "mappingMode": "defineBelow"
        },
        "options": {},
        "operation": "append",
        "sheetName": {
          "__rl": true,
          "mode": "name",
          "value": "Sheet1"
        },
        "documentId": {
          "__rl": true,
          "mode": "id",
          "value": "={{$env.ANALYTICS_SPREADSHEET_ID}}"
        }
      },
      "typeVersion": 4.4,
      "continueOnFail": true
    },
    {
      "id": "01fbedd7-b27d-457a-937f-b8d9d392bb7d",
      "name": "Capture Sheet Errors",
      "type": "n8n-nodes-base.code",
      "position": [
        6192,
        2720
      ],
      "parameters": {
        "jsCode": "const errors = $input.all()\n  .filter(i => i.json.error)\n  .map(i => i.json.error?.message || 'Unknown error writing to Sheets');\n\nreturn [{ json: { sheetErrors: errors, sheetErrorCount: errors.length } }];"
      },
      "typeVersion": 2
    },
    {
      "id": "59b495a0-ba79-4232-b76f-f82d31423f32",
      "name": "Calculate Analytics (WES)",
      "type": "n8n-nodes-base.code",
      "position": [
        5664,
        3104
      ],
      "parameters": {
        "jsCode": "// Use native JS Date.now() \u2014 safe in all n8n hosting environments\nconst items = $input.all();\nlet totalImpressions = 0;\nlet totalWesSum = 0;\nlet postsWithImpressions = 0;\nlet postsWithoutImpressions = 0;\nlet bestPost = null;\nlet worstPost = null;\nconst platformStats = {};\n\nitems.forEach(item => {\n  const d = item.json;\n  if (!d || Object.keys(d).length === 0) return;\n\n  const wLike    = d._wLike    ?? 1;\n  const wComment = d._wComment ?? 3;\n  const wShare   = d._wShare   ?? 5;\n  const wSave    = d._wSave    ?? 4;\n\n  const platform    = d._platform;\n  const likes       = d._likes    || 0;\n  const comments    = d._comments || 0;\n  const shares      = d._shares   || 0;\n  const saves       = d._saves    || 0;\n  const impressions = d._impressions;\n  const wes         = (wLike * likes) + (wComment * comments) + (wShare * shares) + (wSave * saves);\n\n  if (!platformStats[platform]) {\n    platformStats[platform] = { posts: 0, impressions: 0, wesSum: 0, postsWithImpressions: 0 };\n  }\n  platformStats[platform].posts++;\n\n  if (impressions !== null && impressions > 0) {\n    const rate = (wes / impressions) * 100;\n    platformStats[platform].impressions          += impressions;\n    platformStats[platform].wesSum               += wes;\n    platformStats[platform].postsWithImpressions++;\n    totalImpressions    += impressions;\n    totalWesSum         += wes;\n    postsWithImpressions++;\n\n    const postPayload = { platform, text: d._text || 'Media post', rate, url: d._url };\n    if (!bestPost  || rate > bestPost.rate)  bestPost  = postPayload;\n    if (!worstPost || rate < worstPost.rate) worstPost = postPayload;\n  } else {\n    postsWithoutImpressions++;\n  }\n});\n\nconst platformRankings = Object.entries(platformStats)\n  .map(([name, s]) => ({\n    name,\n    avgEngagementRate: s.postsWithImpressions > 0\n      ? ((s.wesSum / s.impressions) * 100).toFixed(2)\n      : 'N/A',\n    totalPosts:       s.posts,\n    totalImpressions: s.impressions\n  }))\n  .sort((a, b) => parseFloat(b.avgEngagementRate) - parseFloat(a.avgEngagementRate));\n\n// Native JS date math \u2014 no Luxon dependency\nconst now      = new Date();\nconst weekAgo  = new Date(now.getTime() - (7 * 24 * 60 * 60 * 1000));\nconst toISODate = d => d.toISOString().slice(0, 10);\n\nreturn [{\n  json: {\n    weekStart:               toISODate(weekAgo),\n    weekEnd:                 toISODate(now),\n    totalPosts:              items.length,\n    postsWithImpressions,\n    postsWithoutImpressions,\n    totalImpressions,\n    avgEngagementRate: postsWithImpressions > 0\n      ? ((totalWesSum / totalImpressions) * 100).toFixed(2)\n      : 'N/A',\n    platformRankings,\n    bestPost,\n    worstPost\n  }\n}];"
      },
      "typeVersion": 2
    },
    {
      "id": "e39147ed-6397-42f3-a815-7dba43dc7ef1",
      "name": "AI Strategy Insights",
      "type": "@n8n/n8n-nodes-langchain.openAi",
      "position": [
        6000,
        3104
      ],
      "parameters": {
        "modelId": {
          "__rl": true,
          "mode": "list",
          "value": "gpt-4o"
        },
        "options": {
          "temperature": 0.7
        },
        "messages": {
          "values": [
            {
              "content": "=Based on this week's content performance data, provide 3 actionable recommendations for improving our social media strategy.\n\n**Overall Stats:**\n- Total Posts: {{$json.totalPosts}} ({{$json.postsWithImpressions}} with impression data, {{$json.postsWithoutImpressions}} without)\n- Avg Engagement Rate (WES): {{$json.avgEngagementRate}}%\n\n**Platform Rankings:**\n{{$json.platformRankings.map(p => `${p.name}: ${p.avgEngagementRate}% (${p.totalPosts} posts, ${p.totalImpressions} impressions)`).join('\\n')}}\n\n**Best Performing Post ({{$json.bestPost ? $json.bestPost.rate.toFixed(2) : 'N/A'}}%):**\n{{$json.bestPost ? $json.bestPost.text : 'No posts with impression data this week'}}\n\n**Worst Performing Post ({{$json.worstPost ? $json.worstPost.rate.toFixed(2) : 'N/A'}}%):**\n{{$json.worstPost ? $json.worstPost.text : 'No posts with impression data this week'}}\n\nCRITICAL OUTPUT FORMATTING:\nReturn your complete response in clean, semantic HTML format.\nUse ONLY <h3> for section titles, <p> for body text, <strong> for emphasis, and <ul>/<li> for action items.\nDo NOT wrap the response in markdown code blocks. Begin output directly with the first HTML tag."
            }
          ]
        }
      },
      "typeVersion": 1.1,
      "continueOnFail": true
    },
    {
      "id": "05b5400f-9a6b-4dc1-984c-0b66afd31fea",
      "name": "Wait for All Branches",
      "type": "n8n-nodes-base.merge",
      "position": [
        6464,
        2656
      ],
      "parameters": {
        "mode": "combine",
        "options": {},
        "combinationMode": "mergeByPosition"
      },
      "typeVersion": 2.1
    },
    {
      "id": "0faf1bf2-e313-436c-b5a1-fb5709b88242",
      "name": "Combine Data & AI Output",
      "type": "n8n-nodes-base.set",
      "position": [
        6720,
        2656
      ],
      "parameters": {
        "options": {},
        "assignments": {
          "assignments": [
            {
              "id": "1",
              "name": "weekStart",
              "type": "string",
              "value": "={{ $('Calculate Analytics (WES)').first().json.weekStart }}"
            },
            {
              "id": "2",
              "name": "weekEnd",
              "type": "string",
              "value": "={{ $('Calculate Analytics (WES)').first().json.weekEnd }}"
            },
            {
              "id": "3",
              "name": "totalPosts",
              "type": "number",
              "value": "={{ $('Calculate Analytics (WES)').first().json.totalPosts }}"
            },
            {
              "id": "4",
              "name": "postsWithImpressions",
              "type": "number",
              "value": "={{ $('Calculate Analytics (WES)').first().json.postsWithImpressions }}"
            },
            {
              "id": "5",
              "name": "postsWithoutImpressions",
              "type": "number",
              "value": "={{ $('Calculate Analytics (WES)').first().json.postsWithoutImpressions }}"
            },
            {
              "id": "6",
              "name": "totalImpressions",
              "type": "number",
              "value": "={{ $('Calculate Analytics (WES)').first().json.totalImpressions }}"
            },
            {
              "id": "7",
              "name": "avgEngagementRate",
              "type": "string",
              "value": "={{ $('Calculate Analytics (WES)').first().json.avgEngagementRate }}"
            },
            {
              "id": "8",
              "name": "sheetErrorCount",
              "type": "number",
              "value": "={{ $('Capture Sheet Errors').all(0, 0).reduce((total, item) => total + (item.json.sheetErrorCount || 0), 0) }}"
            },
            {
              "id": "9",
              "name": "sheetErrors",
              "type": "array",
              "value": "={{ $('Capture Sheet Errors').all(0, 0).flatMap(item => item.json.sheetErrors || []) }}"
            },
            {
              "id": "10",
              "name": "aiInsights",
              "type": "string",
              "value": "={{ $('AI Strategy Insights').first().json.error ? '<p><em>AI insights unavailable this week: ' + $('AI Strategy Insights').first().json.error.message + '</em></p>' : ($('AI Strategy Insights').first().json.message.content || '').replace(/```html|```/g, '') }}"
            }
          ]
        }
      },
      "typeVersion": 3.4
    },
    {
      "id": "dcda36dd-3c67-4350-97f5-f4da2ae5d2dd",
      "name": "Email Weekly Report",
      "type": "n8n-nodes-base.emailSend",
      "position": [
        6928,
        2656
      ],
      "parameters": {
        "html": "=<html>\n<body style=\"font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto;\">\n  <h1>\ud83d\udcca Weekly Content Performance Report</h1>\n  <p><strong>Period:</strong> {{$json.weekStart}} to {{$json.weekEnd}}</p>\n  ={{ $json.sheetErrorCount > 0 ? '<div style=\"background:#fff3cd;border:1px solid #ffc107;padding:12px;border-radius:4px;margin-bottom:16px;\"><strong>\u26a0\ufe0f Warning:</strong> ' + $json.sheetErrorCount + ' post(s) failed to log to Google Sheets.<br>Errors: ' + ($json.sheetErrors || []).join(', ') + '</div>' : '' }}\n  <h2>Key Metrics</h2>\n  <table style=\"border-collapse: collapse; width: 100%;\">\n    <tr style=\"background-color: #f0f0f0;\">\n      <td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Total Posts</strong></td>\n      <td style=\"padding: 10px; border: 1px solid #ddd;\">{{$json.totalPosts}}</td>\n    </tr>\n    <tr>\n      <td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Posts with impression data</strong></td>\n      <td style=\"padding: 10px; border: 1px solid #ddd;\">{{$json.postsWithImpressions}} / {{$json.totalPosts}}</td>\n    </tr>\n    <tr style=\"background-color: #f0f0f0;\">\n      <td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Total Impressions</strong></td>\n      <td style=\"padding: 10px; border: 1px solid #ddd;\">{{Number($json.totalImpressions).toLocaleString()}}</td>\n    </tr>\n    <tr>\n      <td style=\"padding: 10px; border: 1px solid #ddd;\"><strong>Avg Engagement Rate (WES)</strong></td>\n      <td style=\"padding: 10px; border: 1px solid #ddd;\">{{$json.avgEngagementRate}}%</td>\n    </tr>\n  </table>\n  <h2>\ud83d\udca1 AI Strategy Recommendations</h2>\n  <div style=\"background: #fff3e0; padding: 15px; border-radius: 5px;\">\n    {{$json.aiInsights}}\n  </div>\n</body>\n</html>",
        "options": {},
        "subject": "=Weekly Content Performance Report ({{$json.weekStart}} - {{$json.weekEnd}}){{$json.sheetErrorCount > 0 ? ' \u26a0\ufe0f Sheet errors' : ''}}",
        "toEmail": "user@example.com",
        "fromEmail": "user@example.com"
      },
      "typeVersion": 2.1
    },
    {
      "id": "b886cc87-5743-47bf-8568-5e63e044bbc9",
      "name": "Sticky Note",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        4416,
        2480
      ],
      "parameters": {
        "color": 7,
        "width": 784,
        "height": 784,
        "content": "### Multi-Platform Data Fetch\nPulls the last 7 days of posts from LinkedIn, X (Twitter), and Instagram in\nparallel, merges LinkedIn and X first, then combines all three streams into\na single unified feed for downstream processing."
      },
      "typeVersion": 1
    },
    {
      "id": "d5fce582-efcd-4085-891f-07a209ccb200",
      "name": "Sticky Note3",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5232,
        2576
      ],
      "parameters": {
        "color": 7,
        "width": 304,
        "height": 480,
        "content": "### Normalization & Validation\nDetects the platform of each post, extracts engagement metrics into a unified schema, filters out posts older than 7 days, and drops any empty items before passing clean data to both downstream branches."
      },
      "typeVersion": 1
    },
    {
      "id": "ff20cbd7-5a33-4ba2-b035-474fdd6ab4e2",
      "name": "Sticky Note4",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3840,
        2720
      ],
      "parameters": {
        "color": 7,
        "width": 544,
        "height": 368,
        "content": "### Trigger & Weight Config\nFires every Monday at 9 AM and sets the engagement weights used across the entire pipeline: likes (1), comments (3), shares (5), saves (4)."
      },
      "typeVersion": 1
    },
    {
      "id": "34c00412-a56f-42ad-92ca-6973a4fb02ea",
      "name": "Sticky Note5",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5568,
        2944
      ],
      "parameters": {
        "color": 7,
        "width": 768,
        "height": 384,
        "content": "### WES Calculation & AI Insights\nCalculates the Weighted Engagement Score for each post and aggregates platform rankings, best and worst performers, and overall stats. GPT-4o then generates 3 actionable strategy recommendations based on the week's performance data."
      },
      "typeVersion": 1
    },
    {
      "id": "ffbbd2b6-858a-4c20-8a42-b9756199bd43",
      "name": "Sticky Note7",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        5568,
        2416
      ],
      "parameters": {
        "color": 7,
        "width": 768,
        "height": 496,
        "content": "### Raw Post Logging\nIterates over normalized posts in batches of 50 and appends each one to a Google Sheets log with date, platform, content preview, impressions, engagement score, and URL. Sheet write errors are captured without crashing the loop."
      },
      "typeVersion": 1
    },
    {
      "id": "193530a4-e194-4a4b-b45e-fa418995de6a",
      "name": "Sticky Note8",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        6384,
        2416
      ],
      "parameters": {
        "color": 7,
        "width": 720,
        "height": 496,
        "content": "### Report Assembly & Delivery\nWaits for both the Sheets logging and AI insights branches to complete, merges all analytics data and AI output into a single payload, and sends a formatted HTML email report with key metrics, platform rankings, and strategy recommendations."
      },
      "typeVersion": 1
    },
    {
      "id": "5d09c70d-a04c-4172-b92d-a509a0dca327",
      "name": "Sticky Note1",
      "type": "n8n-nodes-base.stickyNote",
      "position": [
        3024,
        2416
      ],
      "parameters": {
        "width": 768,
        "height": 912,
        "content": "## Weekly Social Media Analytics: WES Scoring, AI Insights, and Automated Email Report\n\n### Cross-Platform Content Performance Reporting via LinkedIn, X, and Instagram\n\n### How it works\nThis workflow runs every Monday at 9 AM, pulls the last 7 days of posts from LinkedIn, X (Twitter), and Instagram, calculates a Weighted Engagement Score for each post, generates AI strategy recommendations with GPT-4o, logs raw data to Google Sheets, and delivers a formatted HTML report by email.\n\n### Setup steps\n- [ ] Set the LINKEDIN_OWNER_URN environment variable to your LinkedIn URN\n      (format: urn:li:person:XXXXXXXX or urn:li:organization:XXXXXXXX).\n- [ ] Set the TWITTER_SEARCH_QUERY environment variable to the search query\n      or handle you want to monitor (e.g. from:yourusername).\n- [ ] Set the ANALYTICS_SPREADSHEET_ID environment variable to your Google\n      Sheets document ID where raw posts should be logged.\n- [ ] Connect your LinkedIn OAuth2 credential to the Get LinkedIn Posts node.\n- [ ] Connect your X (Twitter) credential to the Search Tweets node.\n- [ ] Connect your Instagram credential to the Get media node.\n- [ ] Connect your OpenAI credential to the AI Strategy Insights node.\n- [ ] Connect your Google Sheets credential to the Log Raw Posts to Sheets node.\n- [ ] Update the fromEmail and toEmail values in the Email Weekly Report node\n      to match your sender and recipient addresses.\n- [ ] Adjust the engagement weights in the WES Config node to reflect your\n      team's priorities. Default: likes 1, comments 3, shares 5, saves 4.\n\n### Required credentials\n- **LinkedIn OAuth2** (post fetch)\n- **X (Twitter) OAuth2** (tweet search)\n- **Instagram** (media fetch)\n- **OpenAI API** (strategy recommendations via GPT-4o)\n- **Google Sheets** (raw post logging)\n- **SMTP / email** (weekly report delivery)\n\n### Environment variables required\n`LINKEDIN_OWNER_URN` \u00b7 `TWITTER_SEARCH_QUERY` \u00b7 `ANALYTICS_SPREADSHEET_ID`\n\n### Notes\nWES (Weighted Engagement Score) is calculated as:\n`(likes \u00d7 1) + (comments \u00d7 3) + (shares \u00d7 5) + (saves \u00d7 4) / impressions \u00d7 100`\nPosts without impression data are excluded from the engagement rate calculation\nbut still counted in total post volume. Adjust weights in the WES Config node."
      },
      "typeVersion": 1
    }
  ],
  "active": false,
  "settings": {
    "binaryMode": "separate",
    "executionOrder": "v1"
  },
  "versionId": "bb8a05fb-fdcc-443e-a8af-88ee2aee37a5",
  "connections": {
    "Get media": {
      "main": [
        [
          {
            "node": "Merge All Streams",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "WES Config": {
      "main": [
        [
          {
            "node": "Get LinkedIn Posts",
            "type": "main",
            "index": 0
          },
          {
            "node": "Search Tweets",
            "type": "main",
            "index": 0
          },
          {
            "node": "Get media",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge LI & TW": {
      "main": [
        [
          {
            "node": "Merge All Streams",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Search Tweets": {
      "main": [
        [
          {
            "node": "Merge LI & TW",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Every Monday 9 AM": {
      "main": [
        [
          {
            "node": "WES Config",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Loop Over Batches": {
      "main": [
        [
          {
            "node": "Wait for All Branches",
            "type": "main",
            "index": 0
          }
        ],
        [
          {
            "node": "Log Raw Posts to Sheets",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Merge All Streams": {
      "main": [
        [
          {
            "node": "Normalize & Validate Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Get LinkedIn Posts": {
      "main": [
        [
          {
            "node": "Merge LI & TW",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "AI Strategy Insights": {
      "main": [
        [
          {
            "node": "Wait for All Branches",
            "type": "main",
            "index": 1
          }
        ]
      ]
    },
    "Capture Sheet Errors": {
      "main": [
        [
          {
            "node": "Loop Over Batches",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Wait for All Branches": {
      "main": [
        [
          {
            "node": "Combine Data & AI Output",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Log Raw Posts to Sheets": {
      "main": [
        [
          {
            "node": "Capture Sheet Errors",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Combine Data & AI Output": {
      "main": [
        [
          {
            "node": "Email Weekly Report",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Calculate Analytics (WES)": {
      "main": [
        [
          {
            "node": "AI Strategy Insights",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Normalize & Validate Posts": {
      "main": [
        [
          {
            "node": "Loop Over Batches",
            "type": "main",
            "index": 0
          },
          {
            "node": "Calculate Analytics (WES)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  }
}