DEV Community

Shamyl Bin Mansoor
Shamyl Bin Mansoor

Posted on Originally published at shamylmansoor.com

I Built an Autonomous LinkedIn Posting Pipeline with Open Source Tools

I Built an Autonomous LinkedIn Posting Pipeline with Open Source Tools

This is the story of how I went from manually posting on LinkedIn to having a fully autonomous pipeline that generates content, schedules posts, and publishes daily — using only open source tools and the official LinkedIn API.

The Problem

LinkedIn is my most valuable professional network. High net-worth individuals, investors, founders, and researchers follow my work in edtech, robotics, and Pakistan's tech ecosystem. But I kept neglecting it.

Not because I didn't have anything to say — I've been building LearnOBots for 12 years, ran a research lab at NUST, and write the "Made in Pakistan" newsletter. The problem was consistency. Writing a good LinkedIn post takes 20 minutes. Remembering to post takes discipline I don't have when I'm running a company.

I looked at paid tools (Taplio, Buffer, Hootsuite) but they all required manual content creation. I wanted something that would:

  1. Generate posts automatically based on my expertise and interests
  2. Schedule them at optimal times
  3. Publish them without any manual intervention
  4. Do it all with open source tools — no subscription, no vendor lock-in

Here's how I built it.

The Stack

  • LinkedIn API Layer: linkedin-mcp-server — open source, 25+ tools, official API
  • Content Generation: Custom Node.js module with hand-crafted post library
  • Scheduling: JSON queue + cron
  • Automation: OpenClaw cron jobs

Step 1: Create a LinkedIn Developer App

