Skip to main content

Build an AI Research Agent: Automated Web Research with LangGraph and Apify (2026)

· 7 min read
Yassine El Haddad
Software Developer & Automation Specialist

I build production AI agents, web scrapers, and automation pipelines. Most of what I publish here comes from the actual problems they run into: proxies that get banned, anti-bot stacks that fingerprint your client, RAG that drifts when the underlying data moves. Stack: Python, TypeScript, Go, FastAPI, LangChain, Crawlee, Playwright, deployed on AWS, GCP, and Cloudflare.

An AI research agent automates the full loop: given a research question, it searches the web via Apify, fetches and reads pages, extracts key findings, and synthesizes a structured report. This guide walks you through building one with LangGraph and the Apify Python client.

Start building with Apify →

What the Agent Does

Given a research question (e.g. "What are the main risks of AI agents in production as of 2026?"), the agent:

  1. Searches — Calls Apify Google Search Scraper to get relevant URLs.
  2. Fetches — Uses Apify Website Content Crawler on each URL to extract markdown.
  3. Analyzes — An LLM (Claude Sonnet 4.6 or a comparable GPT model) reads each page and extracts key findings.
  4. Reports — Synthesizes all findings into a structured markdown report with sources cited.

If insufficient data is found after analysis, a conditional edge can route back to search with refined queries.

Architecture: LangGraph StateGraph

┌─────────┐ ┌──────────────┐ ┌─────────┐ ┌───────┐
│ START │───▶│ search │───▶│ fetch │───▶│analyze│───▶ report ──▶ END
└─────────┘ │ (Apify SERP) │ │ (Apify │ │ (LLM) │
└──────────────┘ │ Crawler)│ └───┬───┘
▲ └─────────┘ │
│ │ │
└──────────────────┴─────────────┘
(re-search if insufficient)

Prerequisites

  • Python 3.10+
  • Apify account (sign up free)
  • OpenAI or Anthropic API key
  • APIFY_API_TOKEN in environment
pip install langgraph langchain-anthropic langchain-openai apify-client

Complete Python Code

from typing import TypedDict
from langgraph.graph import StateGraph, END
from langchain_anthropic import ChatAnthropic
from langchain_openai import ChatOpenAI
from apify_client import ApifyClient
import os

# --- State ---
class ResearchState(TypedDict):
research_question: str
urls: list[str]
documents: list[dict]
analysis: list[dict]
report: str
iteration: int

# --- Node 1: Search ---
def search_node(state: ResearchState) -> dict:
"""Call Apify Google Search Scraper. Return top URLs."""
client = ApifyClient(os.environ["APIFY_API_TOKEN"])
query = state["research_question"][:200] # truncate for SERP
run_input = {
"queries": query,
"maxPagesPerQuery": 1,
"resultsPerPage": 10,
}
run = client.actor("apify/google-search-scraper").call(run_input=run_input)
items = list(client.dataset(run["defaultDatasetId"]).iterate_items())
urls = []
for item in items:
if "organicResults" in item:
for r in item["organicResults"][:5]:
if r.get("url"):
urls.append(r["url"])
return {"urls": urls[:10]} # top 10

# --- Node 2: Fetch pages ---
def fetch_pages_node(state: ResearchState) -> dict:
"""Crawl each URL with Website Content Crawler. Return markdown."""
client = ApifyClient(os.environ["APIFY_API_TOKEN"])
urls = state["urls"]
if not urls:
return {"documents": []}
start_urls = [{"url": u} for u in urls]
run_input = {
"startUrls": start_urls,
"maxCrawlPages": len(urls),
"maxCrawlDepth": 0,
"crawlerType": "cheerio",
}
run = client.actor("apify/website-content-crawler").call(run_input=run_input)
items = list(client.dataset(run["defaultDatasetId"]).iterate_items())
docs = [{"url": i.get("url", ""), "markdown": i.get("markdown", "")} for i in items]
return {"documents": docs}

# --- Node 3: Analyze ---
def analyze_node(state: ResearchState) -> dict:
"""LLM reads each doc, extracts key findings."""
llm = ChatAnthropic(model="claude-sonnet-4-6", api_key=os.environ.get("ANTHROPIC_API_KEY"))
# Or: llm = ChatOpenAI(model="gpt-4o", api_key=os.environ.get("OPENAI_API_KEY"))
docs = state["documents"]
question = state["research_question"]
analyses = []
for d in docs[:5]: # limit to 5 pages
if not d.get("markdown"):
continue
prompt = f"""Given this web page content, extract key findings relevant to: "{question}".
Cite the source URL: {d.get('url', 'unknown')}

Content:
{d['markdown'][:8000]}

Return: bullet points of findings, each with [Source: URL]."""
resp = llm.invoke(prompt)
analyses.append({"url": d["url"], "findings": resp.content})
return {"analysis": analyses}

