Multi-Agent Systems

How to Build a Researcher-Writer Agent Handoff Loop for Documentation

A useful research-to-writing handoff needs more than two agents taking turns. The writer needs a clear record of the topic, supporting evidence, citations, and any unresolved questions, while the workflow needs a defined way to decide whether to research again or finish.

This tutorial builds that two-agent documentation loop in Python with LangGraph. You’ll define shared state, route research to a writer, send unresolved conflicts back for review, and set a bounded loop.

One important limitation: LangGraph provides state and routing controls, but your application must decide which sources to trust, how to resolve conflicts, and what counts as an acceptable draft.

Digital illustration of cyberpunk cats in neon-lit urban scenes, engaged in AI-agent tasks such as coding, debugging, or analyzing data streams. The style features glowing outlines, high contrast, and a vibrant acid-color palette. No brand logos, no invented software screenshots, no legible text.: How to Build a Researcher-Writer Agent Handoff Loop for Documentation

Quick answer

For the short version, give the researcher and writer a shared, typed state contract. Carry the topic, evidence with citation details, unresolved conflicts, and draft status in that state; LangGraph nodes can read it and return updates.

After research, route to the writer when the evidence is ready, or back to research when your application’s conflict checks identify questions to resolve. After drafting, end the workflow only when your own acceptance checks pass—for example, required sections are present and claims have supporting citations.

Conditional edges can route based on state, while a Command can update state and choose the next destination.

Set a recursion limit as a separate safety measure. If the workflow exceeds it, LangGraph raises GraphRecursionError; that signals the limit was reached, not that the documentation is complete.

Review the state and routes before deciding what to change.

Step-by-step guide

Prerequisites and starting state

Before wiring the workflow, start with a Python project where LangGraph is available and choose one documentation topic with a clear scope. Write down what the researcher should investigate and what the finished documentation needs to cover, so both agents have the same target.

Next, decide which sources your application will accept and how it will handle conflicting evidence. Specify what counts as a resolved conflict, and define concrete acceptance checks for the draft, such as required topics being covered and supporting evidence being recorded.

These decisions belong in your application: LangGraph provides graph state and routing, but it does not define source-trust rules, conflict adjudication, or when documentation is complete. Keep those policies explicit before building the handoff, rather than expecting the agents or framework to infer them.

Sources: Use the graph API.

Define the shared handoff state

Give both agents one typed state contract so the handoff carries the topic, evidence, conflicts, and draft status—not just an unstructured prompt. LangGraph lets you define graph state with a TypedDict; nodes read that state and return updates.

from typing import TypedDict

class HandoffState(TypedDict):
    topic: str
    evidence: list[dict[str, str]]
    unresolved_conflicts: list[str]
    draft: str
    draft_status: str

Initialize the state with the agreed topic, empty evidence and conflict lists, an empty draft, and a status such as pending. Have the researcher return updates to evidence and unresolved_conflicts, keeping each evidence record structured with the claim and its citation details.

The writer can then read those fields and return updates to draft and draft_status.

Keep the field names consistent across both nodes. That makes it easier to inspect what crossed the handoff and spot missing research before drafting proceeds.

Sources: Use the graph API.

Add the researcher and writer nodes

Implement each agent as a graph node that reads the shared state and returns only the fields it updates. Keep the application-specific research and drafting logic in separate functions, so the handoff remains clear:

def researcher(state):
    result = gather_evidence(state["topic"])
    return {
        "evidence": result["evidence"],
        "unresolved_conflicts": result["unresolved_conflicts"],
    }

def writer(state):
    draft = draft_documentation(
        topic=state["topic"],
        evidence=state["evidence"],
        unresolved_conflicts=state["unresolved_conflicts"],
    )
    return {"draft": draft, "draft_status": "drafted"}

Here, gather_evidence and draft_documentation are functions you provide; they are not LangGraph APIs. Include citations in each evidence record, and have the writer flag unresolved conflicts rather than silently treating them as settled.

That gives the next routing step explicit material to review and keeps the draft tied to the agreed topic.

Sources: Use the graph API.

Route conflicts and define the finish condition

Add a routing function that reads the shared state and returns a destination. For example, after research, send unresolved conflicts back to the researcher with the specific questions to investigate; otherwise, send the evidence to the writer.

After drafting, end only when your application’s checks pass.

def after_research(state):
    return "research" if state["unresolved_conflicts"] else "writer"

def after_writer(state):
    return "finish" if state["draft_status"] == "accepted" else "research"

Connect these decisions with conditional edges, or use a node’s Command to update state and choose its next destination. Define what “accepted” means in your application—for example, required sections are present, claims have citations, and no unresolved conflicts remain.

Settle conflicts using your documented policy; the graph cannot decide which source is more trustworthy or whether the documentation is complete. If checks fail, record what needs correction so the next research pass has a clear target.

Sources: Use the graph API.

Bound the research-and-draft loop

Set a finite recursion limit when you invoke the compiled graph. It acts as a safety ceiling if the research-and-draft loop fails to reach its finish condition; it does not decide whether the documentation is good enough.

result = app.invoke(
    initial_state,
    config={"recursion_limit": 20},
)

Treat 20 as an example, not a universal setting. Choose a limit that allows the expected passes through your researcher and writer nodes, then test it against your workflow.

Confirm that ordinary runs end through the acceptance route you defined, rather than merely because the limit was reached.

If execution exceeds the configured limit, LangGraph raises GraphRecursionError. Review the state and routing conditions to find why the loop did not terminate; do not mark the draft complete just because the safety limit stopped execution.

Sources: Use the graph API.

Verify the handoff and completion behavior

Run a representative topic through the graph, then inspect the shared state to confirm that the researcher’s evidence and citations appear in the expected fields. Check that the writer receives those fields and produces a draft tied to the original topic.

Test both routing paths. With an unresolved conflict present, verify that your configured condition sends the workflow back to research; with conflicts resolved, verify that it reaches the writer.

Then test a draft that fails your acceptance checks and one that passes, confirming that the graph continues in the first case and ends only in the second.

Finally, check that a normal successful run ends through your application’s acceptance condition, not because it used up its recursion limit. The limit is a safety guard, not a measure of documentation quality; if it is exceeded, review the state and routing conditions rather than treating the draft as complete.

Sources: Use the graph API.

Handle a loop that exceeds its limit

If the graph raises GraphRecursionError, it exceeded the configured recursion limit. Treat that as a safety stop, not evidence that the draft passed your acceptance checks.

  1. Inspect the latest state, especially unresolved_conflicts and draft_status. Check whether a conflict is still being reported or the draft is still marked as needing work.
  2. Trace the state-based routes. Confirm that unresolved conflicts return to research and that the finish condition can become true when your acceptance checks pass.
  3. Correct any state or routing condition that keeps sending the graph around the loop. If the workflow needs more passes even when routing is correct, adjust the finite limit and test again; raising it alone can hide a loop that never reaches its finish condition.

Verify success by confirming that the graph ends through your application’s acceptance condition, not merely because execution stopped at the limit.

Sources: Use the graph API.

Next steps

Choose this pattern when you want research and drafting to remain separate while still sharing an explicit record of what was found and what remains uncertain. Keep source selection, conflict resolution, and draft acceptance as application-level policies; the graph can enforce the routes you define, but it cannot supply those judgments for you.

Before relying on the workflow, test both paths: unresolved conflicts should return to research, and a draft should end the run only when it passes your acceptance checks. If the recursion limit is reached, inspect the latest state and routing logic rather than treating the stop as approval.

Your next step is to run a representative documentation topic through those checks and adjust the workflow based on where it fails.