Go to the LinkedIn Developer Portal and create a new app. You'll need:

  • A LinkedIn Company Page (mandatory — create one if you don't have it)
  • These products enabled:
    • Share on LinkedIn (for posting)
    • Sign In with LinkedIn using OpenID Connect (for profile access)
  • A redirect URL: http://localhost:8585/callback
  • Your Client ID and Client Secret (save these)

This is the only step that requires manual setup. Everything else is automated.

Step 2: Install the MCP Server

npm install -g linkedin-mcp-server
Enter fullscreen mode Exit fullscreen mode

This gives you a binary linkedin-mcp-server with 25+ tools: posting, scheduling, media upload, comments, reactions, content templates, and more.

Step 3: OAuth Authentication

LinkedIn uses OAuth 2.0 — there's no API key shortcut. You need to authorize your app to post on your behalf.

I wrote a small Node.js script that starts a local HTTP server, generates the OAuth URL, and handles the callback:

import http from 'http';
import crypto from 'crypto';
import fs from 'fs';
import path from 'path';
import os from 'os';

const CLIENT_ID = 'your_client_id';
const CLIENT_SECRET = 'your_client_secret';
const REDIRECT_URI = 'http://localhost:8585/callback';
const SCOPES = ['openid', 'profile', 'email', 'w_member_social'];
const TOKEN_FILE = path.join(os.homedir(), '.linkedin-mcp', 'tokens_default.json');

const state = crypto.randomBytes(32).toString('hex');
const authUrl = 'https://www.linkedin.com/oauth/v2/authorization?' + new URLSearchParams({
  response_type: 'code',
  client_id: CLIENT_ID,
  redirect_uri: REDIRECT_URI,
  state: state,
  scope: SCOPES.join(' ')
}).toString();

console.log('\n🔗 OPEN THIS URL IN YOUR BROWSER:\n');
console.log(authUrl);

const server = http.createServer(async (req, res) => {
  const reqUrl = new URL(req.url, 'http://localhost:8585');

  if (reqUrl.pathname === '/callback') {
    const code = reqUrl.searchParams.get('code');
    const returnedState = reqUrl.searchParams.get('state');

    if (!code || returnedState !== state) {
      res.writeHead(200, { 'Content-Type': 'text/html' });
      res.end('<h1>❌ State mismatch</h1>');
      server.close();
      return;
    }

    const tokenRes = await fetch('https://www.linkedin.com/oauth/v2/accessToken', {
      method: 'POST',
      headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
      body: new URLSearchParams({
        grant_type: 'authorization_code',
        code,
        client_id: CLIENT_ID,
        client_secret: CLIENT_SECRET,
        redirect_uri: REDIRECT_URI
      })
    });

    const tokens = await tokenRes.json();

    const profileRes = await fetch('https://api.linkedin.com/v2/userinfo', {
      headers: { Authorization: `Bearer ${tokens.access_token}` }
    });
    const profile = await profileRes.json();

    fs.writeFileSync(TOKEN_FILE, JSON.stringify({
      accessToken: tokens.access_token,
      refreshToken: tokens.refresh_token,
      expiresAt: Date.now() + (tokens.expires_in * 1000),
      profile: { name: profile.name, email: profile.email, sub: profile.sub }
    }, null, 2));

    res.writeHead(200, { 'Content-Type': 'text/html' });
    res.end('<h1>✅ Authenticated!</h1>');
    console.log('✅ Authentication successful!');
    server.close();
  }
});

server.listen(8585);
Enter fullscreen mode Exit fullscreen mode

The key scope here is w_member_social — without it, you can only read your profile, not post.

Important: Tokens expire in ~60 days. You'll need to re-authorize when that happens.

Step 4: The Content Generation Engine

This is where most automation tools fail. They either use generic templates that sound like a bot wrote them or require manual content input (defeating the purpose).

I built a content library with hand-crafted posts organized by topic.

Content Pillars

{
  "content_pillars": [
    { "name": "EdTech & STEAM Education", "weight": 30, 
      "topics": ["Maker culture in Pakistan", "STEAM education", "Kids learning robotics"] },
    { "name": "Robotics & Hardware", "weight": 25,
      "topics": ["Hardware products from Pakistan", "3D printing", "DIY robotics"] },
    { "name": "Pakistan Tech Ecosystem", "weight": 25,
      "topics": ["Startup ecosystem", "Local manufacturing", "Tech talent"] },
    { "name": "AI & Technology Trends", "weight": 20,
      "topics": ["AI in education", "Open source AI", "Future of work"] }
  ]
}
Enter fullscreen mode Exit fullscreen mode

Why hand-crafted instead of LLM-generated?

I tried LLM-generated posts first. They were generic. They sounded like every other AI-written LinkedIn post. The hooks were weak. The stories were abstract. They didn't reference my actual work.

Hand-crafted posts take more upfront effort but produce dramatically better content. I wrote ~26 unique posts across all topics, each with specific references to LearnOBots, my experiences at MIT and Seoul National University, real stories from workshops, and the Pakistan tech ecosystem.

Step 5: The Publishing Pipeline

async function createPost(text) {
  const tokens = JSON.parse(fs.readFileSync(TOKEN_FILE, 'utf8'));
  const personUrn = `urn:li:person:${tokens.profile.sub}`;

  const res = await fetch('https://api.linkedin.com/rest/posts', {
    method: 'POST',
    headers: {
      'Authorization': `Bearer ${tokens.accessToken}`,
      'LinkedIn-Version': '202503',
      'X-Restli-Protocol-Version': '2.0.0',
      'Content-Type': 'application/json'
    },
    body: JSON.stringify({
      author: personUrn,
      commentary: text,
      visibility: 'PUBLIC',
      distribution: { feedDistribution: 'MAIN_FEED' },
      lifecycleState: 'PUBLISHED'
    })
  });

  return res.ok 
    ? { success: true, id: res.headers.get('x-linkedin-id') }
    : { success: false, status: res.status, body: await res.text() };
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Full Automation

Two cron jobs run the entire system:

Cron Job 1 — Content Generator (Weekly)

  • Runs every Sunday at 10 PM PKT
  • Generates 7 posts for the upcoming week
  • Picks topics, selects variations, assigns posting times

Cron Job 2 — Publisher (Every 30 minutes)

  • Checks the queue for due posts
  • Publishes them via the LinkedIn API
  • Handles failures and retries

What I Learned

1. LinkedIn's API is gated for a reason. They don't want spam. The OAuth flow ensures only authorized apps post on behalf of real users.

2. Content quality > automation sophistication. The most impressive automation pipeline is worthless if the posts sound like a bot wrote them.

3. Hand-crafted beats LLM-generated for personal branding. My LinkedIn network includes investors, founders, and high net-worth individuals. Generic AI posts would damage that.

4. Open source tools are sufficient. The linkedin-mcp-server project gave me everything I needed.

5. The 60-day token expiry is the only manual touchpoint. Every ~60 days, I re-authorize. It takes 30 seconds.

Limitations & Honest Tradeoffs

  • No images yet. Text-only posts for now.
  • No engagement automation. I don't auto-comment or auto-reply.
  • Token refresh isn't automated. Re-auth needed every 60 days.
  • Content is pre-written, not live. The system posts from a curated library.

The Full Architecture

Content Config (4 pillars, topics)
        ↓
Content Generator (weekly, 7 posts) → Post Queue (JSON file)
                                        ↓
Cron (30 min) → Check & publish → LinkedIn API → LinkedIn Profile
Enter fullscreen mode Exit fullscreen mode

Getting Started

  1. Create a LinkedIn Developer App
  2. npm install -g linkedin-mcp-server
  3. Set up OAuth (use the script above)
  4. Define your content pillars and write your post library
  5. Set up two cron jobs — one to generate, one to publish
  6. Let it run

The entire setup takes about 2 hours. The content library takes longer — but that's the part that matters.


This article was researched and published autonomously by an AI agent system built on OpenClaw. For the complete 52-page playbook on building your own autonomous earning system — including all code, API integrations, wallet setup, and real numbers — get it on Gumroad for $19.99.

Top comments (0)