{
  "name": "Louis LinkedIn Automation OS",
  "nodes": [
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 8 * * 1,3,5"
            }
          ]
        }
      },
      "id": "schedule-trigger",
      "name": "\ud83d\udcc5 Schedule Trigger (Mon/Wed/Fri 8am)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        240,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const skipToday = Math.random() < 0.1;\nif (skipToday) { console.log('Randomly skipping today'); return []; }\nreturn [{ json: { skip: false, timestamp: new Date().toISOString() } }];"
      },
      "id": "randomize-timing",
      "name": "\ud83c\udfb2 Randomize Timing (Anti-Detection)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const { execSync } = require('child_process');\nconst output = execSync('node -r dotenv/config content-generator.js generate-scheduled', { cwd: 'C:/Users/FX/Downloads/LINKEDIN', encoding: 'utf8' });\nreturn [{ json: { stdout: output } }];"
      },
      "id": "generate-content",
      "name": "\ud83e\udd16 Generate Post (Claude API)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        680,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const output = $input.first().json.stdout;\nconst lines = output.trim().split('\\n');\nconst jsonLine = lines.find(line => line.trim().startsWith('{'));\nif (!jsonLine) throw new Error('No JSON found in output: ' + output);\nconst parsed = JSON.parse(jsonLine);\nif (!parsed.content || parsed.content.length < 50) throw new Error('Content too short or missing');\nconsole.log('Post type:', parsed.type, '| Topic:', parsed.topic);\nreturn [{ json: parsed }];"
      },
      "id": "parse-content",
      "name": "Parse Generated Content",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        900,
        300
      ]
    },
    {
      "parameters": {
        "operation": "append",
        "documentId": {
          "__rl": true,
          "value": "1c9Txq2uD9JEY_SUdQ7NN7FlwG_ZQf_wtJ3xhfaBTCxg",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Posts Queue",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "value": {
            "content": "={{ $json.content }}",
            "topic": "={{ $json.topic }}",
            "type": "={{ $json.type }}",
            "status": "pending_review",
            "generated_at": "={{ $json.generatedAt }}"
          }
        }
      },
      "id": "save-for-review",
      "name": "\ud83d\udcbe Save for Review",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1120,
        300
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "*/30 * * * *"
            }
          ]
        }
      },
      "id": "check-approved-schedule",
      "name": "\u23f0 Check for Approved Posts (Every 30 min)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        240,
        600
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "1c9Txq2uD9JEY_SUdQ7NN7FlwG_ZQf_wtJ3xhfaBTCxg",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Posts Queue",
          "mode": "name"
        },
        "options": {}
      },
      "id": "read-posts-queue",
      "name": "\ud83d\udccb Read Posts Queue",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        460,
        600
      ]
    },
    {
      "parameters": {
        "conditions": {
          "options": {
            "caseSensitive": false
          },
          "conditions": [
            {
              "leftValue": "={{ $json.status }}",
              "rightValue": "approved",
              "operator": {
                "type": "string",
                "operation": "equals"
              }
            }
          ]
        }
      },
      "id": "filter-approved",
      "name": "\u2705 Only Approved Posts",
      "type": "n8n-nodes-base.filter",
      "typeVersion": 2,
      "position": [
        680,
        600
      ]
    },
    {
      "parameters": {
        "batchSize": 1,
        "options": {}
      },
      "id": "process-one-by-one",
      "name": "\ud83d\udd00 Process One by One",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        900,
        600
      ]
    },
    {
      "parameters": {
        "jsCode": "const { spawn, execSync } = require('child_process');\nconst fs = require('fs');\n\n// Load .env (process.env not available in n8n sandbox)\nconst dotenv = {};\ntry {\n  const envContent = fs.readFileSync('C:/Users/FX/Downloads/LINKEDIN/.env', 'utf8');\n  envContent.split('\\n').forEach(line => {\n    const match = line.match(/^([A-Z_0-9]+)=(.+)$/);\n    if (match) dotenv[match[1]] = match[2].trim();\n  });\n} catch(e) {}\n\nconst row = $input.first().json;\nconst content = row.content;\nconst topic = row.topic || 'business automation';\nif (!content) throw new Error('No content in approved row');\n\n// Write post content\nfs.writeFileSync('C:/Users/FX/Downloads/LINKEDIN/playwright/pending-post.txt', content, 'utf8');\nfs.writeFileSync('C:/Users/FX/Downloads/LINKEDIN/playwright/post-result.json', JSON.stringify({ status: 'running', startedAt: new Date().toISOString() }), 'utf8');\n\n// Use pre-generated image from Telegram preview \u2014 skip regenerating\nconst imgPath = 'C:/Users/FX/Downloads/LINKEDIN/playwright/pending-image.png';\nif (fs.existsSync(imgPath)) {\n  console.log('Using pre-generated Nano Banana 2 image from Telegram preview');\n} else {\n  try {\n    const safeT = topic.replace(/[\"\\n]/g, ' ');\n    const safeC = content.substring(0, 200).replace(/[\"\\n]/g, ' ');\n    execSync('node image-generator.js \"' + safeT + '\" \"' + safeC + '\"', {\n      cwd: 'C:/Users/FX/Downloads/LINKEDIN/playwright',\n      timeout: 30000\n    });\n    console.log('Image generated successfully');\n  } catch (imgErr) {\n    console.log('Image generation failed (will post text only):', imgErr.message);\n  }\n}\n\n// Spawn playwright to post\nconst child = spawn('node', ['-r', 'dotenv/config', 'linkedin-poster.js', 'post'], {\n  cwd: 'C:/Users/FX/Downloads/LINKEDIN/playwright',\n  detached: true,\n  stdio: 'ignore',\n  windowsHide: true\n});\nchild.unref();\n\nreturn [{ json: { ...row, status: 'posting', postedAt: new Date().toISOString() } }];"
      },
      "id": "publish-approved",
      "name": "\ud83d\ude80 Publish Approved Post",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1120,
        600
      ]
    },
    {
      "parameters": {
        "operation": "update",
        "documentId": {
          "__rl": true,
          "value": "1c9Txq2uD9JEY_SUdQ7NN7FlwG_ZQf_wtJ3xhfaBTCxg",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Posts Queue",
          "mode": "name"
        },
        "columns": {
          "mappingMode": "defineBelow",
          "matchingColumns": [
            "generated_at"
          ],
          "value": {
            "generated_at": "={{ $json.generated_at }}",
            "status": "posting"
          }
        },
        "options": {}
      },
      "id": "mark-as-posting",
      "name": "\ud83d\udcdd Mark as Posting",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        1340,
        600
      ]
    },
    {
      "parameters": {
        "httpMethod": "POST",
        "path": "linkedin-post-webhook",
        "options": {}
      },
      "id": "manual-webhook",
      "name": "\ud83d\udd17 Manual Post Webhook",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [
        240,
        900
      ]
    },
    {
      "parameters": {
        "jsCode": "const { execSync } = require('child_process');\nconst topic = $input.first().json.body.topic;\nconst type = $input.first().json.body.type || 'viral';\nconst cmd = 'node -r dotenv/config content-generator.js generate-custom \"' + topic + '\" ' + type;\nconst output = execSync(cmd, { cwd: 'C:/Users/FX/Downloads/LINKEDIN', encoding: 'utf8' });\nreturn [{ json: { stdout: output } }];"
      },
      "id": "generate-custom-post",
      "name": "Generate Custom Post",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        900
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "cronExpression",
              "expression": "0 10 * * 2,4"
            }
          ]
        }
      },
      "id": "connection-schedule",
      "name": "\ud83d\udcc5 Connection Schedule (Tue/Thu 10am)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        240,
        1200
      ]
    },
    {
      "parameters": {
        "jsCode": "const dailyLimit = Math.floor(Math.random() * 6) + 10;\nreturn [{ json: { dailyLimit, date: new Date().toISOString() } }];"
      },
      "id": "set-daily-limit",
      "name": "Set Daily Connection Limit",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        1200
      ]
    },
    {
      "parameters": {
        "operation": "read",
        "documentId": {
          "__rl": true,
          "value": "1c9Txq2uD9JEY_SUdQ7NN7FlwG_ZQf_wtJ3xhfaBTCxg",
          "mode": "id"
        },
        "sheetName": {
          "__rl": true,
          "value": "Target Profiles",
          "mode": "name"
        },
        "options": {}
      },
      "id": "read-target-profiles",
      "name": "\ud83d\udccb Read Target Profiles",
      "type": "n8n-nodes-base.googleSheets",
      "typeVersion": 4,
      "position": [
        680,
        1200
      ]
    },
    {
      "parameters": {
        "jsCode": "const { spawn } = require('child_process');\nconst fs = require('fs');\nconst item = $input.first().json;\n\n// Write FULL content to file \u2014 telegram-sender.js will read this\ntry {\n  fs.writeFileSync(\n    'C:/Users/FX/Downloads/LINKEDIN/playwright/pending-preview.json',\n    JSON.stringify({\n      topic: item.topic || '',\n      content: item.content || '',\n      imagePrompt: (() => { let p = item.imagePrompt || ''; if (!p) { try { p = $('Parse Generated Content').first().json.imagePrompt || ''; } catch(e) {} } return p; })(),\n      generatedAt: item.generatedAt || new Date().toISOString()\n    }),\n    'utf8'\n  );\n  console.log('\ud83d\udcc4 Wrote pending-preview.json (' + (item.content || '').length + ' chars)');\n} catch(e) {\n  console.log('Could not write pending-preview.json:', e.message);\n}\n\n// Spawn as DETACHED background process \u2014 image generation takes 30-50s\n// Using execSync would block n8n runner until timeout kills it\nconst child = spawn('node', ['telegram-sender.js'], {\n  cwd: 'C:/Users/FX/Downloads/LINKEDIN/playwright',\n  detached: true,\n  stdio: 'ignore',\n  windowsHide: true\n});\nchild.unref();\n\nconsole.log('\ud83d\udcf1 Telegram sender launched in background for:', item.topic);\nreturn [{ json: item }];"
      },
      "id": "send-telegram-preview",
      "name": "\ud83d\udcf1 Send Telegram Preview",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1340,
        300
      ]
    },
    {
      "parameters": {
        "jsCode": "const fs = require('fs');\nconst queueFile = 'C:/Users/FX/Downloads/LINKEDIN/playwright/telegram-queue.json';\n\nlet queue = [];\ntry { queue = JSON.parse(fs.readFileSync(queueFile, 'utf8')); } catch(e) {}\n\nconst rows = $input.all();\nif (!queue.length) return rows;\n\nconst updatedRows = rows.map(row => {\n  const match = queue.find(q => q.postId === row.json.generated_at);\n  if (match) {\n    const newStatus = match.action === 'approve' ? 'approved' : 'skipped';\n    console.log('\ud83d\udd04 Telegram action:', match.action, '\u2192', row.json.topic);\n    return { json: { ...row.json, status: newStatus } };\n  }\n  return row;\n});\n\nconst processedIds = rows.map(r => r.json.generated_at).filter(Boolean);\nconst remaining = queue.filter(q => !processedIds.includes(q.postId));\nfs.writeFileSync(queueFile, JSON.stringify(remaining, null, 2), 'utf8');\n\nreturn updatedRows;"
      },
      "id": "merge-telegram-approvals",
      "name": "\ud83d\udd04 Merge Telegram Approvals",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        570,
        600
      ]
    },
    {
      "parameters": {
        "rule": {
          "interval": [
            {
              "field": "seconds",
              "secondsInterval": 120
            }
          ]
        }
      },
      "id": "telegram-poller",
      "name": "\u23f1\ufe0f Telegram Poller (every 2 min)",
      "type": "n8n-nodes-base.scheduleTrigger",
      "typeVersion": 1,
      "position": [
        240,
        1500
      ]
    },
    {
      "parameters": {
        "jsCode": "const https = require('https');\nconst fs = require('fs');\n\nconst dotenv = {};\ntry {\n  const envContent = fs.readFileSync('C:/Users/FX/Downloads/LINKEDIN/.env', 'utf8');\n  envContent.split('\\n').forEach(line => {\n    const match = line.match(/^([A-Z_0-9]+)=(.+)$/);\n    if (match) dotenv[match[1]] = match[2].trim();\n  });\n} catch(e) {}\n\nconst token = dotenv.TELEGRAM_BOT_TOKEN;\nif (!token) { console.log('No TELEGRAM_BOT_TOKEN'); return [{ json: { processed: 0 } }]; }\n\nconst offsetFile = 'C:/Users/FX/Downloads/LINKEDIN/playwright/telegram-offset.json';\nconst queueFile = 'C:/Users/FX/Downloads/LINKEDIN/playwright/telegram-queue.json';\n\nlet offset = 0;\ntry { offset = JSON.parse(fs.readFileSync(offsetFile, 'utf8')).offset || 0; } catch(e) {}\n\nconst updates = await new Promise((resolve) => {\n  const req = https.request({\n    hostname: 'api.telegram.org',\n    path: '/bot' + token + '/getUpdates?offset=' + offset + '&timeout=1&allowed_updates=%5B%22callback_query%22%5D',\n    method: 'GET'\n  }, (res) => {\n    let data = '';\n    res.on('data', chunk => data += chunk);\n    res.on('end', () => { try { resolve(JSON.parse(data)); } catch(e) { resolve({ ok: false, result: [] }); } });\n  });\n  req.on('error', () => resolve({ ok: false, result: [] }));\n  req.end();\n});\n\nif (!updates.ok || !updates.result || !updates.result.length) {\n  return [{ json: { processed: 0 } }];\n}\n\nlet queue = [];\ntry { queue = JSON.parse(fs.readFileSync(queueFile, 'utf8')); } catch(e) {}\n\nlet processed = 0;\nfor (const update of updates.result) {\n  const cb = update.callback_query;\n  if (!cb) { offset = update.update_id + 1; continue; }\n\n  const parts = (cb.data || '').split('|');\n  const action = parts[0];\n  const postId = parts.slice(1).join('|');\n\n  if (action && postId) {\n    queue.push({ action, postId, processedAt: new Date().toISOString() });\n    processed++;\n    console.log('\ud83d\udcec Telegram action:', action, 'for post:', postId.substring(0, 20));\n  }\n\n  const answerText = action === 'approve' ? 'Posting now! Ready in ~7 min...' : 'Skipped!';\n  const answerBody = JSON.stringify({ callback_query_id: cb.id, text: answerText });\n  await new Promise((resolve) => {\n    const req = https.request({\n      hostname: 'api.telegram.org',\n      path: '/bot' + token + '/answerCallbackQuery',\n      method: 'POST',\n      headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(answerBody) }\n    }, () => resolve());\n    req.on('error', () => resolve());\n    req.write(answerBody);\n    req.end();\n  });\n\n  offset = update.update_id + 1;\n}\n\nfs.writeFileSync(queueFile, JSON.stringify(queue, null, 2), 'utf8');\nfs.writeFileSync(offsetFile, JSON.stringify({ offset }), 'utf8');\n\nconsole.log('\ud83d\udcec Processed ' + processed + ' updates, queue: ' + queue.length);\nreturn [{ json: { processed, queueSize: queue.length } }];"
      },
      "id": "process-telegram-updates",
      "name": "\ud83d\udcec Process Telegram Updates",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        460,
        1500
      ]
    },
    {
      "parameters": {
        "batchSize": 1,
        "options": {}
      },
      "id": "split-profiles",
      "name": "Split Profiles One by One",
      "type": "n8n-nodes-base.splitInBatches",
      "typeVersion": 3,
      "position": [
        900,
        1200
      ]
    },
    {
      "parameters": {
        "amount": 60,
        "unit": "minutes"
      },
      "id": "delay-between-connections",
      "name": "\u23f1 Human Delay (60 min)",
      "type": "n8n-nodes-base.wait",
      "typeVersion": 1,
      "position": [
        1120,
        1200
      ]
    },
    {
      "parameters": {
        "jsCode": "const { execSync } = require('child_process');\nconst item = $input.first().json;\nconst targetInfo = item.name + ', ' + item.title + ' at ' + item.company;\nconst connType = item.connection_type || 'founder';\nconst cmd = 'node -r dotenv/config content-generator.js generate-connection \"' + targetInfo + '\" ' + connType;\nconst output = execSync(cmd, { cwd: 'C:/Users/FX/Downloads/LINKEDIN', encoding: 'utf8' });\nconst parsed = JSON.parse(output);\nreturn [{ json: { ...item, message: parsed.message } }];"
      },
      "id": "generate-connection-msg",
      "name": "Generate Connection Message",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1340,
        1200
      ]
    },
    {
      "parameters": {
        "jsCode": "const { execSync } = require('child_process');\nconst item = $input.first().json;\nconst cmd = 'node -r dotenv/config linkedin-poster.js connect \"' + item.profile_url + '\" \"' + (item.message || '') + '\"';\nconst output = execSync(cmd, { cwd: 'C:/Users/FX/Downloads/LINKEDIN/playwright', encoding: 'utf8' });\nreturn [{ json: { stdout: output } }];"
      },
      "id": "send-connection",
      "name": "Send Connection Request",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [
        1560,
        1200
      ]
    }
  ],
  "connections": {
    "\ud83d\udcc5 Schedule Trigger (Mon/Wed/Fri 8am)": {
      "main": [
        [
          {
            "node": "\ud83c\udfb2 Randomize Timing (Anti-Detection)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83c\udfb2 Randomize Timing (Anti-Detection)": {
      "main": [
        [
          {
            "node": "\ud83e\udd16 Generate Post (Claude API)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83e\udd16 Generate Post (Claude API)": {
      "main": [
        [
          {
            "node": "Parse Generated Content",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Parse Generated Content": {
      "main": [
        [
          {
            "node": "\ud83d\udcbe Save for Review",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcbe Save for Review": {
      "main": [
        [
          {
            "node": "\ud83d\udcf1 Send Telegram Preview",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u23f0 Check for Approved Posts (Every 30 min)": {
      "main": [
        [
          {
            "node": "\ud83d\udccb Read Posts Queue",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udccb Read Posts Queue": {
      "main": [
        [
          {
            "node": "\ud83d\udd04 Merge Telegram Approvals",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd04 Merge Telegram Approvals": {
      "main": [
        [
          {
            "node": "\u2705 Only Approved Posts",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u23f1\ufe0f Telegram Poller (every 2 min)": {
      "main": [
        [
          {
            "node": "\ud83d\udcec Process Telegram Updates",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u2705 Only Approved Posts": {
      "main": [
        [
          {
            "node": "\ud83d\udd00 Process One by One",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd00 Process One by One": {
      "main": [
        [],
        [
          {
            "node": "\ud83d\ude80 Publish Approved Post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\ude80 Publish Approved Post": {
      "main": [
        [
          {
            "node": "\ud83d\udcdd Mark as Posting",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udd17 Manual Post Webhook": {
      "main": [
        [
          {
            "node": "Generate Custom Post",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udcc5 Connection Schedule (Tue/Thu 10am)": {
      "main": [
        [
          {
            "node": "Set Daily Connection Limit",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Set Daily Connection Limit": {
      "main": [
        [
          {
            "node": "\ud83d\udccb Read Target Profiles",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\ud83d\udccb Read Target Profiles": {
      "main": [
        [
          {
            "node": "Split Profiles One by One",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Split Profiles One by One": {
      "main": [
        [
          {
            "node": "\u23f1 Human Delay (60 min)",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "\u23f1 Human Delay (60 min)": {
      "main": [
        [
          {
            "node": "Generate Connection Message",
            "type": "main",
            "index": 0
          }
        ]
      ]
    },
    "Generate Connection Message": {
      "main": [
        [
          {
            "node": "Send Connection Request",
            "type": "main",
            "index": 0
          }
        ]
      ]
    }
  },
  "settings": {
    "executionOrder": "v1",
    "saveManualExecutions": true,
    "callerPolicy": "workflowsFromSameOwner"
  },
  "tags": []
}