Skip to content

Multi-Agent Pipeline β€” LinkedIn Article Publisher

Publishing consistent, high-quality content on LinkedIn takes time. The process is always the same: research the topic, brainstorm angles, write a draft, edit it, and publish. Every step is something an AI agent can do β€” but no single agent prompt does all of it well.

This article walks through building a multi-agent pipeline where specialised agents handle each step, passing output to the next agent in the chain β€” just like a real editorial team.


Topic Input
β”‚
β–Ό
[Research Agent] ──── searches news, blogs, YouTube ──────► Research Report
β”‚
β–Ό
[Brainstorm Agent] ── generates angles + outlines ─────────► Best Angle + Outline
β”‚
β–Ό
[Writer Agent] ──── writes full article ───────────────────► Draft Article
β”‚
β–Ό
[Scorer Agent] ──── rates clarity, engagement, value ──────► Score + Feedback
β”‚
β”œβ”€β”€ Score β‰₯ 8? ──► [Publisher Agent] ──► LinkedIn Post Draft
β”‚
└── Score < 8? ──► [Rewrite Agent] ──────► back to Scorer

LangGraph handles the conditional loop (rewrite if score is low) cleanly:

from typing import TypedDict, Annotated
import operator
from langgraph.graph import StateGraph, END
from langchain_openai import ChatOpenAI
from langchain_core.prompts import ChatPromptTemplate
from langchain_core.output_parsers import StrOutputParser
llm = ChatOpenAI(model="gpt-4o")
# --- State ---
class ArticleState(TypedDict):
topic: str
research: str
outline: str
draft: str
score: float
feedback: str
rewrites: int
final: str
# --- Agents as functions ---
def research_agent(state: ArticleState) -> ArticleState:
prompt = ChatPromptTemplate.from_template("""
Research the topic: {topic}
Find:
- 3-5 recent news items or developments (last 6 months)
- Key statistics or data points
- Differing perspectives or debates
- Practical implications for software developers
Format as a structured research report.
""")
chain = prompt | llm | StrOutputParser()
research = chain.invoke({"topic": state["topic"]})
return {"research": research}
def brainstorm_agent(state: ArticleState) -> ArticleState:
prompt = ChatPromptTemplate.from_template("""
Based on this research about "{topic}":
{research}
Generate the best LinkedIn article angle. Consider:
- What would resonate most with senior software developers?
- What's the surprising or contrarian take?
- What practical value can you provide?
Provide:
1. A compelling headline
2. The core thesis in one sentence
3. A 5-point outline
""")
chain = prompt | llm | StrOutputParser()
outline = chain.invoke({"topic": state["topic"], "research": state["research"]})
return {"outline": outline}
def writer_agent(state: ArticleState) -> ArticleState:
prompt = ChatPromptTemplate.from_template("""
Write a LinkedIn article based on this outline:
{outline}
Using this research as your source of truth:
{research}
Requirements:
- 600-800 words
- Start with a hook that creates curiosity or tension
- Write for senior software developers and architects
- Include 1-2 concrete examples or mini case studies
- End with a clear call-to-action or question to encourage comments
- 3-5 relevant hashtags at the end
- Conversational but professional tone
""")
chain = prompt | llm | StrOutputParser()
draft = chain.invoke({
"outline": state["outline"],
"research": state["research"]
})
return {"draft": draft}
def scorer_agent(state: ArticleState) -> ArticleState:
prompt = ChatPromptTemplate.from_template("""
Score this LinkedIn article:
---
{draft}
---
Rate each dimension from 1-10:
- CLARITY: Is it easy to understand? Clear structure?
- ENGAGEMENT: Would people stop scrolling? Is the hook compelling?
- VALUE: Does it teach something useful to a developer?
- LINKEDIN_FIT: Right length, tone, and format for LinkedIn?
Respond in exactly this format:
CLARITY: [score]
ENGAGEMENT: [score]
VALUE: [score]
LINKEDIN_FIT: [score]
OVERALL: [average]
FEEDBACK: [2-3 specific improvement suggestions if overall < 8, else "APPROVED"]
""")
chain = prompt | llm | StrOutputParser()
result = chain.invoke({"draft": state["draft"]})
# Parse score
import re
overall_match = re.search(r'OVERALL:\s*(\d+(?:\.\d+)?)', result)
score = float(overall_match.group(1)) if overall_match else 5.0
feedback_match = re.search(r'FEEDBACK:\s*(.+?)$', result, re.DOTALL)
feedback = feedback_match.group(1).strip() if feedback_match else result
return {"score": score, "feedback": feedback}
def rewrite_agent(state: ArticleState) -> ArticleState:
prompt = ChatPromptTemplate.from_template("""
Rewrite this LinkedIn article based on the feedback provided.
Original article:
{draft}
Feedback to address:
{feedback}
Keep what's working. Fix what the feedback identifies.
Maintain the same topic, length, and audience.
""")
chain = prompt | llm | StrOutputParser()
new_draft = chain.invoke({
"draft": state["draft"],
"feedback": state["feedback"]
})
return {"draft": new_draft, "rewrites": state["rewrites"] + 1}
def publisher_agent(state: ArticleState) -> ArticleState:
prompt = ChatPromptTemplate.from_template("""
Format this article for LinkedIn publishing:
{draft}
Output:
1. The article text exactly as written (ready to paste into LinkedIn)
2. A suggested best time to post (based on LinkedIn engagement research)
3. 3 suggested people or topics to tag for reach
4. First comment to post immediately after publishing (to boost early engagement)
""")
chain = prompt | llm | StrOutputParser()
final = chain.invoke({"draft": state["draft"]})
return {"final": final}
# --- Routing ---
def should_rewrite(state: ArticleState) -> str:
if state["score"] >= 8.0:
return "publish"
elif state.get("rewrites", 0) >= 2:
return "publish" # Publish after max 2 rewrites even if score is low
else:
return "rewrite"
# --- Build the graph ---
graph = StateGraph(ArticleState)
graph.add_node("research", research_agent)
graph.add_node("brainstorm", brainstorm_agent)
graph.add_node("write", writer_agent)
graph.add_node("score", scorer_agent)
graph.add_node("rewrite", rewrite_agent)
graph.add_node("publish", publisher_agent)
graph.set_entry_point("research")
graph.add_edge("research", "brainstorm")
graph.add_edge("brainstorm", "write")
graph.add_edge("write", "score")
graph.add_conditional_edges(
"score",
should_rewrite,
{"publish": "publish", "rewrite": "rewrite"}
)
graph.add_edge("rewrite", "score") # loop back for re-scoring
graph.add_edge("publish", END)
app = graph.compile()

