← Back to DevBytes

Building a Content Writing Agent with LangGraph: Step-by-Step Tutorial

What is LangGraph?

LangGraph is an open-source framework from LangChain designed to build stateful, multi-step applications with large language models (LLMs). At its core, it lets you define a computation as a directed graph—where nodes represent actions or LLM calls, and edges represent the flow of data and logic between those actions. The graph maintains a shared state that is passed and updated at each node, enabling complex, agent-like workflows that can branch, loop, and make decisions dynamically.

Unlike simple chains that follow a linear sequence, LangGraph allows you to create cycles (e.g., for iterative refinement), conditional branching, and even human-in-the-loop checkpoints. It's perfect for orchestrating an AI agent that must coordinate multiple sub-tasks, such as researching a topic, planning an outline, drafting text, and editing the result.

Why Use LangGraph for Content Writing Agents?

Writing high-quality content is rarely a single-shot generation task. It involves multiple stages: gathering information, structuring ideas, drafting, and polishing. A content writing agent built with LangGraph provides:

Step-by-Step: Building a Content Writing Agent

Let’s build a complete content writing agent from scratch using LangGraph. We'll create a four-stage workflow: Research → Outline → Draft → Edit, with an optional refinement loop.

Step 1: Setting Up Your Environment

First, install the required packages. You'll need langgraph, langchain-core, and an LLM provider (we'll use OpenAI). In your terminal:

pip install langgraph langchain langchain-openai python-dotenv

Set your OpenAI API key as an environment variable or pass it directly. For simplicity, we'll load it from a .env file.

Step 2: Defining the Agent State

In LangGraph, the state is a shared data structure that flows through the graph. We'll use a Python TypedDict to define a strongly-typed state with all the fields our agent needs.

from typing import TypedDict, List, Optional

class ContentState(TypedDict):
    topic: str                      # The main topic for the content
    research_notes: str             # Raw research findings
    outline: str                    # Structured article outline
    draft: str                      # First draft of the content
    final_content: Optional[str]    # Polished final version
    editor_feedback: Optional[str]  # Feedback from the editing stage

Each node will return a dictionary with keys that should be updated in the state. LangGraph merges these partial updates automatically. For example, the research node returns {"research_notes": "..."}.

Step 3: Creating Node Functions

Nodes are Python functions (or LangChain runnables) that receive the current state and return an update. We'll build four nodes, each calling an LLM with a specific prompt.

Research Node

This node uses an LLM to gather background information about the topic. In a production agent, you might plug in a real search tool; here we simulate with an LLM call.

from langchain_openai import ChatOpenAI
from langchain_core.messages import HumanMessage

llm = ChatOpenAI(model="gpt-4o-mini", temperature=0.7)

def research_node(state: ContentState) -> dict:
    prompt = f"Research the topic '{state['topic']}' thoroughly. Provide key facts, statistics, and interesting angles."
    response = llm.invoke([HumanMessage(content=prompt)])
    return {"research_notes": response.content}

Outline Node

Based on research notes, the agent creates a structured outline with sections and subsections.

def outline_node(state: ContentState) -> dict:
    prompt = f"""Using the following research notes, create a detailed outline for an article.
Research notes:
{state['research_notes']}

The outline should include an introduction, main sections (each with bullet points), and a conclusion."""
    response = llm.invoke([HumanMessage(content=prompt)])
    return {"outline": response.content}

Drafting Node

This node writes the full first draft by expanding the outline into prose.

def drafting_node(state: ContentState) -> dict:
    prompt = f"""Write a complete, engaging first draft of an article based on the following outline and research notes.
Topic: {state['topic']}
Outline:
{state['outline']}

Research notes:
{state['research_notes']}

Write in a natural, informative style. The draft should flow smoothly from introduction to conclusion."""
    response = llm.invoke([HumanMessage(content=prompt)])
    return {"draft": response.content}

Editing Node

The editor reviews the draft, provides feedback, and optionally produces a polished final version. We'll also generate a quality score to decide whether to loop back for revisions.

def editing_node(state: ContentState) -> dict:
    prompt = f"""You are a meticulous editor. Review the following draft and provide constructive feedback.
Also decide if the draft meets quality standards: output a score from 1-10, where 1-7 means 'needs revision' and 8-10 means 'ready to publish'.
Draft:
{state['draft']}

Output format:
Feedback: (your detailed feedback)
Score: (number)
Final version: (if score >= 8, rewrite the draft incorporating fixes; if score < 8, output just the feedback without rewriting)"""
    response = llm.invoke([HumanMessage(content=prompt)])
    content = response.content

    # Simple parsing (in practice, use structured output)
    feedback = ""
    score = 0
    final_version = None

    if "Feedback:" in content:
        feedback = content.split("Feedback:")[1].split("Score:")[0].strip()
    if "Score:" in content:
        try:
            score_str = content.split("Score:")[1].split("\n")[0].strip()
            score = int(score_str)
        except:
            score = 0
    if score >= 8 and "Final version:" in content:
        final_version = content.split("Final version:")[1].strip()

    updates = {"editor_feedback": feedback}
    if final_version:
        updates["final_content"] = final_version
    return updates

Step 4: Building the Graph

Now we assemble the graph using LangGraph's StateGraph. We'll define nodes, a linear sequence, and a conditional edge for refinement.

from langgraph.graph import StateGraph, END

# Initialize graph with our state schema
builder = StateGraph(ContentState)

# Add nodes
builder.add_node("research", research_node)
builder.add_node("outline", outline_node)
builder.add_node("drafting", drafting_node)
builder.add_node("editing", editing_node)

# Add edges for the main pipeline
builder.add_edge("research", "outline")
builder.add_edge("outline", "drafting")
builder.add_edge("drafting", "editing")

# Conditional edge: after editing, decide next step
def should_revise(state: ContentState) -> str:
    # If final_content exists, we're done; else loop back to drafting
    if state.get("final_content"):
        return "end"
    return "drafting"

builder.add_conditional_edges(
    "editing",
    should_revise,
    {
        "end": END,
        "drafting": "drafting"
    }
)

# Set the entry point
builder.set_entry_point("research")

# Compile the graph
app = builder.compile()

Here, after editing, the graph evaluates the state: if a final version is already present, the process ends; otherwise it routes back to the drafting node to incorporate feedback. The should_revise function acts as the router. Note that the drafting node will run again with the updated state (including editor feedback) — you could modify drafting_node to use that feedback to improve the draft.

Step 5: Compiling and Running the Agent

With the graph compiled, you can invoke it like any runnable. Pass an initial state with at least the topic field.

initial_state = {"topic": "The Future of Renewable Energy in 2025"}
result = app.invoke(initial_state)

print("Final Content:", result.get("final_content"))
print("Editor Feedback:", result.get("editor_feedback"))

The agent will automatically execute the nodes in the defined order, possibly looping through drafting and editing multiple times until a publishable final content is produced. You can inspect the full state history by enabling tracing or simply printing intermediate states in each node.

Best Practices for LangGraph Content Agents

Conclusion

You’ve now built a complete, multi-stage content writing agent using LangGraph. The graph orchestrates research, outlining, drafting, and editing in a stateful, iterative workflow that can automatically refine drafts until they meet a quality threshold. This approach brings order to the otherwise chaotic process of generating long-form content with LLMs, making your application more reliable, maintainable, and capable of producing consistently high-quality writing. With the foundation in place, you can extend the agent with custom tools, human approval steps, and even parallel branches for different article sections — all while keeping a clean, graph-based architecture.

— Ad —

Google AdSense will appear here after approval

← Back to all articles