Orchestrating Browser Agents with LangGraph and Anchor

Hands On
Jun 9
by Idan Raman
Orchestrating Browser Agents with LangGraph and Anchor

Most browser agent tutorials show you a single agent completing a task. Production systems look nothing like that. Real workflows branch, retry, hand off between specialized sub-agents, and run dozens of sessions in parallel. LangGraph gives you the graph-based control flow to build these systems; Anchor gives you the browser infrastructure to run them reliably at scale.

This post walks through a concrete pattern: a LangGraph workflow where multiple Anchor browser sessions fan out in parallel, with conditional routing based on what each agent finds.

Why LangGraph for Browser Agents?

LangGraph models your agent workflow as a directed graph where nodes do work and edges route between them. This maps naturally to browser automation patterns:

  • A routing node decides which sub-agents to spawn based on the task
  • Browser nodes each own an isolated Anchor session and execute a specific goal
  • A reducer node aggregates results across parallel branches
  • Conditional edges handle retries, fallbacks, and early exits

Compare this to a flat Python async loop: with LangGraph you get built-in state management, resumability, and a visual graph you can inspect — without writing orchestration boilerplate yourself.

Setup

pip install langgraph langchain-anthropic anchor-browser playwright

Set your API keys:

export ANCHOR_API_KEY="..."
export ANTHROPIC_API_KEY="..."

Defining the Graph State

LangGraph passes a shared state object between nodes. For a browser agent graph, we track tasks, results, and errors:

from typing import TypedDict, Annotated
from langgraph.graph import StateGraph, END
import operator

class BrowserAgentState(TypedDict):
    tasks: list[dict]                       # Goals for each browser agent
    results: Annotated[list, operator.add]  # Merged across parallel branches
    errors: Annotated[list, operator.add]
    retry_count: int
    final_report: str

The Annotated[list, operator.add] tells LangGraph to merge lists from parallel branches rather than overwrite them — essential when multiple browser nodes write results simultaneously.

The Browser Node

Each browser node creates an isolated Anchor session, executes its task, and returns structured results:

import os
import asyncio
from anchor_browser import AnchorClient
from langchain_anthropic import ChatAnthropic

anchor = AnchorClient(api_key=os.environ["ANCHOR_API_KEY"])
llm = ChatAnthropic(model="claude-opus-4-8")

async def browser_node(config: dict) -> dict:
    task = config["task"]

    session = anchor.sessions.create(
        proxy_country="us",
        options={"adblock": True}
    )
    try:
        playwright = session.get_playwright()
        page = playwright.chromium.connect_over_cdp(
            session.ws_endpoint
        ).contexts[0].pages[0]

        await page.goto(task["url"])
        content = await page.inner_text("body")

        response = await llm.ainvoke([{
            "role": "user",
            "content": f"Task: {task['goal']}

Page content:
{content[:5000]}"
        }])

        return {
            "results": [{"url": task["url"], "output": response.content}],
            "errors": []
        }
    except Exception as e:
        return {
            "results": [],
            "errors": [{"url": task["url"], "error": str(e)}]
        }
    finally:
        session.close()

Wiring the Graph with Fan-Out

LangGraph's Send API lets you dynamically spawn one node per task — all running in parallel Anchor sessions:

from langgraph.constants import Send

def route_tasks(state: BrowserAgentState):
    """Fan out: one browser_node per task, all run in parallel."""
    return [
        Send("browser_agent", {"task": task})
        for task in state["tasks"]
    ]

def check_errors(state: BrowserAgentState) -> str:
    """Retry on failures, up to 2 times."""
    has_failures = len(state.get("errors", [])) > 0
    under_limit = state.get("retry_count", 0) < 2
    return "retry" if has_failures and under_limit else "aggregate"

async def aggregate_results(state: BrowserAgentState) -> dict:
    """Summarize all browser agent outputs into one report."""
    combined = "

".join(
        f"[{r['url']}]
{r['output']}" for r in state["results"]
    )
    summary = await llm.ainvoke([{
        "role": "user",
        "content": f"Synthesize these research findings into a concise report:

{combined}"
    }])
    return {"final_report": summary.content}

# Build the graph
graph = StateGraph(BrowserAgentState)
graph.add_node("browser_agent", browser_node)
graph.add_node("aggregate", aggregate_results)

graph.set_conditional_entry_point(route_tasks)
graph.add_conditional_edges(
    "browser_agent",
    check_errors,
    {"retry": "browser_agent", "aggregate": "aggregate"}
)
graph.add_edge("aggregate", END)

app = graph.compile()

Running It

import asyncio

async def main():
    result = await app.ainvoke({
        "tasks": [
            {"url": "https://competitor-a.com/pricing", "goal": "Extract all pricing tiers and features"},
            {"url": "https://competitor-b.com/pricing", "goal": "Extract all pricing tiers and features"},
            {"url": "https://competitor-c.com/pricing", "goal": "Extract all pricing tiers and features"},
        ],
        "results": [],
        "errors": [],
        "retry_count": 0,
        "final_report": ""
    })

    print(result["final_report"])

asyncio.run(main())

Three Anchor sessions spin up in parallel, each visits a different URL, and the results merge into a single competitive-pricing report. The whole flow completes in roughly the time of the slowest individual task — not the sum of all three.

What This Pattern Gives You

LangGraph handles control flow, state, and agent coordination. Anchor handles browser reliability, session isolation, anti-bot resilience, and fingerprint management. Neither tries to be the other.

In practice this means your graph logic stays clean — you write nodes and edges, not retry-on-bot-detection code. When Anchor's infrastructure handles a Cloudflare challenge or an unexpected redirect, your graph just sees a successful result. When a session genuinely fails, the conditional edge retries it automatically without restarting the full graph.

For teams moving beyond single-task scripts, this separation of concerns is what holds up under real production workloads — dozens of agents, diverse targets, and complex branching logic all managed cleanly in one place.

Build your first multi-agent browser workflow — start with Anchor for free →

Stay ahead in browser automation

We respect your inbox. Privacy policy

Welcome aboard! Thanks for signing up
Oops! Something went wrong while submitting the form.