result = app.invoke({
"topic": "Why most enterprise AI projects fail in production",
"research": "",
"outline": "",
"draft": "",
"score": 0.0,
"feedback": "",
"rewrites": 0,
"final": ""
})
print("=== FINAL ARTICLE ===")
print(result["final"])
print(f"\nScore achieved: {result['score']}/10")
print(f"Rewrites needed: {result['rewrites']}")

Add web search (so the research agent uses real current data):

from langchain_community.tools import DuckDuckGoSearchRun
search = DuckDuckGoSearchRun()
def research_agent(state):
results = search.run(f"{state['topic']} 2025 latest developments")
# Pass results to LLM for synthesis
...

Add human review before publishing:

from langgraph.graph import interrupt
def human_review(state):
approval = interrupt({
"message": "Review the article before publishing",
"draft": state["draft"],
"score": state["score"]
})
if approval == "reject":
return {"feedback": "Human rejected β€” needs complete rewrite", "score": 0}
return state
graph.add_node("human_review", human_review)
# Insert between score and publish

Add LinkedIn API publishing (real posting):

import linkedin_api
def publisher_agent(state):
client = linkedin_api.Linkedin(email, password)
# Format and post directly to LinkedIn
post_id = client.post(state["draft"])
return {"final": f"Published: https://linkedin.com/feed/update/{post_id}"}

This research β†’ draft β†’ score β†’ rewrite β†’ publish loop works for any content creation pipeline:

Use caseResearchWriteScorePublish
Technical blog postsdocs, GitHub, papersarticlereadability, accuracydev.to, Medium
YouTube scriptstrends, competitor videosscriptengagement scorescript file
API docscode + testsdocumentationcompletenessdocs site
Code reviewschanged files, contextreview commentscoverage scorePR comment
Release notescommits, PRs, issueschangelogclarity scoreGitHub release

  1. Narrow specialisation beats one big prompt β€” a dedicated researcher produces better research than asking a writer to also research
  2. Scoring creates a quality feedback loop β€” without a scorer, you have no way to know if the output is good enough
  3. Limit rewrites β€” cap at 2-3 to prevent infinite loops, and always eventually publish
  4. Human checkpoints are cheap β€” a 30-second review before publishing is worth it for content that represents your professional brand