# --- Node 4: Report ---
def report_node(state: ResearchState) -> dict:
"""Synthesize final report."""
llm = ChatAnthropic(model="claude-sonnet-4-6", api_key=os.environ.get("ANTHROPIC_API_KEY"))
analyses = state["analysis"]
question = state["research_question"]
combined = "\n\n".join([f"## Source: {a['url']}\n{a['findings']}" for a in analyses])
prompt = f"""Synthesize a structured markdown report for this research question:
"{question}"

Input findings (with sources):
{combined}

Output: Executive summary, main sections, conclusions, and a Sources section listing all URLs."""
report = llm.invoke(prompt)
return {"report": report.content}

# --- Graph ---
def should_continue(state: ResearchState) -> str:
"""Optionally re-search if insufficient content."""
if len(state.get("documents", [])) < 2 and state.get("iteration", 0) < 1:
return "search"
return "report"

graph_builder = StateGraph(ResearchState)
graph_builder.add_node("search", search_node)
graph_builder.add_node("fetch_pages", fetch_pages_node)
graph_builder.add_node("analyze", analyze_node)
graph_builder.add_node("report", report_node)

graph_builder.set_entry_point("search")
graph_builder.add_edge("search", "fetch_pages")
graph_builder.add_edge("fetch_pages", "analyze")
graph_builder.add_conditional_edges("analyze", should_continue, {"search": "search", "report": "report"})
graph_builder.add_edge("report", END)

graph = graph_builder.compile()

# --- Run ---
if __name__ == "__main__":
result = graph.invoke({
"research_question": "What are the main risks of AI agents in production as of 2026?",
"urls": [],
"documents": [],
"analysis": [],
"report": "",
"iteration": 0,
})
print(result["report"])

Output Format

The agent produces a markdown report with:

  • Executive summary
  • Main sections (findings grouped by theme)
  • Conclusions
  • Sources section (all URLs cited)

Example excerpt:

## Executive Summary
AI agents in production face three primary risks in 2026: ...

## 1. Hallucination and Data Drift
- [Source: https://example.com/...]
- ...

## Sources
- https://example.com/...

Handling Loops: Re-search if Insufficient Data

The conditional edge should_continue checks whether fewer than 2 documents were fetched. If so and iteration < 1, it routes back to search with refined behavior (you can add query refinement logic in the search node). Otherwise it proceeds to report.

Cost Per Research Run

ComponentEstimate
Apify Google Search Scraper~$0.002 per SERP page
Apify Website Content Crawler~$0.001–0.01 per page (depends on size)
Claude Sonnet 4.6~$3/1M input, ~$15/1M output
GPT-4o~$2.50/1M input, ~$10/1M output

A typical run (10 URLs, 5 pages analyzed, 2K tokens out): ~$0.05–0.15 in Apify credits + ~$0.03–0.10 in LLM costs. Scale up by increasing URLs or pages; watch Apify compute units and token usage.

Comparison: Approaches to AI Research

ApproachProsCons
LangGraph + Apify (this)Full control, loops, structured outputRequires Python and Apify/LangGraph setup
Claude + Apify MCPInteractive, no codeManual per-query; not batch
LangChain Apify pipelineSimpler, linearNo graph, no re-search loops
Make.com + ApifyNo-code, scheduledLess flexible logic

For automated, scheduled research with re-search logic, LangGraph + Apify is the strongest option. See the LangChain Apify content pipeline for a simpler linear scrape→summarize flow, or Claude real-time web access with Apify for interactive use.

Apify Affiliate Banner 728x90Apify Affiliate Banner 728x90Apify Affiliate Banner 300x50Apify Affiliate Banner 300x50
Next step

Clone the code, set APIFY_API_TOKEN and ANTHROPIC_API_KEY (or OPENAI_API_KEY), then run with your own research question. Try Apify free →

Frequently Asked Questions

apify/google-search-scraper for SERP URLs and apify/website-content-crawler for page content. Both are official Apify Actors with pay-per-usage pricing.

Yes. Replace ChatAnthropic with ChatOpenAI(model='gpt-4o') and set OPENAI_API_KEY. The prompts work with either provider.

Limit to 5–10 URLs per run. Use Apify's built-in concurrency controls. Add delays between crawls if hitting target site rate limits.

Yes. The should_continue conditional edge routes back to search when documents < 2 and iteration < 1. Extend logic for refined queries.

LangChain pipeline is linear: scrape → summarize → publish. This LangGraph agent adds search, multi-step analysis, re-search loops, and report synthesis.

LangGraph uses standard Python. Call the Apify client (apify-client) inside any node. No official LangGraph–Apify integration; the client works directly.

Common mistakes and fixes

Actor run times out or returns empty dataset

Reduce maxCrawlPages and maxItems. Check Apify credit balance. Use simpler Actors first (e.g. web-scraper) to validate.

LangGraph state graph fails to compile

Ensure all nodes return dict with keys matching State definition. Check add_edge/add_conditional_edge targets exist.