Scenario
Your Company Page publishes 10–20 posts per month, but nobody analyzes which topics and formats actually work. This job pulls recent company posts along with their engagement metrics (likes, comments, shares), ranks them by total engagement, identifies thematic patterns across top performers, then asks Mave to explain what the winners have in common and generate 10 new content concepts that follow those patterns. You get a data-backed content calendar instead of brainstorming in a vacuum.Architecture
Code
import os, requests, time
LI = os.environ["LINKEDIN_ACCESS_TOKEN"]
MV = os.environ["MAVERA_API_KEY"]
LI_BASE = "https://api.linkedin.com/rest"
MV_BASE = "https://app.mavera.io/api/v1"
LI_H = {"Authorization": f"Bearer {LI}", "LinkedIn-Version": "202401", "X-Restli-Protocol-Version": "2.0.0"}
MV_H = {"Authorization": f"Bearer {MV}", "Content-Type": "application/json"}
ORG_URN = "urn:li:organization:12345678"
# 1. Pull recent company posts
r = requests.get(f"{LI_BASE}/posts",
headers=LI_H,
params={
"q": "author",
"author": ORG_URN,
"count": 50,
"sortBy": "LAST_MODIFIED",
})
if r.status_code == 429:
time.sleep(int(r.headers.get("Retry-After", 60)))
r = requests.get(f"{LI_BASE}/posts", headers=LI_H,
params={"q": "author", "author": ORG_URN, "count": 50, "sortBy": "LAST_MODIFIED"})
r.raise_for_status()
posts = r.json().get("elements", [])
# 2. Fetch engagement for each post
enriched = []
for post in posts:
post_urn = post.get("id", "")
commentary = post.get("commentary", "")
if not commentary:
continue
sr = requests.get(f"{LI_BASE}/socialActions/{post_urn}",
headers=LI_H)
if sr.status_code == 429:
time.sleep(int(sr.headers.get("Retry-After", 30)))
sr = requests.get(f"{LI_BASE}/socialActions/{post_urn}", headers=LI_H)
if sr.ok:
actions = sr.json()
likes = actions.get("likesSummary", {}).get("totalLikes", 0)
comments = actions.get("commentsSummary", {}).get("totalFirstLevelComments", 0)
shares = actions.get("sharesSummary", {}).get("totalShares", 0)
else:
likes = comments = shares = 0
total_engagement = likes + comments * 3 + shares * 5
enriched.append({
"urn": post_urn,
"commentary": commentary[:500],
"content_type": post.get("content", {}).get("contentType", "NONE"),
"likes": likes,
"comments": comments,
"shares": shares,
"engagement_score": total_engagement,
})
time.sleep(0.4)
# 3. Rank and take top performers
ranked = sorted(enriched, key=lambda x: -x["engagement_score"])
top = ranked[:10]
bottom = ranked[-5:] if len(ranked) >= 15 else []
# 4. Build analysis prompt
top_block = "\n\n---\n\n".join(
f"POST {i+1} (engagement: {p['engagement_score']} | {p['likes']} likes, {p['comments']} comments, {p['shares']} shares | {p['content_type']}):\n{p['commentary']}"
for i, p in enumerate(top)
)
bottom_block = "\n\n---\n\n".join(
f"LOW {i+1} (engagement: {p['engagement_score']}):\n{p['commentary']}"
for i, p in enumerate(bottom)
) if bottom else "No low-performing posts to compare."
# 5. Ask Mave for theme analysis + content generation
analysis = requests.post(f"{MV_BASE}/mave/chat", headers=MV_H, json={
"message": f"""Analyze these LinkedIn Company Page posts. I'm giving you the top 10 performers and bottom 5.
TOP PERFORMERS:
{top_block}
LOW PERFORMERS:
{bottom_block}
Produce:
1. THEME ANALYSIS: What do the top posts have in common? (format, length, hooks, tone, topics)
2. ANTI-PATTERNS: What do underperformers share?
3. CONTENT FORMULA: A repeatable structure based on the winners
4. NEW CONCEPTS: Generate 10 new LinkedIn post concepts that follow the winning patterns. For each: topic, hook (first line), format (text-only/carousel/video/poll), and why it should work based on the data.
5. POSTING CADENCE: Recommended frequency and timing based on content type distribution."""
}).json()
print("=" * 60)
print("LINKEDIN CONTENT STRATEGY — DATA-DRIVEN")
print("=" * 60)
print(f"Analyzed {len(enriched)} posts | Top engagement: {top[0]['engagement_score']}")
print(f"Avg engagement (top 10): {sum(p['engagement_score'] for p in top) / len(top):.0f}")
print(f"Avg engagement (all): {sum(p['engagement_score'] for p in enriched) / len(enriched):.0f}")
print()
print(analysis.get("content", "")[:2000])
const LI = process.env.LINKEDIN_ACCESS_TOKEN;
const MV = process.env.MAVERA_API_KEY;
const LI_BASE = "https://api.linkedin.com/rest";
const MV_BASE = "https://app.mavera.io/api/v1";
const LI_H = { Authorization: `Bearer ${LI}`, "LinkedIn-Version": "202401", "X-Restli-Protocol-Version": "2.0.0" };
const MV_H = { Authorization: `Bearer ${MV}`, "Content-Type": "application/json" };
const ORG_URN = "urn:li:organization:12345678";
// 1. Pull recent company posts
let res = await fetch(
`${LI_BASE}/posts?q=author&author=${encodeURIComponent(ORG_URN)}&count=50&sortBy=LAST_MODIFIED`,
{ headers: LI_H }
);
if (res.status === 429) {
await new Promise(r => setTimeout(r, parseInt(res.headers.get("Retry-After") || "60", 10) * 1000));
res = await fetch(
`${LI_BASE}/posts?q=author&author=${encodeURIComponent(ORG_URN)}&count=50&sortBy=LAST_MODIFIED`,
{ headers: LI_H }
);
}
if (!res.ok) throw new Error(`LinkedIn ${res.status}: ${await res.text()}`);
const posts = (await res.json()).elements || [];
// 2. Fetch engagement per post
const enriched = [];
for (const post of posts) {
const commentary = post.commentary || "";
if (!commentary) continue;
const sr = await fetch(`${LI_BASE}/socialActions/${post.id}`, { headers: LI_H });
let likes = 0, comments = 0, shares = 0;
if (sr.ok) {
const actions = await sr.json();
likes = actions.likesSummary?.totalLikes || 0;
comments = actions.commentsSummary?.totalFirstLevelComments || 0;
shares = actions.sharesSummary?.totalShares || 0;
}
enriched.push({
urn: post.id, commentary: commentary.slice(0, 500),
content_type: post.content?.contentType || "NONE",
likes, comments, shares,
engagement_score: likes + comments * 3 + shares * 5,
});
await new Promise(r => setTimeout(r, 400));
}
// 3. Rank
const ranked = enriched.sort((a, b) => b.engagement_score - a.engagement_score);
const top = ranked.slice(0, 10);
const bottom = ranked.length >= 15 ? ranked.slice(-5) : [];
// 4. Build prompt
const topBlock = top.map((p, i) =>
`POST ${i + 1} (engagement: ${p.engagement_score} | ${p.likes} likes, ${p.comments} comments, ${p.shares} shares | ${p.content_type}):\n${p.commentary}`
).join("\n\n---\n\n");
const bottomBlock = bottom.length
? bottom.map((p, i) => `LOW ${i + 1} (engagement: ${p.engagement_score}):\n${p.commentary}`).join("\n\n---\n\n")
: "No low-performing posts to compare.";
// 5. Mave analysis
const analysis = await fetch(`${MV_BASE}/mave/chat`, {
method: "POST", headers: MV_H,
body: JSON.stringify({
message: `Analyze these LinkedIn Company Page posts. Top 10 and bottom 5.\n\nTOP PERFORMERS:\n${topBlock}\n\nLOW PERFORMERS:\n${bottomBlock}\n\nProduce:\n1. THEME ANALYSIS: What do top posts have in common?\n2. ANTI-PATTERNS: What do underperformers share?\n3. CONTENT FORMULA: Repeatable structure from winners\n4. NEW CONCEPTS: 10 new post concepts following winning patterns (topic, hook, format, rationale)\n5. POSTING CADENCE: Recommended frequency and timing`,
}),
}).then(r => r.json());
console.log("=".repeat(60));
console.log("LINKEDIN CONTENT STRATEGY — DATA-DRIVEN");
console.log("=".repeat(60));
const avgTop = top.reduce((s, p) => s + p.engagement_score, 0) / (top.length || 1);
const avgAll = enriched.reduce((s, p) => s + p.engagement_score, 0) / (enriched.length || 1);
console.log(`Analyzed ${enriched.length} posts | Top: ${top[0]?.engagement_score} | Avg top 10: ${avgTop.toFixed(0)} | Avg all: ${avgAll.toFixed(0)}`);
console.log();
console.log((analysis.content || "").slice(0, 2000));
Example Output
============================================================
LINKEDIN CONTENT STRATEGY — DATA-DRIVEN
============================================================
Analyzed 42 posts | Top engagement: 847 | Avg top 10: 423 | Avg all: 112
## 1. Theme Analysis
Top performers share three patterns:
- **Personal hooks**: Posts opening with "I" or a story outperform by 4x
- **Data specificity**: "60% faster" beats "significantly faster" every time
- **Carousel format**: 7 of 10 top posts use document/carousel attachments
- **Length**: Winners average 180–250 words (not short, not long)
## 2. Anti-Patterns
Low performers: product announcements with no context, generic industry
commentary, posts that read like press releases.
## 3. Content Formula
Hook (personal/provocative) → Context (1-2 sentences) → Insight with data →
Takeaway → CTA question
## 4. New Concepts
1. "We analyzed 500 customer calls. Here's what nobody talks about."
Format: Carousel | Hook: contrarian insight | Rationale: data + surprise
2. "The metric we stopped tracking (and what we watch instead)"
Format: Text-only | Hook: anti-conventional | Rationale: personal + specific
3. "Our biggest marketing mistake this quarter — a thread"
Format: Text | Hook: vulnerability | Rationale: authenticity pattern
...
## 5. Cadence
3–4 posts/week. Carousels Tue/Thu. Text-only Mon. Polls sparingly (1/month).
Error Handling
Community Management API required
Community Management API required
The
GET /posts and GET /socialActions endpoints require the Community Management API product. Without approval, calls return 403.Post URN format varies
Post URN format varies
Post URNs can be
urn:li:share:{id} or urn:li:ugcPost:{id} depending on creation method. The socialActions endpoint accepts both formats.Engagement weighting
Engagement weighting
The code weights comments at 3x and shares at 5x vs. likes at 1x. Adjust these multipliers based on your funnel — if shares drive traffic, weight higher.
Rate limit on socialActions
Rate limit on socialActions
Fetching engagement per post can burn rate limits fast on 50+ posts. The 400ms delay prevents throttling. For larger volumes, cache engagement data daily.