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:
- Generate posts automatically based on my expertise and interests
- Schedule them at optimal times
- Publish them without any manual intervention
- 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
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);
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"] }
]
}
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() };
}
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
Getting Started
- Create a LinkedIn Developer App
npm install -g linkedin-mcp-server- Set up OAuth (use the script above)
- Define your content pillars and write your post library
- Set up two cron jobs — one to generate, one to publish
- 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)