Multi-Agent Pipeline β LinkedIn Article Publisher
The Problem This Solves
Section titled βThe Problem This Solvesβ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.
The Pipeline
Section titled βThe Pipelineβ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 ScorerImplementation with LangGraph
Section titled βImplementation with LangGraphβLangGraph handles the conditional loop (rewrite if score is low) cleanly:
from typing import TypedDict, Annotatedimport operatorfrom langgraph.graph import StateGraph, ENDfrom langchain_openai import ChatOpenAIfrom langchain_core.prompts import ChatPromptTemplatefrom 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-scoringgraph.add_edge("publish", END)
app = graph.compile()Running the Pipeline
Section titled βRunning the Pipelineβ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']}")Extending the Pipeline
Section titled βExtending the PipelineβAdd web search (so the research agent uses real current data):
from langchain_community.tools import DuckDuckGoSearchRunsearch = 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 publishAdd 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}"}The Same Pattern Applied Elsewhere
Section titled βThe Same Pattern Applied ElsewhereβThis research β draft β score β rewrite β publish loop works for any content creation pipeline:
| Use case | Research | Write | Score | Publish |
|---|---|---|---|---|
| Technical blog posts | docs, GitHub, papers | article | readability, accuracy | dev.to, Medium |
| YouTube scripts | trends, competitor videos | script | engagement score | script file |
| API docs | code + tests | documentation | completeness | docs site |
| Code reviews | changed files, context | review comments | coverage score | PR comment |
| Release notes | commits, PRs, issues | changelog | clarity score | GitHub release |
Key Lessons
Section titled βKey Lessonsβ- Narrow specialisation beats one big prompt β a dedicated researcher produces better research than asking a writer to also research
- Scoring creates a quality feedback loop β without a scorer, you have no way to know if the output is good enough
- Limit rewrites β cap at 2-3 to prevent infinite loops, and always eventually publish
- Human checkpoints are cheap β a 30-second review before publishing is worth it for content that represents your professional brand